Open AMatecki opened 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.
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
.
@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)?
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?
@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.
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.
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...@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 @inlinable
s, 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 @inlinable
s 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 :).
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.
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).
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:
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
@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 :)
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
@AMatecki got it, thank you!
@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:
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!
@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
).
@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
@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:
@_implementationOnly import GRPC...
/ NIO
and other things that you want to import but should remain an implementation detail of your library@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
@6epreu very cool! Thanks for reporting back!
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?
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?
@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).
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!
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.
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):
.swiftinterface
file?-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.
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):
- This might be a stupid question but is there no way to use xcframeworks without .swiftinterface file?
- 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.
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.
After update to xcode15, looks like this workaround not work anymore?
In what way does the workaround not work?
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?