Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Library Evolution support #1257

Open
AMatecki opened this issue Nov 20, 2019 · 24 comments
Open

Library Evolution support #1257

AMatecki opened this issue Nov 20, 2019 · 24 comments

Comments

@AMatecki
Copy link

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
Copy link
Member

weissi commented Nov 20, 2019

@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
Copy link
Author

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
Copy link
Member

weissi commented Nov 20, 2019

@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
Copy link
Author

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
Copy link
Member

weissi commented Nov 20, 2019

@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 remove internal ExpressibleBy(Integer|Array)Literal conformances #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:

  • which platforms are you shipping binaries to actual customers for? (I only care about the ones that do or will contain SwiftNIO)
  • what is the distribution mechanism?
  • are you distributing the framework that is exporting SwiftNIO data types or functions separately from your app?
  • are there other people (outside of your organisation) that do not have access to the source code of the framework that exports SwiftNIO stuff who have to to link a binary framework?

@AMatecki
Copy link
Author

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
Copy link
Member

weissi commented Nov 20, 2019

@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
Copy link
Author

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
Copy link
Member

weissi commented Nov 20, 2019

@AMatecki got it, thank you!

@AMatecki
Copy link
Author

@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
Copy link
Member

weissi commented Nov 25, 2019

@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).

weissi added a commit to weissi/swift-nio that referenced this issue Dec 9, 2019
Motivation:

In SelectableEventloop, Heap, PriorityQueue, etc there were quite a few
phantom publics left which cause issues with apple#1257 and are confusing.

Modifications:

- remove the phantom publics
- move SelectableEventLoop to its own file

Result:

- clearer what's going on (public means public)
weissi added a commit to weissi/swift-nio that referenced this issue Dec 10, 2019
Motivation:

In SelectableEventloop, Heap, PriorityQueue, etc there were quite a few
phantom publics left which cause issues with apple#1257 and are confusing.

Modifications:

- remove the phantom publics
- move SelectableEventLoop to its own file

Result:

- clearer what's going on (public means public)
weissi added a commit to weissi/swift-nio that referenced this issue Dec 11, 2019
Motivation:

In SelectableEventloop, Heap, PriorityQueue, etc there were quite a few
phantom publics left which cause issues with apple#1257 and are confusing.

Modifications:

- remove the phantom publics
- move SelectableEventLoop to its own file

Result:

- clearer what's going on (public means public)
weissi added a commit that referenced this issue Dec 11, 2019
Motivation:

In SelectableEventloop, Heap, PriorityQueue, etc there were quite a few
phantom publics left which cause issues with #1257 and are confusing.

Modifications:

- remove the phantom publics
- move SelectableEventLoop to its own file

Result:

- clearer what's going on (public means public)
@6epreu
Copy link

6epreu commented Feb 20, 2021

@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
Copy link
Member

weissi commented Feb 21, 2021

@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:

  • Only do @_implementationOnly import GRPC... / NIO and other things that you want to import but should remain an implementation detail of your library
  • compile your framework with "Build Libraries for Distribution"
  • do not compile NIO/GRPC with "Build Libraries for Distribution" (they should be embedded in your library as an implementation detail)

@6epreu
Copy link

6epreu commented Feb 24, 2021

@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
Copy link
Member

weissi commented Feb 24, 2021

@6epreu very cool! Thanks for reporting back!

@shahzadmajeed
Copy link

shahzadmajeed commented Aug 7, 2023

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
Copy link
Contributor

Lukasa commented Aug 8, 2023

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
Copy link

@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
Copy link
Contributor

Lukasa commented Aug 11, 2023

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
Copy link

shahzadmajeed commented Aug 11, 2023

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
Copy link
Contributor

Lukasa commented Aug 14, 2023

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
Copy link

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
Copy link

bimawa commented Jul 3, 2024

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

@Lukasa
Copy link
Contributor

Lukasa commented Jul 5, 2024

In what way does the workaround not work?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants