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

Create XCFrameworks and retain project compatibility #3071

Merged
merged 25 commits into from
Dec 18, 2020

Conversation

elliottwilliams
Copy link
Contributor

@elliottwilliams elliottwilliams commented Oct 10, 2020

Hey! I want to propose an alternate xcframeworks implementation to #2801 and #2881. This PR fixes #3019 by combining built frameworks into an xcframework when supported.

I don't mean to steal @tmspzz's thunder here, just wanted to give us another implementation to consider. I'm trying to keep the PR small and retain full compatibility with existing projects.

I've tested this in a large iOS+watchOS project (the Yelp app) and on some smaller multiplatform projects. I'm sure there are edge cases I'm missing -- would love for others to try this branch out and let me know what you find 🙂

Summary of changes

  • By default, all frameworks built for a dependency will be merged into an xcframework in Carthage/Build.

    • Building the same dependency multiple times with different --platform is supported -- new build products will be merged into an existing xcframework.
    • Similarly, rebuilding a dependency replaces previous builds for that platform in the xcframework.
    • xcframework creation can be disabled with --no-create-xcframework, and is disabled by default on systems without Xcode 12.
  • No configuration changes required for existing dependencies to support xcframeworks.

    • The xcframework is created using -allow-internal-distribution , which prevents it from requiring the usual BUILD_LIBRARY_FOR_DISTRIBUTION=YES / SKIP_INSTALL=NO.
    • When building, Carthage extracts platform-specific frameworks to a temporary directory and passes that directory to Xcode. This allows frameworks to link against nested dependencies without adding xcframeworks to their project.
  • Xcode 12 is required when creating an xcframework

    • This allows Carthage to embed dSYMs and bcsymbolmaps into the xcframework bundle, so we don't have to do additional work to manage multiple dSYMs.
  • Version file (--cache-builds) support is complete.

    • Each entry in a version file still corresponds to one framework bundle, but frameworks contained in xcframeworks have additional container and identifier fields that describe their location within the .xcframework directory.
    • Cache tools like Rome can support xcframeworks by caching the container directory when present.
  • No copy-frameworks support. Users should embed xcframeworks directly in Xcode instead of using copy-frameworks.

@tmspzz
Copy link
Member

tmspzz commented Oct 10, 2020

Hey @elliottwilliams thanks for the great effort!

I'm not sure however now what to do with this. If I understand correctly this build one .xcframework per platform instead of a combined version (when needed). In the other PR the --create-xcframework flag was intended to support both combined and per platform version.

Can you think of a way of combining both PRs to get to finish line? I'm a bit wary of supporting both combined and singe xcframework packaging because it add a lot of complexity to an already complex project.

@elliottwilliams
Copy link
Contributor Author

elliottwilliams commented Oct 11, 2020

Can you think of a way of combining both PRs to get to finish line? I'm a bit wary of supporting both combined and singe xcframework packaging because it add a lot of complexity to an already complex project.

@tmspzz Yeah – I can see the value of building a combined xcframework, and agree with you there's no reason to support combined xcframeworks and one-per-platform xcframeworks. Let me see if I can refactor to make that work!

@elliottwilliams elliottwilliams changed the title Create XCFrameworks when necessary Create XCFrameworks and retain project compatibility Oct 13, 2020
@elliottwilliams
Copy link
Contributor Author

@tmspzz updated this to create a combined xcframework. I had to give up the behavior where it would automatically create an xcframework if build settings indicated lipo would fail, so I resorted to adding a flag and creating xcframeworks by default on Xcode 12+. I still agree with you that this is the right way to go – it makes the PR itself more complex but is a lot closer to how I'd expect this to work as a user.

@tmspzz
Copy link
Member

tmspzz commented Oct 14, 2020

@elliottwilliams thanks! I'll review asap

On top of being closer to the status quo for Carthage, this makes sense
for dependencies which build multiple, unrelated frameworks, where users
need control over which ones get linked.
@elliottwilliams
Copy link
Contributor Author

We can't easily write unit tests for this, because Carthage's version of Nimble needs to be upgraded to compile on Swift 5.2+, and that would introduce breaking changes to the tests. So I'd prefer to tackle that later if possible :)

We can write integration tests, though. I'd like to get the current suite of bats tests passing with --create-xcframework before merging.

@chrisballinger
Copy link

Just want to say that I'm really excited about this, and I like the strategy of making things just work by default out of the box for existing projects.

However, I'm not sure if -allow-internal-distribution is the right path for all scenarios. This flag doesn't seem well documented, all I could find was "Specifies that the created xcframework contains information not suitable for public distribution." from the CLI help, but this post makes it seem like it may make xcframeworks produced this way not compatible between Xcode versions.

I don't think it should be a blocker for merging this PR, but I think that longer term that frameworks produced via carthage archive should be automatically built with BUILD_LIBRARY_FOR_DISTRIBUTION=YES and not use the -allow-internal-distribution flag. Otherwise stable binary framework releases published by others might not be compatible between Xcode / Swift compiler versions.

@elliottwilliams
Copy link
Contributor Author

@chrisballinger good points! Definitely agree that there's value in looking at BUILD_LIBRARY_FOR_DISTRIBUTION in the future, but that it's an intentional non-goal for this PR.

My understanding is that from Apple's perspective, "distribution" means "a binary framework that you provide to users without source code". If you do ship source code, Apple wants you to use a Swift package, which is much more analogous to Carthage's model. So while BUILD_LIBRARY_FOR_DISTRIBUTION builds might be valuable for us in the future, the status quo is building an xcframework with only the local machine in mind.

@mman
Copy link

mman commented Nov 4, 2020

@elliottwilliams BUILD_LIBRARY_FOR_DISTRIBUTION generates a stable swift interface and binary compatible object files that guarantee that the framework can be linked into the app with potentially newer versions of swift. So theoretically speaking, if you carthage build your dependencies with old Xcode and then update to new Xcode and try to rebuild a project, it may fail until you also rebuild the .xcframework.

@elliottwilliams
Copy link
Contributor Author

@mman

So theoretically speaking, if you carthage build your dependencies with old Xcode and then update to new Xcode and try to rebuild a project, it may fail until you also rebuild the .xcframework.

Exactly, and that's the status quo behavior of Carthage 🙂. Using --cache-builds is the current workflow to handle scenarios like these, and this PR doesn't change that. You can run

carthage build --cache-builds

before you start building in Xcode to rebuild any out-of-date frameworks. (Depending on how your project is structured, you could even run this as a build phase!)

The version file logic will invalidate frameworks inside an xcframework unless:

  • they were built for module stability, or
  • they were built using the same compiler version

So if you care a lot about portability of built frameworks, you can set BUILD_LIBRARY_FOR_DISTRIBUTION=YES yourself, and Carthage will still do the right thing.

@shaps80
Copy link

shaps80 commented Dec 16, 2020

You should also checkout the Readme of this pull request how to setup your project, because the old copy frameworks script is also not needed anymore: https://github.com/elliottwilliams/Carthage/tree/xcframeworks#building-platform-independent-xcframeworks-xcode-12-and-above

So when I remove the workout and just run carthage update I still get a lipo failure so, I can't get the framework to build.
Does the framework provider need to make any changes on their end?

I also pulled this repo and did a make install in case I needed this version of carthage but it made no difference :(

@renep
Copy link

renep commented Dec 16, 2020

So when I remove the workout and just run carthage update I still get a lipo failure so, I can't get the framework to build.
Does the framework provider need to make any changes on their end?

Run carthage update --create-xcframework

@shaps80
Copy link

shaps80 commented Dec 16, 2020

Run carthage update --create-xcframework

Ahhh! Do I need to be on this branch?

EDIT: I assume I do. So it now built perfectly thank you so much.

I notice I have some pre-built frameworks (e.g. Firebase). Can I assume I should drag in the 2 xcframework’s I’ve now created and remove them from the copy phase. But everything else, remains as is?

@shaps80
Copy link

shaps80 commented Dec 16, 2020

Hmmm... so when I run carthage update —create-xcframework it looks like it obviously just does the right thing for the checkouts it can.

For pre-built frameworks, I still need to do a full carthage update (I assume not with the workaround?).

Trying it out, hopefully this works.

@shaps80
Copy link

shaps80 commented Dec 16, 2020

@renep thank you so much for your support/help here, really needed it.

Ok, so I’ve managed to get all the frameworks to compile as above which is great. Xcode also compiles for device.

However when building for iPhone 12 mini Simulator for example:

Failed to build module 'Onfido' from its module interface; the compiler that produced it, 'Apple Swift version 5.1.3 effective-4.1.50 (swiftlang-1100.0.282.1 clang-1100.0.33.15)', may have used features that aren't supported by this compiler, 'Apple Swift version 5.3.2 effective-4.1.50 (swiftlang-1200.0.45 clang-1200.0.32.28)'

This is a pre-built library, so Carthage isn’t building this one. I’m guessing this is unrelated?

I can see there are newer releases on their repo, I’ll try updating.

EDIT:
This didn't work :(

Also, when I run on device I'm getting an "Image not found" error for another framework Intercom. The only difference here is that previously it was included as embed and sign which I changed to do not embed since I assumed if I'm copying it, wasn't necessary. Keeping that enabled, fails to compile since its also pre-built and not for M1.

@CorbinMontague
Copy link

@elliottwilliams A few of my Carthage dependencies do not get an xcframework built when I run carthage bootstrap --create-xcframework --cache-builds --platform iOS. The one I'm looking at right now is Branch. The logs show that carthage is downloading the framework directly (Downloading ios-branch-deep-linking-attribution.framework binary at "Branch iOS SDK 0.36.0"). When I compare this to other carthage dependencies I have that successfully have an xcframework built, the logs says: Building scheme "Foo" in Foo.xcodeproj. Is there something that Branch needs to do to support being built as an xcframework by Carthage?

@elliottwilliams
Copy link
Contributor Author

elliottwilliams commented Dec 16, 2020

@elliottwilliams A few of my Carthage dependencies do not get an xcframework built when I run carthage bootstrap --create-xcframework --cache-builds --platform iOS. The one I'm looking at right now is Branch. The logs show that carthage is downloading the framework directly (Downloading ios-branch-deep-linking-attribution.framework binary at "Branch iOS SDK 0.36.0").

@CorbinMontague Downloading binary dependencies is a separate codepath, and is something I intend to address in a follow-up PR. The "Downloading at " is a giveaway that Carthage has found a binary version and is preferring it.

Branch's repo has source code available, so you should be able to pass --no-use-binaries and have Carthage build a fresh xcframework.

@elliottwilliams
Copy link
Contributor Author

For an app where I have a main target, and an app extension that depend on the same library, what settings should I set for embedding/signing?

@kcharwood See instructions in README.md on this branch: https://github.com/elliottwilliams/Carthage/blob/xcframeworks/README.md#building-platform-independent-xcframeworks-xcode-12-and-above

Signing shouldn't be any different than it has been. The only change wrt embedding is that you'll use a Copy Files phase instead of carthage copy-frameworks.

@Patrick-Kladek
Copy link

I'm using github "MessageKit/MessageKit" in my Cartfile and got the following error (running carthage update --create-xcframework):

*** Building scheme "MessageKit" in MessageKit.xcodeproj
Build Failed
	Task failed with exit code 65:
	/usr/bin/xcrun xcodebuild -project /Users/patrick/Developer/Medbee/Carthage/Checkouts/MessageKit/MessageKit.xcodeproj -scheme MessageKit -configuration Release -derivedDataPath /Users/patrick/Library/Caches/org.carthage.CarthageKit/DerivedData/12.3_12C33/MessageKit/3.4.2 -sdk iphoneos ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES archive -archivePath /var/folders/_h/n2d19nrj2dvbxljvt7jrrzmr0000gn/T/MessageKit SKIP_INSTALL=YES GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=NO CLANG_ENABLE_CODE_COVERAGE=NO STRIP_INSTALLED_PRODUCT=NO (launched in /Users/patrick/Developer/Medbee/Carthage/Checkouts/MessageKit)

This usually indicates that project itself failed to compile. Please check the xcodebuild log for more details: /var/folders/_h/n2d19nrj2dvbxljvt7jrrzmr0000gn/T/carthage-xcodebuild.5hCKLp.log

The last part of the log

SetOwnerAndGroup patrick:staff /Users/patrick/Library/Caches/org.carthage.CarthageKit/DerivedData/12.3_12C33/MessageKit/3.4.2/Build/Intermediates.noindex/ArchiveIntermediates/MessageKit/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/MessageKit.framework (in target 'MessageKit' from project 'MessageKit')
    cd /Users/patrick/Developer/Medbee/Carthage/Checkouts/MessageKit
    /usr/sbin/chown -RH patrick:staff /Users/patrick/Library/Caches/org.carthage.CarthageKit/DerivedData/12.3_12C33/MessageKit/3.4.2/Build/Intermediates.noindex/ArchiveIntermediates/MessageKit/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/MessageKit.framework

SetMode u+w,go-w,a+rX /Users/patrick/Library/Caches/org.carthage.CarthageKit/DerivedData/12.3_12C33/MessageKit/3.4.2/Build/Intermediates.noindex/ArchiveIntermediates/MessageKit/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/MessageKit.framework (in target 'MessageKit' from project 'MessageKit')
    cd /Users/patrick/Developer/Medbee/Carthage/Checkouts/MessageKit
    /bin/chmod -RH u+w,go-w,a+rX /Users/patrick/Library/Caches/org.carthage.CarthageKit/DerivedData/12.3_12C33/MessageKit/3.4.2/Build/Intermediates.noindex/ArchiveIntermediates/MessageKit/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/MessageKit.framework

RegisterExecutionPolicyException /Users/patrick/Library/Caches/org.carthage.CarthageKit/DerivedData/12.3_12C33/MessageKit/3.4.2/Build/Intermediates.noindex/ArchiveIntermediates/MessageKit/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/MessageKit.framework (in target 'MessageKit' from project 'MessageKit')
    cd /Users/patrick/Developer/Medbee/Carthage/Checkouts/MessageKit
    builtin-RegisterExecutionPolicyException /Users/patrick/Library/Caches/org.carthage.CarthageKit/DerivedData/12.3_12C33/MessageKit/3.4.2/Build/Intermediates.noindex/ArchiveIntermediates/MessageKit/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/MessageKit.framework

Touch /Users/patrick/Library/Caches/org.carthage.CarthageKit/DerivedData/12.3_12C33/MessageKit/3.4.2/Build/Intermediates.noindex/ArchiveIntermediates/MessageKit/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/MessageKit.framework (in target 'MessageKit' from project 'MessageKit')
    cd /Users/patrick/Developer/Medbee/Carthage/Checkouts/MessageKit
    /usr/bin/touch -c /Users/patrick/Library/Caches/org.carthage.CarthageKit/DerivedData/12.3_12C33/MessageKit/3.4.2/Build/Intermediates.noindex/ArchiveIntermediates/MessageKit/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/MessageKit.framework

** ARCHIVE FAILED **


The following build commands failed:
	CompileSwift normal arm64
	CompileSwiftSources normal arm64 com.apple.xcode.tools.swift.compiler
(2 failures)

Not sure if its relevant but MessageKit has a subdependency. Cartfile:

github "nathantannar4/InputBarAccessoryView" ~> 5.2.1

And Cartfile.private

github "Quick/Quick" ~> 2.0.0
github "Quick/Nimble" ~> 8.0.0

Has someone an idea what's wrong?

@renep
Copy link

renep commented Dec 17, 2020

Not sure if its relevant but MessageKit has a subdependency:

This could be the case. Try to checkout the MessageKit (and maybe the other dependencies) and see if can be build as xcframwork using: carthage build --archive --create-xcframework

I myself had to update some framework that I use to support xcframeworks.

@shaps80
Copy link

shaps80 commented Dec 17, 2020

@elliottwilliams / @renep sorry to be a pain. I have a closed-source dependency. I assume this is not something you can resolve, so if they don't support Apple Silicon, I'm essentially screwed?

EDIT:

I've taken the strip-frameworks approach and just added it as a step right after running carthage update.
Normally you'd strip them for archived builds bound for the app-store, but on M1 you'll need it even for dev since we can't have Intel references included. So this way, I strip them immediately since on Apple Silicon the Simulator's are ARM as well.

@Patrick-Kladek
Copy link

@renep Thanks for your help.

I tried that. I had to add --platform iOS so the project compiles but Carthage still showed an error message:

cd Developer/MessageKit
carthage build --archive --create-xcframework --platform iOS
*** xcodebuild output can be found in /var/folders/_h/n2d19nrj2dvbxljvt7jrrzmr0000gn/T/carthage-xcodebuild.Hmq82Z.log
*** Building scheme "InputBarAccessoryView" in InputBarAccessoryView.xcworkspace
*** Building scheme "Nimble-iOS" in Nimble.xcodeproj
*** Building scheme "Quick-iOS" in Quick.xcworkspace
*** Building scheme "MessageKit" in MessageKit.xcodeproj
Could not find any copies of MessageKit.framework. Make sure you're in the project's root and that the frameworks have already been built using 'carthage build --no-skip-current'.

Carthage/Build does contain 4 xcframeworks (the 3 dependencies + MessageKit.xcframework). Not sure if the error message from Carthage is a big issue. For me it looks like the xcframeworks were build successfully.

Bildschirmfoto 2020-12-17 um 15 09 32

However adding MessageKit as a dependency to my project results in the error message in my previous comment.

The same issue also happens for CTAssetsPicker (I have a fork which I maintain as official development is suspended). CTAssetsPicker uses PureLayout as a sub dependency so it's not specific to MessageKit.

PS: I'm using Version 3.0.0 of MessageKit if it helps reproducing.

@renep
Copy link

renep commented Dec 17, 2020

Could not find any copies of MessageKit.framework. Make sure you're in the project's root and that the frameworks have already been built using 'carthage build --no-skip-current'.

You can ignore this error message. The reason is that this pull request cannot create a zip file for the created binaries yet. As @elliottwilliams already mentioned, this will be part of an additional pull request.

…ures in common

This happens when an xcframework needs to be created. Since we're no
longer creating them automatically, a guided error message is the next
best thing.
@tmspzz
Copy link
Member

tmspzz commented Dec 18, 2020

Thanks @elliottwilliams. Off we go 🚀

@tmspzz tmspzz merged commit 5c4d4bb into Carthage:master Dec 18, 2020
@elliottwilliams elliottwilliams deleted the xcframeworks branch December 19, 2020 18:09
@EmDee
Copy link

EmDee commented Dec 19, 2020

@tmspzz Do you have a rough ETA for a new release?

@0x5e
Copy link

0x5e commented Dec 22, 2020

Thanks everyone!

@kientux
Copy link

kientux commented Dec 30, 2020

When I use both --cache-builds and --use-xcframeworks, Carthage won't create xcframeworks even when xcframeworks doesn't exist. It checks for .framework only and if the framework is built before, it assumes build is cached and skips building altogether.

Can Carthage check if .framework is available, skip build step, and run only xcframework step?

@elliottwilliams
Copy link
Contributor Author

When I use both --cache-builds and --use-xcframeworks, Carthage won't create xcframeworks even when xcframeworks doesn't exist. It checks for .framework only and if the framework is built before, it assumes build is cached and skips building altogether.

Can Carthage check if .framework is available, skip build step, and run only xcframework step?

@kientux not without additional code changes. My intention was that you'd need to clear out your Carthage/Build folder when first switching to xcframeworks.

Though I think it would be reasonable to have the version file logic only consider xcframeworks when --use-xcframeworks is passed, and ignore them when it isn't.

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

Successfully merging this pull request may close these issues.

Carthage builds fail at xcrun lipo on Xcode 12 beta (3,4,5...)