diff --git a/bindings/swift/OuisyncLib/.gitignore b/bindings/swift/OuisyncLib/.gitignore index 72917e5fd..878cfd1fb 100644 --- a/bindings/swift/OuisyncLib/.gitignore +++ b/bindings/swift/OuisyncLib/.gitignore @@ -1,4 +1,5 @@ -/output +/OuisyncLibFFI.xcframework +/config.sh .DS_Store /.build /Packages diff --git a/bindings/swift/OuisyncLib/Package.swift b/bindings/swift/OuisyncLib/Package.swift index 592ff50cf..49a169653 100644 --- a/bindings/swift/OuisyncLib/Package.swift +++ b/bindings/swift/OuisyncLib/Package.swift @@ -1,7 +1,6 @@ // swift-tools-version: 5.9 import PackageDescription - let package = Package( name: "OuisyncLib", platforms: [.macOS(.v13), .iOS(.v16)], @@ -25,7 +24,7 @@ let package = Package( path: "Tests"), // FIXME: move this to a separate package / framework .binaryTarget(name: "OuisyncLibFFI", - path: "output/OuisyncLibFFI.xcframework"), + path: "OuisyncLibFFI.xcframework"), .plugin(name: "FFIBuilder", capability: .buildTool(), path: "Plugins/Builder"), @@ -34,8 +33,7 @@ let package = Package( description: "Update rust dependencies"), permissions: [ .allowNetworkConnections(scope: .all(), - reason: "Downloads dependencies defined by Cargo.toml"), - .writeToPackageDirectory(reason: "These are not the droids you are looking for")]), + reason: "Downloads dependencies defined by Cargo.toml")]), path: "Plugins/Updater"), ] ) diff --git a/bindings/swift/OuisyncLib/Plugins/Updater/updater.swift b/bindings/swift/OuisyncLib/Plugins/Updater/updater.swift index a32fb6d60..af74e20e6 100644 --- a/bindings/swift/OuisyncLib/Plugins/Updater/updater.swift +++ b/bindings/swift/OuisyncLib/Plugins/Updater/updater.swift @@ -18,18 +18,11 @@ import PackagePlugin func performCommand(context: PackagePlugin.PluginContext, arguments: [String] = []) async throws { - let update = context.pluginWorkDirectory - - // FIXME: this path is very unstable; we might need to search the tree instead - let build = update - .removingLastComponent() - .appending(["\(context.package.id).output", "OuisyncLib", "FFIBuilder"]) - let task = Process() let exe = context.package.directory.appending(["Plugins", "update.sh"]).string task.standardInput = nil task.executableURL = URL(fileURLWithPath: exe) - task.arguments = [update.string, build.string] + task.arguments = [context.pluginWorkDirectory.string] do { try task.run() } catch { panic("Unable to start \(exe): \(error)") } task.waitUntilExit() diff --git a/bindings/swift/OuisyncLib/Plugins/build.sh b/bindings/swift/OuisyncLib/Plugins/build.sh index a08e07b3c..1e616d649 100755 --- a/bindings/swift/OuisyncLib/Plugins/build.sh +++ b/bindings/swift/OuisyncLib/Plugins/build.sh @@ -1,26 +1,19 @@ #!/usr/bin/env zsh # Command line tool which produces a `OuisyncLibFFI` framework for all configured llvm triples from -# OuisyncLib/config.sh -# -# This tool runs in a sandboxed process that can only write to a `output` folder and cannot access -# the network, so it relies on the `Updater` companion plugin to download the required dependencies -# before hand. Unfortunately, this does not work 100% of the time since both rust and especially -# cargo like to touch the lockfiles or the network for various reasons even when told not to. -# -# Called by the builder plugin which passes its own environment as well as the input, dependency -# and output paths. The builder checks that the dependency folder exists, but does not otherwise -# FIXME: validate that it contains all necessary dependencies as defined in Cargo.toml -# -# Hic sunt dracones! These might be of interest to anyone thinking they can do better than this mess: +# OuisyncLib/config.sh (currently generated in the ouisync-app repository) # +# This tool runs in a sandboxed process that cannot access the network, so it relies on the updater +# companion plugin to download the required dependencies ahead of time. Called by the builder plugin +# which passes both plugins' output paths as arguments. Hic sunt dracones! These may be of interest: # [1] https://forums.developer.apple.com/forums/thread/666335 # [2] https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md#build-tool-target-dependencies # [3] https://www.amyspark.me/blog/posts/2024/01/10/stripping-rust-libraries.html +fatal() { echo "Error $@" && exit $1 } PROJECT_HOME=$(realpath "$(dirname "$0")/../../../../") -PACKAGE_HOME=$(realpath "$PROJECT_HOME/bindings/swift/OuisyncLib") -export CARGO_HOME="$1" +export CARGO_HOME=$(realpath "$1") +export PATH="$CARGO_HOME/bin:$PATH" export RUSTUP_HOME="$CARGO_HOME/.rustup" -BUILD_OUTPUT="$2" +BUILD_OUTPUT=$(realpath "$2") # cargo builds some things that confuse xcode such as fingerprints and depfiles which cannot be # (easily) disabled; additionally, xcode does pick up the xcframework and reports it as a duplicate @@ -29,7 +22,7 @@ BUILD_OUTPUT="$2" mkdir -p "$BUILD_OUTPUT/dummy" # read config and prepare to build -source "$PACKAGE_HOME/config.sh" +source "$PROJECT_HOME/bindings/swift/OuisyncLib/config.sh" if [ $SKIP ] && [ $SKIP -gt 0 ]; then exit 0 fi @@ -49,12 +42,12 @@ for TARGET in $LIST[@]; do TARGETS[$TARGET]="" done # build configured targets cd $PROJECT_HOME for TARGET in ${(k)TARGETS}; do - "$CARGO_HOME/bin/cross" build \ + cross build \ --frozen \ --package ouisync-ffi \ --target $TARGET \ --target-dir "$BUILD_OUTPUT" \ - $FLAGS || exit 1 + $FLAGS || fatal 1 "Unable to compile for $TARGET" done # generate include files @@ -64,11 +57,7 @@ echo "module OuisyncLibFFI { header \"bindings.h\" export * }" > "$INCLUDE/module.modulemap" -"$CARGO_HOME/bin/cbindgen" --lang C --crate ouisync-ffi > "$INCLUDE/bindings.h" || exit 2 - -# delete previous framework (possibly a stub) and replace with new one that contains the archive -# TODO: some symlinks would be lovely here instead, cargo already create two copies -rm -Rf $BUILD_OUTPUT/output/OuisyncLibFFI.xcframework +cbindgen --lang C --crate ouisync-ffi > "$INCLUDE/bindings.h" || fatal 2 "Unable to generate bindings.h" # xcodebuild refuses multiple architectures per platform, instead expecting fat libraries when the # destination operating system supports multiple architectures; apple also explicitly rejects any @@ -96,11 +85,17 @@ for PLATFORM OUTPUTS in ${(kv)TREE}; do else # at least two architectures; run lipo on all matches and link the output instead LIBRARY="$BUILD_OUTPUT/$PLATFORM/libouisync_ffi.a" mkdir -p "$(dirname "$LIBRARY")" - lipo -create $MATCHED[@] -output $LIBRARY || exit 3 + lipo -create $MATCHED[@] -output $LIBRARY || fatal 3 "Unable to run lipo for ${MATCHED[@]}" fi PARAMS+=("-library" "$LIBRARY" "-headers" "$INCLUDE") done -echo ${PARAMS[@]} + +# TODO: skip xcodebuild and manually create symlinks instead (faster but Info.plist would be tricky) +rm -Rf "$BUILD_OUTPUT/temp.xcframework" +find "$BUILD_OUTPUT/OuisyncLibFFI.xcframework" -mindepth 1 -delete xcodebuild \ -create-xcframework ${PARAMS[@]} \ - -output "$BUILD_OUTPUT/output/OuisyncLibFFI.xcframework" || exit 4 + -output "$BUILD_OUTPUT/temp.xcframework" || fatal 4 "Unable to build xcframework" +for FILE in $(ls "$BUILD_OUTPUT/temp.xcframework"); do + mv "$BUILD_OUTPUT/temp.xcframework/$FILE" "$BUILD_OUTPUT/OuisyncLibFFI.xcframework/$FILE" +done diff --git a/bindings/swift/OuisyncLib/Plugins/update.sh b/bindings/swift/OuisyncLib/Plugins/update.sh index 570fe5859..445ea331c 100755 --- a/bindings/swift/OuisyncLib/Plugins/update.sh +++ b/bindings/swift/OuisyncLib/Plugins/update.sh @@ -1,39 +1,28 @@ #!/usr/bin/env zsh # Command line tool which pulls all dependencies needed to build the rust core library. -# -# Assumes that `cargo` and `rustup` are installed and available in REAL_PATH and it is run with the -# two plugin output paths (update and build) +fatal() { echo "Error $@" >&2 && exit $1 } PROJECT_HOME=$(realpath "$(dirname "$0")/../../../../") -PACKAGE_HOME=$(realpath "$PROJECT_HOME/bindings/swift/OuisyncLib") -export CARGO_HOME="$1" -export CARGO_HTTP_CHECK_REVOKE="false" # unclear why this fails, but it does -export RUSTUP_USE_CURL=1 # https://github.com/rust-lang/rustup/issues/1856 - -# download all possible toolchains: they only take up about 100MiB in total -mkdir -p .rustup +export CARGO_HOME=$(realpath "$1") +export PATH="$CARGO_HOME/bin:$PATH" export RUSTUP_HOME="$CARGO_HOME/.rustup" -rustup default stable -rustup target install aarch64-apple-darwin aarch64-apple-ios aarch64-apple-ios-sim \ - x86_64-apple-darwin x86_64-apple-ios - -cd "$PROJECT_HOME" -cargo fetch --locked || exit 1 # this is currently only fixable by moving the plugin location -cargo install cbindgen cross || exit 2 # build.sh also needs `cbindgen` and `cross` -# as part of the updater, we also perform the xcode symlink hack: we replace the existing -# $PACKAGE_HOME/output folder (either stub checked out by git or symlink to a previous build) with -# a link to the $BUILD_OUTPUT/output folder which will eventually contain an actual framework -BUILD_OUTPUT="$2" -mkdir -p "$BUILD_OUTPUT" -cd "$BUILD_OUTPUT" > /dev/null -# if this is the first time we build at this location, generate a new stub library to keep xcode -# happy in case the build process fails later down the line -if ! [ -d "output/OuisyncLibFFI.xcframework" ]; then - "$PACKAGE_HOME/reset-output.sh" +# install rust or update to latest version +export RUSTUP_USE_CURL=1 # https://github.com/rust-lang/rustup/issues/1856 +if [ -f "$CARGO_HOME/bin/rustup" ]; then + rustup update || fatal 1 "Unable to update rust" +else + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path \ + || fatal 1 "Unable to install rust" fi -# we can now replace the local stub (or prior link) with a link to the most recent build location -rm -rf "$PACKAGE_HOME/output" -ln -s "$BUILD_OUTPUT/output" "$PACKAGE_HOME/output" +# also install all possible toolchains since they only take up about 100MiB in total +export CARGO_HTTP_CHECK_REVOKE="false" # unclear it fails without this, but it does +rustup target install aarch64-apple-darwin aarch64-apple-ios \ + aarch64-apple-ios-sim x86_64-apple-darwin x86_64-apple-ios || fatal 2 "Unable to install rust via rustup" + +# build.sh needs `cbindgen` and `cross` to build as a multiplatform framework +cargo install cbindgen cross || fatal 3 "Unable to install header generator or cross compiler" -# unfortunately, we can't trigger a build from here because `build.sh` runs in a different sandbox +# fetch all up to date package dependencies for the next build (which must run offline) +cd "$PROJECT_HOME" +cargo fetch --locked || fatal 4 "Unable to fetch library dependencies" diff --git a/bindings/swift/OuisyncLib/config.sh b/bindings/swift/OuisyncLib/config.sh deleted file mode 100644 index 32dc0832a..000000000 --- a/bindings/swift/OuisyncLib/config.sh +++ /dev/null @@ -1,11 +0,0 @@ -# this file is sourced by `build.sh`; keep as many options enabled as you have patience for -SKIP=1 -#DEBUG=1 # set to 0 to generate release builds (much faster) -TARGETS=( - aarch64-apple-darwin # mac on apple silicon - x86_64-apple-darwin # mac on intel - aarch64-apple-ios # all real devices (ios 11+ are 64 bit only) - aarch64-apple-ios-sim # simulators when running on M chips - x86_64-apple-ios # simulator running on intel chips -) -# make sure to re-run "Update rust dependencies" after making changes here diff --git a/bindings/swift/OuisyncLib/output/OuisyncLibFFI.xcframework/Info.plist.sample b/bindings/swift/OuisyncLib/output/OuisyncLibFFI.xcframework/Info.plist.sample deleted file mode 100644 index 670c55dbb..000000000 --- a/bindings/swift/OuisyncLib/output/OuisyncLibFFI.xcframework/Info.plist.sample +++ /dev/null @@ -1,13 +0,0 @@ - - - - - AvailableLibraries - - - CFBundlePackageType - XFWK - XCFrameworkFormatVersion - 1.0 - - diff --git a/bindings/swift/OuisyncLib/reset-output.sh b/bindings/swift/OuisyncLib/reset-output.sh deleted file mode 100755 index 2d3489aed..000000000 --- a/bindings/swift/OuisyncLib/reset-output.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env sh -# Command line tool which produces a stub `OuisyncLibFFI` framework in the current directory -# -# Xcode expects a valid binary target to be available at package resolution time at the location -# specified in Package.swift, otherwise it refuses to register any plugins. To work around this -# limitation, we include a gitignored stub in the repository that is then then first replaced by the -# the updater plugin with a link to the same stub in the build folder (the only folder writable -# within the build plugin sandbox), then replaced with a real framework by the build plugin. -# -# While the process seems to work, we may run into edge cases where the framework gets corrupted, -# resulting in the inability to run the updater script that would fix it. If and when that happens, -# run this script to reset the framework to its original stub version from git. -# -echo d - ./output -rm -Rf ./output -# -# Generated by shar $(find output -print) -# -# This is a shell archive. Save it in a file, remove anything before -# this line, and then unpack it by entering "sh file". Note, it may -# create directories; files and directories will be owned by you and -# have default permissions. -# -# This archive contains: -# -# . -# ./output -# ./output/OuisyncLibFFI.xcframework -# ./output/OuisyncLibFFI.xcframework/macos-x86_64 -# ./output/OuisyncLibFFI.xcframework/macos-x86_64/Headers -# ./output/OuisyncLibFFI.xcframework/macos-x86_64/Headers/module.modulemap -# ./output/OuisyncLibFFI.xcframework/macos-x86_64/libouisync_ffi.a -# ./output/OuisyncLibFFI.xcframework/Info.plist -# -echo c - . -mkdir -p . > /dev/null 2>&1 -echo c - ./output -mkdir -p ./output > /dev/null 2>&1 -echo c - ./output/OuisyncLibFFI.xcframework -mkdir -p ./output/OuisyncLibFFI.xcframework > /dev/null 2>&1 -echo c - ./output/OuisyncLibFFI.xcframework/macos-x86_64 -mkdir -p ./output/OuisyncLibFFI.xcframework/macos-x86_64 > /dev/null 2>&1 -echo c - ./output/OuisyncLibFFI.xcframework/macos-x86_64/Headers -mkdir -p ./output/OuisyncLibFFI.xcframework/macos-x86_64/Headers > /dev/null 2>&1 -echo x - ./output/OuisyncLibFFI.xcframework/macos-x86_64/Headers/module.modulemap -sed 's/^X//' >./output/OuisyncLibFFI.xcframework/macos-x86_64/Headers/module.modulemap << '27ba995dcca9d28af9ee52fafa7cdc12' -Xmodule OuisyncLibFFI { -X export * -X} -27ba995dcca9d28af9ee52fafa7cdc12 -echo x - ./output/OuisyncLibFFI.xcframework/macos-x86_64/libouisync_ffi.a -sed 's/^X//' >./output/OuisyncLibFFI.xcframework/macos-x86_64/libouisync_ffi.a << '452e73dffcd1e38b0e076852cbd16868' -452e73dffcd1e38b0e076852cbd16868 -echo x - ./output/OuisyncLibFFI.xcframework/Info.plist -sed 's/^X//' >./output/OuisyncLibFFI.xcframework/Info.plist << '176f576dd1dba006b62db63408ad24c2' -X -X -X -X -X AvailableLibraries -X -X -X BinaryPath -X libouisync_ffi.a -X HeadersPath -X Headers -X LibraryIdentifier -X macos-x86_64 -X LibraryPath -X libouisync_ffi.a -X SupportedArchitectures -X -X x86_64 -X -X SupportedPlatform -X macos -X -X -X CFBundlePackageType -X XFWK -X XCFrameworkFormatVersion -X 1.0 -X -X -176f576dd1dba006b62db63408ad24c2 -exit