apple / swift-nio

Event-driven network application framework for high performance protocol servers & clients, non-blocking.
https://swiftpackageindex.com/apple/swift-nio/documentation
Apache License 2.0
7.91k stars 640 forks source link

Library Evolution support #1257

Open AMatecki opened 4 years ago

AMatecki commented 4 years ago

Hi, I'm using SwiftNIO 2.10.1 as a library attached to tvOS app's UI test to provide mocked backend. Since I'm distributing it as a framework I would really like not to have it rebuild with every new Swift version. To have it working SwiftNIO would have to support Library Evolution. I tried turning it on, but had a lot of errors like this: 'let' property '_storage' may not be initialized directly; use "self.init(...)" or "self = ..." instead in multiple files. I'm not sure how to fix it by myself. Is there a plan to have Library Evolution implemented in SwiftNIO?

weissi commented 4 years ago

@AMatecki are you exporting SwiftNIO types or functions as public API in your framework?

I would assume no and if that's the case, can you try @_implementationOnly import NIO?

We will still have a look into why it fails to compile.

AMatecki commented 4 years ago

Thanks for your suggestion, I'm new to working with Swift libraries, didn't knew about @_implementationOnly. Unfortunately I'm exporting some types from SwiftNIO, so I can't compile with @_implementationOnly.

weissi commented 4 years ago

@AMatecki Right. So right now, SwiftNIO's primary and only distribution mechanism is as a SwiftPM package and SwiftPM right now only supports source distribution. SwiftNIO also doesn't guarantee an ABI because in a source-only world, having an ABI guarantee doesn't mean much because everybody will recompile every time.

Are you making an .xcodeproj from SwiftNIO or are you depending on SwiftNIO using Xcode's SwiftPM Package support (File -> Swift Package -> Add Package Dependency)?

AMatecki commented 4 years ago

My library was created before Xcode's SPM support, so I'm creating .xcodeproj from command line. I could include SwiftNIO package to my testing target, but it would complicate my project's build pipeline, so I prefer to attach it as a framework. Would including it using Xcode change anything?

weissi commented 4 years ago

@AMatecki just to be clear: What you're doing is unsupported but given that you're using it in tests that may be fine. It's unsupported because you're relying on a stable binary interface (ABI) for a source-only package that is only guaranteeing stable APIs. This will only work if you're recompiling everything whenever you update SwiftNIO. So updates of the SwiftNIO containing framework without recompiling the binaries that use it may break.

But in order for you to make progress on this issue, here are a few options that you may want to do.

Options

There are three categories of options and you only need to pick one options out of one category. Each of the options should be able to resolve your immediate issue.

Not recommended: Relying on stable ABI (which isn't guaranteed)

  1. you just ignore the warning warning: module 'NIO' was not compiled with library evolution support; using it means binary compatibility for 'XYZ' can't be guaranteed and you're good as is. We already know that SwiftNIO doesn't guarantee binary compatibility. This option will not fulfil your 'not recompiling for every Swift version' requirement...
  2. If you don't like the warning and want to switch between Swift versions, you can replace all occurrences of @inlinable in the SwiftNIO codebase by nothing (just use search (for @inlinable) and replace (with nothing). Then you should be able to enable Library Evolution. (you'll need to use SwiftNIO's master branch for this to work as you need #1258). The good thing about this option is that without @inlinables, SwiftNIO pretty much has a stable ABI. It's still not guaranteed but it'll break way way less often.

Note that relying on a SwiftNIO having a stable ABI (which it doesn't) means that you cannot just update SwiftNIO without recompiling the clients (the binaries that use your framework) too. If you always recompile the clients after every update of the SwiftNIO framework, it will work just fine because you don't need or use the ABI.

If you were to pick the second option you might actually find that you can update the SwiftNIO containing framework even without recompiling the clients. That is because by removing all the @inlinables and enabling Library Evolution you kind of created a stable SwiftNIO ABI. It's still not guaranteed but it will probably work just fine even across updates :).

Pretty Recommended: Relying on stable ABI of a wrapper framework

If you were to create your own wrapper framework that does not export any SwiftNIO types but wraps everything necessary, then you could @_implementationOnly import NIO and enable Library Evolution ("Build Libraries for Distribution") for that wrapper framework only. That way, SwiftNIO becomes a "private dependency" and you wouldn't need to care anymore about ABI of the SwiftNIO bits because the wrapper framework becomes the ABI for whatever you use from NIO.

Note that you still need to give your wrapper framework a stable ABI which isn't trivial or obvious. If you don't want to give your wrapper framework a stable ABI, then it's not worth it and you can just pick one of the two options in the 'not recommended' section.

Actually Recommended: Use source distribution

The really recommended way is to depend on SwiftNIO using Xcode's package support only. That way you can't distribute frameworks but there's no real need for frameworks because SwiftNIO will be automatically embedded into the binary you're shipping (if you're even shipping a binary).

Which option to pick

I realise this is a lot of information and not everything might be 100% clear. Please feel free to ask further questions. If you would like some support on which option to pick, you'd need to tell us the answers to the following questions and we can help you:

AMatecki commented 4 years ago

Thanks for a very detailed answer. My usage of SwiftNIO is pretty limited and controlled mainly by myself, so I'll try going with Not Recommended point 2. In case of any issues I can always try going with Pretty Recommended. Actually Recommended is also doable, but I don't want to change our build pipeline just for minor convenience related to UI tests

weissi commented 4 years ago

@AMatecki sounds great! Just to be sure: If you were to use Xcode's package support you shouldn't need to change anything in your build pipeline. xcodebuild test should just work and automatically check out SwiftNIO from github. So assuming that your builder has github.com access it should work. But 'not recommended point 2' should work too, let us know how it goes :)

AMatecki commented 4 years ago

We're using a fork this tool for generating .xcodeproj: https://github.com/yonaskolb/XcodeGen That's why I can't add SPM package without additional work. I'll let you know if point 2 works for me

weissi commented 4 years ago

@AMatecki got it, thank you!

AMatecki commented 4 years ago

@weissi After removing @inlinable I was able to compile the framework with Library Evolution. However, I had a couple of errors after adding it to my tests:

Zrzut ekranu 2019-11-22 o 08 52 33

So I added public to those types, which forced me to mark a lot more types to be public. Then it worked :) Having unnecessary public types is not a problem for me, since it's just for running mocked UI tests. Thanks for your help!

weissi commented 4 years ago

@AMatecki thanks for reporting back! Glad you got it fixed, we'll try to make an effort to support this better (without making stuff public).

6epreu commented 3 years ago

@AMatecki are you exporting SwiftNIO types or functions as public API in your framework? I would assume no and if that's the case, can you try @_implementationOnly import NIO? We will still have a look into why it fails to compile.

Could somebody provide an example how to use this import? I have approximatelly the same situation. Im developing the xcframework which force me to compile with BUILD_LIBRARY_FOR_DISTRIBUTION=yes

My dependencies added over cocoapods but I did not call any import of NIO (actually I have GRPC-swift dependency which relies on NIO) I have the same error on compilation time 2021-02-20_14-56-38

weissi commented 3 years ago

@6epreu You should be able to get this to work if you do @_implementationOnly import GRPC... instead of just import GRPC... and you should build NIO & GRPC not in library evolution mode.

So in short:

6epreu commented 3 years ago

@weissi thanks for explanation

I have added this to Podfile of my Framework and also @_implementationOnly import should be used where necessary

post_install do |installer| installer.pods_project.targets.each do |target| puts "#{target.name}" if target.name == "gRPC-Swift" || target.name == "SwiftNIO" || target.name == "SwiftNIOConcurrencyHelpers" || target.name == "SwiftNIOExtras" || target.name == "SwiftNIOFoundationCompat" || target.name == "SwiftNIOHPACK" || target.name == "SwiftNIOHTTP1" || target.name == "SwiftNIOHTTP2" || target.name == "SwiftNIOSSL" || target.name == "SwiftNIOTLS" || target.name == "SwiftNIOTransportServices" target.build_configurations.each do |config| config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'NO' end end end end

weissi commented 3 years ago

@6epreu very cool! Thanks for reporting back!

shahzadmajeed commented 1 year ago

Hi, I'm in the same boat. Using Vapor (that uses NIO) via. Tuist.

My generated project have BUILD_LIBRARY_FOR_DISTRIBUTION=YES for the wrapper framework that uses Vapor but sets BUILD_LIBRARY_FOR_DISTRIBUTION=NO for Vapor itself. I am also using @_implementationOnly import Vapor in addition to that but my XCFramework command still fails with following error:

Creating NIOConcurrencyHelpers.xcframework...
No 'swiftinterface' files found within ..Path_To_Project/MyProject/TuistWorkspace/Projects/GamingXamarin/Build/archives/ios_simulators.xcarchive/Products/Library/Frameworks/NIOConcurrencyHelpers.framework/Modules/NIOConcurrencyHelpers.swiftmodule'.

Any idea how can this be solved?

Lukasa commented 1 year ago

You appear to be building an xcframework for NIOConcurrencyHelpers. That's unlikely to be the right thing to do: you should statically link that into Vapor. How are you running the build?

shahzadmajeed commented 1 year ago

@Lukasa Sorry for long read below but it will give you a better picture:

We have Xamarin app that uses some native code via. XCFrameworks. Until now, we had been embedding all of our swift dependencies as XCFrameworks as well (.app/Frameworks directory).

Xamarin-Bindings

Here is our current script (before we added Vapor):


# Archive for iOS Devices
xcodebuild archive \
 -workspace $WORKSPACE \
 -scheme "$SCHEME \
 -showBuildTimingSummary \
 -derivedDataPath $DERIVED_DATA \
 CODE_SIGN_IDENTITY= \
 CODE_SIGNING_REQUIRED=NO \
 BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
 SKIP_INSTALL=NO" \
  -sdk iphoneos \
  -destination "generic/platform=iOS" \
  -archivePath "Build/archives/ios_devices.xcarchive"

# Archive for iOS Simulators
xcodebuild archive \
 -workspace $WORKSPACE \
 -scheme "$SCHEME \
 -showBuildTimingSummary \
 -derivedDataPath $DERIVED_DATA \
 CODE_SIGN_IDENTITY= \
 CODE_SIGNING_REQUIRED=NO \
 BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
 SKIP_INSTALL=NO" \
 -sdk iphonesimulator \
  -destination "generic/platform=iOS Simulator" \
  -archivePath "Build/archives/ios_simulators.xcarchive"

# Create XCFrameworks
find $BuildDir/archives/ios_devices.xcarchive/Products/Library/Frameworks -name '*.framework' > 
while read p; do
  BaseFrameworkName=$(basename $p .framework)
  FrameworkName=$BaseFrameworkName.framework
  XCFrameworkName=$BaseFrameworkName.xcframework
  DeviceArchive=$BuildDir/archives/ios_devices.xcarchive
  SimulatorArchive=$BuildDir/archives/ios_simulators.xcarchive
  DeviceSymbols=$DeviceArchive/dSYMS/$FrameworkName.dSYM
  DeviceSymbolsSwitch=""
  SimulatorSymbols=$SimulatorArchive/dSYMS/$FrameworkName.dSYM
  SimulatorSymbolsSwitch=""
  if [ -d "$DeviceSymbols" ]; then
    DeviceSymbolsSwitch="-debug-symbols $DeviceSymbols"
  fi
  if [ -d "$SimulatorSymbols" ]; then
    SimulatorSymbolsSwitch="-debug-symbols $SimulatorSymbols"
  fi
  echo
  echo Creating $XCFrameworkName...
  xcodebuild -create-xcframework \
  -framework $DeviceArchive/Products/Library/Frameworks/$FrameworkName \
  $DeviceSymbolsSwitch \
  -framework $SimulatorArchive/Products/Library/Frameworks/$FrameworkName \
  $SimulatorSymbolsSwitch \
  -output $BuildDir/xcframeworks/$(basename $XCFrameworkName)
done

Now, we need to depend on Vapor/SwiftNIO and only way to use that code in our Xamarin app is via. binary dependency.

So for now, in order to build the xcframeworks I have disabled library evolution (by removing BUILD_LIBRARY_FOR_DISTRIBUTION=YES from above script) for all frameworks (internal & external) but xcodebuild -create-xcframework was failing to find .swiftinterface files. I have solved that problem with an additional flag -allow-internal-distribution to xcodebuild -create-xcframework but i'm not convinced that it is a proper solution (as it sounds like this shouldn't be used in production or it will add some metadata to the frameworks which can increase binary size - all my speculations from it's documentation).

So, i'm trying to find a better solution...

Now, I really don't care about library evolution part as we can always rebuild our XCFrameworks and Xamarin code for each release of our app. But the problem is that xcodebuild -archive command doesn't generate .swiftinterface files when we removed BUILD_LIBRARY_FOR_DISTRIBUTION=YES and now I cannot generate xcframeworks anymore. All I need is a way to create XCFrameworks that I can use in my Xamarin app.

Another thing i'm trying to do, in parallel, to solve this problem is basically try to create one XCFramework with a static library that embeds/copies all code from it's dependencies so that XCFramework won't even care about interface files from dependencies but no luck so far. I don't know if its even possible.

I appreciate any feedback/help in this situation. Sorry for long comment again!

Lukasa commented 1 year ago

So the core of your problem here is that you're trying to reconcile two impossible tasks. Xamarin wants you to use Xcframeworks, which require library evolution to be enabled (because you need .swiftinterface files). However, your dependencies don't support being compiled in that mode.

The best solution is to build a framework that hides those dependencies statically within it, and does not expose any of their types in the interface. That framework can be built with library evolution mode, and it keeps your Swift packages as an implementation detail.

shahzadmajeed commented 1 year ago

Before I talk about suggested approach, I have few questions (unrelated to NIO but maybe you know the answers or if you can direct me where to ask that would be great):

  1. This might be a stupid question but is there no way to use xcframeworks without .swiftinterface file?
  2. Any idea what are the side effects of archiving frameworks with -allow-internal-distribution, if any, on the client apps?

On your suggested approach: Yes, i'm heading that direction but don't know how to do that so far. Should the wrapper framework by dynamic or should it also be static?

My wrapper framework DomainKit, Vapor and other implicit dependencies (NIO etc..) are all xcode projects generated via. Tuist so it is easy to override their build settings.

DomainKit Build Settings:

MACH_O_TYPE=mh_dylib
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
SKIP_INSTALL=NO

Vapor and other implicit dependencies:

MACH_O_TYPE=staticlib
BUILD_LIBRARY_FOR_DISTRIBUTION=NO
SKIP_INSTALL=NO

DomainKit private imports vapor via @_implementationOnly import Vapor and calls vapor/swiftnio etc but doesn't expose anything. So, I just hope that linker will copy all of the needed code from Vapor and other dependencies into DomainKit which I can distribute as an XCFramework to Xamarin.

So far generated DomainKit doesn't contain any code from Vapor or NIO family and app crashes.. still looking into how to create this single static xcframework.

Lukasa commented 1 year ago

Before I talk about suggested approach, I have few questions (unrelated to NIO but maybe you know the answers or if you can direct me where to ask that would be great):

  1. This might be a stupid question but is there no way to use xcframeworks without .swiftinterface file?
  2. Any idea what are the side effects of archiving frameworks with -allow-internal-distribution, if any, on the client apps?

For these questions I'd recommend asking on the Apple Developer forums, where you'll be able to get more-expert eyes.

Yes, i'm heading that direction but don't know how to do that so far. Should the wrapper framework by dynamic or should it also be static?

It doesn't much matter, both will work.

shahzadmajeed commented 1 year ago

For these questions I'd recommend asking on the Apple Developer forums, where you'll be able to get more-expert eyes.

Sure, will do that..

It doesn't much matter, both will work.

I have successfully created a static xcframework via. cocoapods in a test project. I think I will have a working solution in my real project soon.

Thank you for all the guidance and help on this forum.

bimawa commented 2 months ago

After update to xcode15, looks like this workaround not work anymore?

Lukasa commented 2 months ago

In what way does the workaround not work?