CMake, vcpkg, and universal builds

Again, none of this is really my choice: our project is multi-platform (specifically, at this time, Windows and macOS). As a result, the people who started 8 hours before I did picked CMake and vcpkg to handle build system generation and 3rd party dependencies. (TBF, I can't blame them for this, since this does work on a Mac, for at least a simple build.)

I want to support Apple Silicon, obviously. I can build native on my M1 MBP. (I could, theoretically, use lipo and create a universal bundle, but that would mean manually signing a handful of executables, and then the whole thing, unless I missed a way to do this much more easily?) We're using CircleCI for CI, or maybe github actions in the future.

I have not been able to figure out how to use CMake and vcpkg to do a cross build. Not even universal, just "build for arm64 on Intel mac". googling and searching these fora hasn't shown a lot that seems to work (mainly, I think, for the vcpkg side). Which is weird, because one of the things people use CMake for is to build Android and iOS apps, which generally does mean cross-compiling. Does anyone know how to do that?

I'm at the point where I'm looking at CocoaPods again -- but that will not work with CMake, so we'll have two completely different build systems, one of which requires a Mac with GUI to add/remove sources/targets.

Replies

There are some hints about makefiles in the Apple silicon documentation.

Cmake, not make. And vcpkg. Alas.

I am very familiar with using Apple‘s tool, including Xcode and make, to build universal binaries of all sorts. I have been for a while. It’s this third-party stuff I need help with.

The important Clang compiler flags for cross-compiling are: --target; --sysroot; and -isysroot.

For CMake specifically, the relevant variables for cross-compiling are: CMAKE_C_COMPILER_TARGET; CMAKE_CXX_COMPILER_TARGET; CMAKE_SYSTEM_PROCESSOR; and CMAKE_SYSTEM_NAME.

For example, to build for x86_64 on an M1 Mac (arm64):

export TARGET="x86_64-apple-darwin"
export CFLAGS="$CFLAGS --target=$TARGET"
export CXXFLAGS="$CXXFLAGS --target=$TARGET"
export SDKROOT="$(xcrun --sdk macosx --show-sdk-path)"
cd mycode
mkdir build
cd build
cmake -DCMAKE_C_COMPILER_TARGET="$TARGET" -DCMAKE_CXX_COMPILER_TARGET="$TARGET" -DCMAKE_SYSTEM_PROCESSOR="x86_64" -DCMAKE_SYSTEM_NAME="Darwin" -DCMAKE_C_FLAGS="$CFLAGS" -DCMAKE_CXX_FLAGS="$CXXFLAGS" ..

Note if you set the SDKROOT environment variable, it is not necessary to add "--sysroot $SDKROOT" and "-isysroot $SDKROOT" to CFLAGS and CXXFLAGS.

Do that for each (x86_64 and arm64) then use lipo to make universal binaries.

Edit: Oh and, with CMake, never set the "--target" flag in LDFLAGS, only in CFLAGS/CXXFLAGS, it won't like that.

I'd never heard of vcpkg before you mentioned it.

I don't think you need to sign before doing lipo. You can wrap the whole thing into a script and easily do the lipo first and then sign the universal app.

I'm afraid the most likely explanation is that your build scripts need significant changes. I haven't encountered any cross platform incompatibilities on the several cmake-based projects I use. Getting cmake to generate iOS code is more difficult. There is a custom config script floating around that is designed specifically for iOS. (See https://fossies.org/linux/opencv/platforms/ios/cmake/Modules/Platform/iOS.cmake

Here are the custom configuration options I specify in my code:

CMAKE_INSTALL_PREFIX (You probably don't need this one)

CMAKE_C_COMPILER (Set to /path/to/Xcode/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang)

CMAKE_AR (Set to /path/to/Xcode/Toolchains/XcodeDefault.xctoolchain/usr/bin/ar)

CMAKE_LINKER (Set to /path/to/Xcode/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld)

CMAKE_TOOLCHAIN_FILE (Set to the iOS.cmake file mentioned above, but only for iphoneos builds).

IOS_PLATFORM=OS (As opposed to SIMULATOR, again, only for iphoneos builds)

Otherwise, aside from being 8 hours too slow, I don't necessarily think you are doing anything wrong. I don't even use the above build settings anymore. One of my most important projects recently switched to cmake and that was the impetus I needed to set VERBOSE=1 and convert everything from automake, cmake, jam, ninja, and gn to Xcode. Now it works great and builds on all platforms, even in Xcode Cloud.

Over, on twitter, Alexander Neumann (@Iluinrandir) gave me the solution. I used

env PATH=${HOME}/vcpkg-arm64:${PATH} cmake -G Xcode .. -DVCPKG_HOST_TRIPLET=x64-osx -DVCPKG_TARGET_TRIPLET=arm64-osx -DCMAKE_TOOLCHAIN_FILE=${HOME}/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_OSX_ARCHITECTURES=arm64 -DMACOS_BUILD_RELEASE=ON

to create the xcodeproj, and then time cmake --build . --config Release -- -allowProvisioningUpdates to build. And... I ended up with arm64 executables that work on my M1 MBP.

I'm not sure if the -DCMAKE_OSX_ARCHITECTURES=arm64 is necessary or not.

Now, this still leaves me wondering how to combine them into a single bundle. I mean, I know how to script the part where I lipo the various parts together, but I'm not sure how to get the proper signing done, other than copying the output of xcodebuild. We can have separate distributions for now (until and unless Apple lets me use the Endpoint Security Framework for real, we can't be feature-complete, or at least feature-parity with Windows, anyway).

I'm not sure what you mean about "copying the output of xcodebuild". What I've done in the past is save one output to "x86_64/myproj.framework" and the other architecture to "arm64/myproj.framework". Save yet another version to just "myproj.framework". Take any "executables" (tools or dylibs) from the two architecture-specific versions, lipo together, and write into the appropriate place in the universal version. Then sign the universal version.

However, I pay very close attention to all of my projects and check them carefully. Some open source projects will generate different build configurations on different platforms. If your projects are doing this, then you have no alternative. You must do two separate, architecture-specific builds.

In theory, a project could also save the architecture-specific information in public header files. Luckily, I haven't had to deal with that or I was able to hack around it. But this is something that has always been in the back of my mind when dealing with these kinds of build problems and hack-arounds.

I meant "copying the signing command." :)