TL;DR
Currently, when using Swift Package Manager packages in Xcode, SPM compiles the package with reference to the name of the Build Configuration and automatically selects whether to compile with debug or release, which determines the compilation flag like DEBUG
and This determines the architecture of the final binary. This automatic selection may cause problems when using a custom Build Configuration in Xcode other than the default “Debug” and “Release”.
Right now (October 2022) there is no particularly good way to map the Build Configuration in Xcode to the SPM build environment. Hopefully, future versions of Xcode and SPM will be improved.
For some examples in the text, you can find the source code here.
Compile conditions in Xcode and SPM
Default DEBUG build conditions
In Xcode, when creating a project we are automatically given two Build Configuration: Debug and Release.
In SWIFT_ACTIVE_COMPILATION_CONDITIONS
, Debug Configuration predefines the DEBUG
condition.
This allows us to use code like the following to compile different content at Debug and Release time.
Starting with Xcode 11, we can add frameworks directly in Xcode using SPM. In the package, we can also use the same code to differentiate.
For observation purposes, these results can be placed on the UI.
Running it with Xcode’s default Debug Configuration gives you the following result, and everything looks great.
Custom compilation conditions
However, this DEBUG
condition in Package is not implemented by passing SWIFT_ACTIVE_COMPILATION_CONDITIONS
from the Xcode project to the SPM. To verify this, we can add a new condition to Xcode, such as CUSTOM
.
Similar to #if DEBUG
, add an attribute to CUSTOM
as well.
Unfortunately, this CUSTOM
condition does not work in the package.
If you do some confirmation on the build log, you can see that DEBUG
and CUSTOM
are correctly passed to the build command for the app target. However, when compiling the package, the condition given in is:
In Xcode 14.0, the conditions passed in are SWIFT_PACKAGE
, DEBUG
and Xcode
; CUSTOM
is not listed here.
At the time of writing, SPM only provides two Build Configuration, .debug
and . release
.
SPM itself supports custom conditions for a Configuration, and for packages that you have control over package, we can pass this condition by adding swiftSettings
to Package.swift.
For external packages that are added directly from the git repository, the contents are locked by default. If you just need to temporarily pass in a compile condition, you can convert it to a local package by convert it to a local package and then add swiftSettings
to it by doing something similar to the above. If a long-term solution is needed, consider wrapping the required external packages again yourself: create a new Swift package that depends on these external packages, and then add the appropriate swiftSettings
when exposing them.
As package maintainers, if we use compile conditions other than
DEBUG
in our packages, it is best to add them in Package.swift accordingly. Xcode will respect these settings when the user compiles your package using Xcode.
Determination based on Build Configuration
When Xcode chooses to use .debug
to build SPM packages, it “automatically” passes in DEBUG
according to Xcode’s general build conditions. But when does Xcode choose to use .debug
and when does it choose to use .release
?
The answer may be a surprise. In the Xcode environment, Xcode selects the build configuration used for SPM packages based on the name of the Build Configuration. Specifically, the rules found for now are.
- If the name contains
Debug
orDevelopment
(case-insensitive), then Xcode will use.debug
to build the SPM package. For example, the defaultDebug
, as well asDevelopment
,Debug_Testing
,_development_
,Not_DEBUG
, andhello development
are listed here. - Otherwise, use
.release
for compilation. For example, the defaultRelease
, as well as things likeDev
,Testing
,Staging
,Prod
,Beta
,QA
,CI
, etc., all use.release
as the compilation configuration.
Xcode has chosen to be “empirical” and presumptuous here, and when SPM is used in Xcode, the name Custom Build Configuration becomes a joke. When you painstakingly configure a Testing
build configuration for your project with the intention of running tests exclusively, you find that the Swift packages compiled under this configuration are optimized and stripped of testable support. To get SPM to work as intended, you must change the name of the Build Configuration in Xcode back to, say, Debug_Testing
.
These rules are written in the Xcode build toolchain, they are not open source, and there is no documentation on the matter, so they are subject to change in the future. It is safer to just use the default Debug
and Release
Build Configuration, and when more environments are needed (e.g. to set different bundle ids or app names for different environments), it may be an option to use multiple schemes and configure them with the appropriate environment variables to to differentiate between them.
Compilation architecture and Apple Silicon
In addition to the DEBUG
flag, Xcode automatically selects the architecture to be compiled for a package based on .debug
and .release
after selecting the build configuration for the SPM package. For the .release
configuration, the situation is relatively simple: ONLY_ACTIVE_ARCH
is set to false, compiling binaries for multiple architectures according to the Standard Architecture defined by the current Xcode version; for .debug
, ONLY_ACTIVE_ARCH
is set to true for .debug
, which determines a build architecture based on the mac device and the target device (emulator or real machine).
Troubleshooting arm64-induced problems on the emulator
In the days of Apple Silicon, by default Xcode would run with the arm64 architecture. At this point, the included iOS simulator will also run under arm64. If you use some old libraries in your project that are released in binary, such as a framework made by fat binary, or .a files that do not contain the emulator arm64, then it is likely that you will see an error like this when linking to the emulator as a target on an Apple Silicon mac.
building for iOS Simulator, but linking in object file built for iOS, for architecture arm64
This is because although arm64 is included in the library, it is marked as being for use on real devices and not the simulator. The common approach on the web will teach you to add arm64
for the simulator in EXCLUDED_ARCHS
, which is used to exclude this architecture.
|
|
This is a “quick fix” that will get you up and running. But you need to be aware of the downside of doing this: because arm64 is excluded, the only architecture option on the iOS simulator is x86_64. This means that your entire app will compile in x86_64 and then run on the x86_64 emulator. On Apple Silicon’s mac, this emulator is actually run using Rosetta 2, which means a significant performance drop.
Even more damaging is the fact that this method is even more problematic when used with SPM.
Because Xcode will not pass the EXCLUDED_ARCHS
you set to SPM, you will have problems compiling against the simulator like this.
Could not find module ‘MyLibrary’ for target ‘x86_64-apple-ios-simulator’; found: arm64-apple-ios-simulator
For .debug
, ONLY_ACTIVE_ARCH
is true and the compilation target is the arm64 iOS simulator, so SPM will only give the compilation result for the arm64-apple-ios-simulator version. But the project itself has EXCLUDED_ARCHS
arm64 set, and it’s actually the x86_64 simulator version of the package it needs when linking packages. Boom!
For older binary dependencies, the best thing to do is to urge the maintainer to adapt the xcframework quickly.
Another possible option is to hack the binary and modify the target field of the arm64 slice to “trick” Xcode into thinking that the binary is for the simulator. This approach is explained in detail here, and the author has also published a related arm64-to-sim tool, which can be used temporarily at your discretion if needed.
Accidents and accidental overlays
Understanding how SPM picks Build Configuration in Xcode and how the build architecture relates, we can “solve” the above problem by “fighting fire with fire”.
The easiest way to do this is to change the name of the Build Configuration in Xcode, e.g. change Debug
to Dev
. This way, SPM will pick .release
to compile the Swift package, and it will then compile all supported architectures. In the app target, even if we exclude arm64, the link will still be there because the x86_64 Swift package is compiled, so we can find the required architecture and link it properly.
This practice of using one “accident” to correct another “accident” is silly, but it is also effective.
The biggest side effects are two.
- because the package is compiled using
.release
, it requires not only compiling unnecessary architecture, but also additional compilation optimizations, which will slow down the package compilation speed. - because the package is optimized by release, debugging becomes difficult: for example, breakpoints set in the package may not work, the output of
po
may have problems, etc.
Summary
To get to the root of these problems, the SPM in Xcode needs to provide some means by which we can map Xcode’s Build Configuration (including the various build flag settings) to the SPM’s Build Configuration. The community envisioned Package Flavors was envisioned by the community to solve this problem, but the topic requires an official Apple modification as it involves the Xcode implementation. Unfortunately, however, we have not seen a public and positive response from Apple on this yet.
Until a mature solution is available, we are quite limited in what we can do, to summarize.
- Try not to customize the name of the Build Configuration. If you do need to change it, understand how the name of the build configuration may affect the SPM compilation.
- If you need to use binary libraries, try to use the xcframework format that includes all architectures. If not provided, consider using arm64-to-sim to convert the arm64 compiled for the device to arm64 for the emulator.
- If you can’t get around it the usual way, you can create your own wrapper package and pass the required compilation parameters in Package.swift.
- If that’s not possible on Apple Silicon, you can try running Xcode with Rosetta as a temporary solution.
Ref
https://onevcat.com/2022/10/spm-in-xcode/