From c7932c2dbf4d03dd081e29bd4f74e1685447dd13 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 5 May 2026 16:40:16 +0800 Subject: [PATCH 01/13] Add Tuist XCFramework build support --- .github/actions/build-xcframework/action.yml | 47 +-- .github/workflows/build_xcframework.yml | 12 +- .github/workflows/release.yml | 28 +- .gitignore | 3 + .tool-versions | 1 + Package.swift | 55 ++- Scripts/build_xcframework.sh | 394 ++++++++---------- .../OpenSwiftUISymbolDualTestsSupport.h | 4 + Tuist.swift | 5 + 9 files changed, 263 insertions(+), 286 deletions(-) create mode 100644 .tool-versions create mode 100644 Sources/OpenSwiftUISymbolDualTestsSupport/OpenSwiftUISymbolDualTestsSupport.h create mode 100644 Tuist.swift diff --git a/.github/actions/build-xcframework/action.yml b/.github/actions/build-xcframework/action.yml index 340ea75f8..92a3b8276 100644 --- a/.github/actions/build-xcframework/action.yml +++ b/.github/actions/build-xcframework/action.yml @@ -1,5 +1,5 @@ -name: 'Build XCFrameworks' -description: 'Build OpenSwiftUI xcframeworks and generate binaryTarget entries' +name: 'Build XCFramework' +description: 'Build OpenSwiftUI.xcframework and generate its binaryTarget entry' inputs: xcode-version: @@ -26,24 +26,6 @@ outputs: checksum_OpenSwiftUI: description: 'Checksum for OpenSwiftUI.xcframework.zip' value: ${{ steps.checksums.outputs.checksum_OpenSwiftUI }} - checksum_OpenSwiftUICore: - description: 'Checksum for OpenSwiftUICore.xcframework.zip' - value: ${{ steps.checksums.outputs.checksum_OpenSwiftUICore }} - checksum_OpenAttributeGraphShims: - description: 'Checksum for OpenAttributeGraphShims.xcframework.zip' - value: ${{ steps.checksums.outputs.checksum_OpenAttributeGraphShims }} - checksum_OpenCoreGraphicsShims: - description: 'Checksum for OpenCoreGraphicsShims.xcframework.zip' - value: ${{ steps.checksums.outputs.checksum_OpenCoreGraphicsShims }} - checksum_OpenObservation: - description: 'Checksum for OpenObservation.xcframework.zip' - value: ${{ steps.checksums.outputs.checksum_OpenObservation }} - checksum_OpenQuartzCoreShims: - description: 'Checksum for OpenQuartzCoreShims.xcframework.zip' - value: ${{ steps.checksums.outputs.checksum_OpenQuartzCoreShims }} - checksum_OpenRenderBoxShims: - description: 'Checksum for OpenRenderBoxShims.xcframework.zip' - value: ${{ steps.checksums.outputs.checksum_OpenRenderBoxShims }} runs: using: 'composite' @@ -52,26 +34,21 @@ runs: uses: OpenSwiftUIProject/setup-xcode@v2 with: xcode-version: ${{ inputs.xcode-version }} + - name: Install Tuist + uses: jdx/mise-action@v2 - name: Set up build environment run: Scripts/CI/darwin_setup_build.sh shell: bash - - name: Build XCFrameworks + - name: Build XCFramework run: Scripts/build_xcframework.sh OpenSwiftUI shell: bash - - name: Code sign XCFrameworks + - name: Code sign XCFramework if: ${{ inputs.signing-certificate-base64 != '' }} uses: ./.github/actions/codesign-xcframework with: signing-certificate-base64: ${{ inputs.signing-certificate-base64 }} signing-certificate-password: ${{ inputs.signing-certificate-password }} - xcframework-paths: >- - build/OpenSwiftUI.xcframework - build/OpenSwiftUICore.xcframework - build/OpenAttributeGraphShims.xcframework - build/OpenCoreGraphicsShims.xcframework - build/OpenObservation.xcframework - build/OpenQuartzCoreShims.xcframework - build/OpenRenderBoxShims.xcframework + xcframework-paths: build/OpenSwiftUI.xcframework - name: Compute Checksums and Generate Summary id: checksums shell: bash @@ -79,15 +56,7 @@ runs: TAG_NAME: ${{ inputs.tag-name }} run: | cd build - FRAMEWORKS=( - OpenSwiftUI - OpenSwiftUICore - OpenAttributeGraphShims - OpenCoreGraphicsShims - OpenObservation - OpenQuartzCoreShims - OpenRenderBoxShims - ) + FRAMEWORKS=(OpenSwiftUI) BODY="" echo "### XCFramework Binary Targets" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/build_xcframework.yml b/.github/workflows/build_xcframework.yml index 8b1ba45f3..b6a2afc72 100644 --- a/.github/workflows/build_xcframework.yml +++ b/.github/workflows/build_xcframework.yml @@ -5,7 +5,7 @@ on: jobs: build_xcframework: - name: Build XCFrameworks + name: Build XCFramework runs-on: macos-15 env: OPENSWIFTUI_OPENATTRIBUTESHIMS_ATTRIBUTEGRAPH: 1 @@ -13,15 +13,15 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v4 - - name: Build XCFrameworks + - name: Build XCFramework uses: ./.github/actions/build-xcframework with: signing-certificate-base64: ${{ secrets.SIGNING_CERTIFICATE_BASE_64 }} signing-certificate-password: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }} - - name: Zip XCFrameworks - run: cd build && zip -ry xcframeworks.zip *.xcframework - - name: Upload XCFrameworks + - name: Zip XCFramework + run: cd build && zip -ry OpenSwiftUI.xcframework.zip OpenSwiftUI.xcframework + - name: Upload XCFramework uses: actions/upload-artifact@v7 with: - path: build/xcframeworks.zip + path: build/OpenSwiftUI.xcframework.zip archive: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ea85965e8..f17a57761 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,25 +15,19 @@ jobs: tag-name: ${{ github.ref_name }} secrets: inherit - build-xcframeworks: - name: Build and Upload XCFrameworks + build-xcframework: + name: Build and Upload XCFramework needs: release-notes runs-on: macos-15 outputs: checksum_OpenSwiftUI: ${{ steps.build.outputs.checksum_OpenSwiftUI }} - checksum_OpenSwiftUICore: ${{ steps.build.outputs.checksum_OpenSwiftUICore }} - checksum_OpenAttributeGraphShims: ${{ steps.build.outputs.checksum_OpenAttributeGraphShims }} - checksum_OpenCoreGraphicsShims: ${{ steps.build.outputs.checksum_OpenCoreGraphicsShims }} - checksum_OpenObservation: ${{ steps.build.outputs.checksum_OpenObservation }} - checksum_OpenQuartzCoreShims: ${{ steps.build.outputs.checksum_OpenQuartzCoreShims }} - checksum_OpenRenderBoxShims: ${{ steps.build.outputs.checksum_OpenRenderBoxShims }} env: OPENSWIFTUI_OPENATTRIBUTESHIMS_ATTRIBUTEGRAPH: 1 OPENSWIFTUI_USE_LOCAL_DEPS: 1 GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v4 - - name: Build XCFrameworks + - name: Build XCFramework id: build uses: ./.github/actions/build-xcframework with: @@ -45,7 +39,7 @@ jobs: update-binary-repo: name: Update OpenSwiftUI-spm - needs: build-xcframeworks + needs: build-xcframework runs-on: ubuntu-latest env: BINARY_REPO_PAT: ${{ secrets.BINARY_REPO_PAT }} @@ -53,13 +47,7 @@ jobs: - name: Update binary repo env: VERSION: ${{ github.ref_name }} - CHECKSUM_OpenSwiftUI: ${{ needs.build-xcframeworks.outputs.checksum_OpenSwiftUI }} - CHECKSUM_OpenSwiftUICore: ${{ needs.build-xcframeworks.outputs.checksum_OpenSwiftUICore }} - CHECKSUM_OpenAttributeGraphShims: ${{ needs.build-xcframeworks.outputs.checksum_OpenAttributeGraphShims }} - CHECKSUM_OpenCoreGraphicsShims: ${{ needs.build-xcframeworks.outputs.checksum_OpenCoreGraphicsShims }} - CHECKSUM_OpenObservation: ${{ needs.build-xcframeworks.outputs.checksum_OpenObservation }} - CHECKSUM_OpenQuartzCoreShims: ${{ needs.build-xcframeworks.outputs.checksum_OpenQuartzCoreShims }} - CHECKSUM_OpenRenderBoxShims: ${{ needs.build-xcframeworks.outputs.checksum_OpenRenderBoxShims }} + CHECKSUM_OpenSwiftUI: ${{ needs.build-xcframework.outputs.checksum_OpenSwiftUI }} run: | if [ -z "$BINARY_REPO_PAT" ]; then echo "::notice::BINARY_REPO_PAT not set, skipping binary repo update" @@ -72,12 +60,6 @@ jobs: sed \ -e "s|{{VERSION}}|${VERSION}|g" \ -e "s|{{CHECKSUM_OpenSwiftUI}}|${CHECKSUM_OpenSwiftUI}|g" \ - -e "s|{{CHECKSUM_OpenSwiftUICore}}|${CHECKSUM_OpenSwiftUICore}|g" \ - -e "s|{{CHECKSUM_OpenAttributeGraphShims}}|${CHECKSUM_OpenAttributeGraphShims}|g" \ - -e "s|{{CHECKSUM_OpenCoreGraphicsShims}}|${CHECKSUM_OpenCoreGraphicsShims}|g" \ - -e "s|{{CHECKSUM_OpenObservation}}|${CHECKSUM_OpenObservation}|g" \ - -e "s|{{CHECKSUM_OpenQuartzCoreShims}}|${CHECKSUM_OpenQuartzCoreShims}|g" \ - -e "s|{{CHECKSUM_OpenRenderBoxShims}}|${CHECKSUM_OpenRenderBoxShims}|g" \ Package.swift.template > Package.swift echo "Generated Package.swift:" head -50 Package.swift diff --git a/.gitignore b/.gitignore index 9df757056..c1f37c67f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .DS_Store /.build /build +/Derived +/OpenSwiftUI.xcodeproj +/Workspace.xcworkspace /Packages xcuserdata/ DerivedData/ diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..a2b8ed8ef --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +tuist 4.174.2 diff --git a/Package.swift b/Package.swift index ea04063a0..df70357db 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,11 @@ public struct PackageContextEnvironmentProvider: EnvironmentProvider { public init() {} public func value(forKey key: String) -> String? { + #if TUIST + ProcessInfo.processInfo.environment[key] + #else Context.environment[key] + #endif } } @@ -120,6 +124,12 @@ EnvManager.shared.register(domain: "OpenSwiftUI") // MARK: - Env and config +#if TUIST +let isTuistEvaluation = true +#else +let isTuistEvaluation = false +#endif + #if os(macOS) // NOTE: #if os(macOS) check is not accurate if we are cross compiling for Linux platform. So we add an env key to specify it. let buildForDarwinPlatform = envBoolValue("BUILD_FOR_DARWIN_PLATFORM", default: true) @@ -138,19 +148,25 @@ let isXcodeEnv = envStringValue("__CFBundleIdentifier", searchInDomain: false) = let development = envBoolValue("DEVELOPMENT", default: false) let warningsAsErrorsCondition = envBoolValue("WERROR", default: isXcodeEnv && development) -let swiftCorelibsPath = envStringValue("LIB_SWIFT_PATH") ?? "\(Context.packageDirectory)/Sources/SwiftCorelibs/include" +#if TUIST +let packageDirectory = FileManager.default.currentDirectoryPath +#else +let packageDirectory = Context.packageDirectory +#endif + +let swiftCorelibsPath = envStringValue("LIB_SWIFT_PATH") ?? "\(packageDirectory)/Sources/SwiftCorelibs/include" let releaseVersion = envIntValue("TARGET_RELEASE", default: 2024) let libraryEvolutionCondition = envBoolValue("LIBRARY_EVOLUTION", default: buildForDarwinPlatform) let compatibilityTestCondition = envBoolValue("COMPATIBILITY_TEST") -let useLocalDeps = envBoolValue("USE_LOCAL_DEPS") +let useLocalDeps = envBoolValue("USE_LOCAL_DEPS", default: isTuistEvaluation) // For OpenAttributeGraphShims let computeCondition = envBoolValue("OPENATTRIBUTESHIMS_COMPUTE", default: false) let danceUIGraphCondition = envBoolValue("OPENATTRIBUTESHIMS_DANCEUIGRAPH", default: false) -let attributeGraphCondition = envBoolValue("OPENATTRIBUTESHIMS_ATTRIBUTEGRAPH", default: false) +let attributeGraphCondition = envBoolValue("OPENATTRIBUTESHIMS_ATTRIBUTEGRAPH", default: isTuistEvaluation) let renderBoxCondition = envBoolValue("RENDERBOX", default: buildForDarwinPlatform && !isSPIBuild) @@ -183,7 +199,7 @@ let enableRuntimeConcurrencyCheck = envBoolValue("ENABLE_RUNTIME_CONCURRENCY_CHE let bridgeFramework = envStringValue("OPENSWIFTUI_BRIDGE_FRAMEWORK", default: "SwiftUI") // Workaround iOS CI build issue (We need to disable this on iOS CI) -let supportMultiProducts: Bool = envBoolValue("SUPPORT_MULTI_PRODUCTS", default: true) +let supportMultiProducts: Bool = envBoolValue("SUPPORT_MULTI_PRODUCTS", default: !isTuistEvaluation) /// CGFloat and CGRect def in CFCGTypes.h will conflict with Foundation's CGSize/CGRect def on Linux. /// macOS: true -> no issue @@ -731,8 +747,9 @@ let openSwiftUISymbolDualTestsTarget = Target.testTarget( // MARK: - Products -let libraryType: Product.Library.LibraryType? -switch envStringValue("LIBRARY_TYPE") { +let configuredLibraryType = envStringValue("LIBRARY_TYPE") ?? (isTuistEvaluation ? "dynamic" : nil) +let libraryType: PackageDescription.Product.Library.LibraryType? +switch configuredLibraryType { case "dynamic": libraryType = .dynamic case "static": @@ -741,7 +758,7 @@ default: libraryType = nil } -var products: [Product] = [ +var products: [PackageDescription.Product] = [ .library(name: "OpenSwiftUI", type: libraryType, targets: ["OpenSwiftUI"]) ] if supportMultiProducts { @@ -908,3 +925,27 @@ if swiftCryptoCondition { openSwiftUICoreTarget.addSwiftCryptoSettings() openSwiftUITarget.addSwiftCryptoSettings() } + +#if TUIST +import struct ProjectDescription.PackageSettings +import enum ProjectDescription.Product + +let packageSettings = PackageSettings( + productTypes: [ + "OpenSwiftUI": ProjectDescription.Product.framework, + "OpenSwiftUICore": ProjectDescription.Product.staticFramework, + "OpenSwiftUI_SPI": ProjectDescription.Product.staticFramework, + "COpenSwiftUI": ProjectDescription.Product.staticFramework, + "OpenSwiftUIMacros": ProjectDescription.Product.macro, + "OpenSwiftUITestsSupport": ProjectDescription.Product.staticFramework, + "OpenSwiftUISymbolDualTestsSupport": ProjectDescription.Product.staticFramework, + "OpenAttributeGraphShims": ProjectDescription.Product.staticFramework, + "OpenCoreGraphicsShims": ProjectDescription.Product.staticFramework, + "OpenObservation": ProjectDescription.Product.staticFramework, + "OpenQuartzCoreShims": ProjectDescription.Product.staticFramework, + "OpenRenderBoxShims": ProjectDescription.Product.staticFramework, + "SymbolLocator": ProjectDescription.Product.staticFramework, + ], + baseProductType: ProjectDescription.Product.staticFramework +) +#endif diff --git a/Scripts/build_xcframework.sh b/Scripts/build_xcframework.sh index dc7489c7c..8f3f7e1b5 100755 --- a/Scripts/build_xcframework.sh +++ b/Scripts/build_xcframework.sh @@ -1,32 +1,25 @@ #!/bin/bash -# Script modified from https://docs.emergetools.com/docs/analyzing-a-spm-framework-ios - set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -# xcodebuild uses the current directory to find the SPM workspace cd "$PROJECT_ROOT" PROJECT_BUILD_DIR="${PROJECT_BUILD_DIR:-"${PROJECT_ROOT}/build"}" +DERIVED_DATA_PATH="$PROJECT_BUILD_DIR/DerivedData" +XCODEPROJ="$PROJECT_ROOT/OpenSwiftUI.xcodeproj" +XCODEWORKSPACE="$PROJECT_ROOT/Workspace.xcworkspace" -# Use xcodebuild's default DerivedData to avoid scheme resolution issues with SPM workspaces. -# Detect the workspace name from the directory and find the matching DerivedData. -WORKSPACE_NAME="$(basename "$PROJECT_ROOT")" -XCODEBUILD_DERIVED_DATA_PATH=$(find ~/Library/Developer/Xcode/DerivedData -maxdepth 1 -name "${WORKSPACE_NAME}-*" -type d 2>/dev/null | head -1) -if [ -z "$XCODEBUILD_DERIVED_DATA_PATH" ]; then - echo "Warning: Could not find DerivedData for workspace '$WORKSPACE_NAME'. Will detect after first build." -fi - -# Parse arguments +# Parse arguments. # --sdk and --archs are paired: --sdk --archs # If --archs is omitted for an SDK, all default architectures are built. SDKS=() -SDK_ARCHS=() # parallel array: archs for each SDK ("" = default) +SDK_ARCHS=() DEBUG_MODE=false -PACKAGE_NAME="" +PACKAGE_NAME="OpenSwiftUI" +RUN_TUIST_INSTALL=true while [[ $# -gt 0 ]]; do case "$1" in @@ -36,9 +29,8 @@ while [[ $# -gt 0 ]]; do shift 2 ;; --archs) - # Apply to the last --sdk if [ ${#SDKS[@]} -gt 0 ]; then - SDK_ARCHS[$((${#SDK_ARCHS[@]}-1))]="$2" + SDK_ARCHS[$((${#SDK_ARCHS[@]} - 1))]="$2" fi shift 2 ;; @@ -46,33 +38,52 @@ while [[ $# -gt 0 ]]; do DEBUG_MODE=true shift ;; + --skip-tuist-install) + RUN_TUIST_INSTALL=false + shift + ;; *) - if [ -z "$PACKAGE_NAME" ]; then - PACKAGE_NAME="$1" - fi + PACKAGE_NAME="$1" shift ;; esac done -# Default: macosx and iphonesimulator -# Note: iphoneos SDK support is blocked by an AG issue. See #835 +# Default: macosx and iphonesimulator. +# Note: iphoneos SDK support is blocked by an AG issue. See #835. if [ ${#SDKS[@]} -eq 0 ]; then SDKS=("macosx" "iphonesimulator") SDK_ARCHS=("" "") fi -if [ -z "$PACKAGE_NAME" ]; then - echo "No package name provided. Using the first scheme found in the Package.swift." - PACKAGE_NAME=$(xcodebuild -list -project "$PROJECT_ROOT" | awk 'schemes && NF>0 { print $1; exit } /Schemes:$/ { schemes = 1 }') - echo "Using: $PACKAGE_NAME" +if [ "${OPENSWIFTUI_SKIP_TUIST_INSTALL:-0}" = "1" ]; then + RUN_TUIST_INSTALL=false fi -# Helper: get archs for a given SDK by index -get_sdk_archs() { - local idx="$1" - echo "${SDK_ARCHS[$idx]}" -} +if ! command -v tuist >/dev/null 2>&1; then + echo "Error: tuist is required to generate $PACKAGE_NAME.xcodeproj." + exit 1 +fi + +if [ "$RUN_TUIST_INSTALL" = true ]; then + echo "Installing Tuist dependencies..." + tuist install +else + echo "Skipping tuist install." +fi + +echo "Generating Xcode project with Tuist..." +tuist generate --no-open + +if [ ! -d "$XCODEPROJ" ]; then + echo "Error: Expected Tuist to generate $XCODEPROJ." + exit 1 +fi + +if [ ! -d "$XCODEWORKSPACE" ]; then + echo "Error: Expected Tuist to generate $XCODEWORKSPACE." + exit 1 +fi echo "SDKs: ${SDKS[*]}" for i in "${!SDKS[@]}"; do @@ -84,8 +95,16 @@ for i in "${!SDKS[@]}"; do done echo "Debug mode: $DEBUG_MODE" -# Dependency modules that need stub xcframeworks (referenced in public swiftinterface) -DEP_MODULES=("OpenSwiftUICore" "OpenAttributeGraphShims" "OpenCoreGraphicsShims" "OpenObservation" "OpenQuartzCoreShims" "OpenRenderBoxShims") +# Dependency modules referenced by public swiftinterfaces. They are copied into +# the one distributable framework instead of emitted as separate stub frameworks. +DEP_MODULES=( + "OpenSwiftUICore" + "OpenAttributeGraphShims" + "OpenCoreGraphicsShims" + "OpenObservation" + "OpenQuartzCoreShims" + "OpenRenderBoxShims" +) sdk_destination() { case "$1" in @@ -96,222 +115,175 @@ sdk_destination() { esac } -build_framework() { - local sdk="$1" - local destination="$2" - local scheme="$3" - local archs="$4" # comma-separated or empty for default +build_products_suffix() { + case "$1" in + macosx) echo "Release" ;; + *) echo "Release-$1" ;; + esac +} - local XCODEBUILD_ARCHIVE_PATH="$PROJECT_BUILD_DIR/$scheme-$sdk.xcarchive" +framework_path() { + local archive_path="$1" + local scheme="$2" + echo "$archive_path/Products/Library/Frameworks/$scheme.framework" +} - rm -rf "$XCODEBUILD_ARCHIVE_PATH" +framework_modules_path() { + local framework="$1" + if [ -d "$framework/Versions" ]; then + echo "$framework/Versions/Current/Modules" + else + echo "$framework/Modules" + fi +} - local archs_arg="" - if [ -n "$archs" ]; then - # Replace commas with spaces for ARCHS setting - archs_arg="ARCHS=${archs//,/ }" +strip_private_interfaces() { + local modules_path="$1" + if [ "$DEBUG_MODE" = false ]; then + find "$modules_path" -name "*.package.swiftinterface" -delete + find "$modules_path" -name "*.private.swiftinterface" -delete fi +} - OPENSWIFTUI_LIBRARY_TYPE=dynamic \ - OPENSWIFTUI_OPENATTRIBUTESHIMS_ATTRIBUTEGRAPH=1 \ - OPENSWIFTUI_LIBRARY_EVOLUTION=1 \ - xcodebuild archive \ - -scheme "$scheme" \ - -archivePath "$XCODEBUILD_ARCHIVE_PATH" \ - -sdk "$sdk" \ - -destination "$destination" \ - INSTALL_PATH='Library/Frameworks' \ - SWIFT_EMIT_MODULE_INTERFACE=YES \ - SWIFT_ACTIVE_COMPILATION_CONDITIONS='$(inherited) OPENSWIFTUI_XCFRAMEWORK_BUILD' \ - $archs_arg - - # Detect DerivedData path after the first archive if not yet known - if [ -z "$XCODEBUILD_DERIVED_DATA_PATH" ]; then - XCODEBUILD_DERIVED_DATA_PATH=$(find ~/Library/Developer/Xcode/DerivedData -maxdepth 1 -name "${WORKSPACE_NAME}-*" -type d 2>/dev/null | head -1) - if [ -z "$XCODEBUILD_DERIVED_DATA_PATH" ]; then - echo "Error: Could not find DerivedData for workspace '$WORKSPACE_NAME'." - exit 1 +canonical_path() { + local path="$1" + if [ -e "$path" ]; then + (cd "$(dirname "$path")" && printf "%s/%s\n" "$(pwd -P)" "$(basename "$path")") + else + echo "$path" + fi +} + +find_swiftmodule() { + local build_products_path="$1" + local archive_path="$2" + local module_name="$3" + + local candidates=( + "$build_products_path/$module_name.swiftmodule" + "$build_products_path/$module_name.framework/Modules/$module_name.swiftmodule" + "$build_products_path/$module_name.framework/Versions/A/Modules/$module_name.swiftmodule" + "$archive_path/Products/Library/Frameworks/$module_name.framework/Modules/$module_name.swiftmodule" + "$archive_path/Products/Library/Frameworks/$module_name.framework/Versions/A/Modules/$module_name.swiftmodule" + ) + + for candidate in "${candidates[@]}"; do + if [ -d "$candidate" ]; then + echo "$candidate" + return fi + done +} + +copy_swiftmodule() { + local build_products_path="$1" + local archive_path="$2" + local module_name="$3" + local modules_path="$4" + + local swiftmodule + swiftmodule="$(find_swiftmodule "$build_products_path" "$archive_path" "$module_name")" + if [ -z "$swiftmodule" ]; then + echo "Warning: No swiftmodule found for $module_name." + return fi - # Determine the build products path suffix - local build_products_suffix="Release-$sdk" - if [ "$sdk" = "macosx" ]; then - build_products_suffix="Release" + local destination="$modules_path/$module_name.swiftmodule" + if [ "$(canonical_path "$swiftmodule")" = "$(canonical_path "$destination")" ]; then + return fi - local BUILD_PRODUCTS_PATH="$XCODEBUILD_DERIVED_DATA_PATH/Build/Intermediates.noindex/ArchiveIntermediates/$scheme/BuildProductsPath/$build_products_suffix" - # Copy main scheme swiftmodule into the framework - if [ "$sdk" = "macosx" ]; then - FRAMEWORK_MODULES_PATH="$XCODEBUILD_ARCHIVE_PATH/Products/Library/Frameworks/$scheme.framework/Versions/Current/Modules" - mkdir -p "$FRAMEWORK_MODULES_PATH" - cp -r "$BUILD_PRODUCTS_PATH/$scheme.swiftmodule" "$FRAMEWORK_MODULES_PATH/$scheme.swiftmodule" - rm -rf "$XCODEBUILD_ARCHIVE_PATH/Products/Library/Frameworks/$scheme.framework/Modules" - ln -s Versions/Current/Modules "$XCODEBUILD_ARCHIVE_PATH/Products/Library/Frameworks/$scheme.framework/Modules" - else - FRAMEWORK_MODULES_PATH="$XCODEBUILD_ARCHIVE_PATH/Products/Library/Frameworks/$scheme.framework/Modules" - mkdir -p "$FRAMEWORK_MODULES_PATH" - cp -r "$BUILD_PRODUCTS_PATH/$scheme.swiftmodule" "$FRAMEWORK_MODULES_PATH/$scheme.swiftmodule" + rm -rf "$destination" + cp -R "$swiftmodule" "$destination" +} + +build_framework() { + local sdk="$1" + local destination="$2" + local scheme="$3" + local archs="$4" + + local archive_path="$PROJECT_BUILD_DIR/$scheme-$sdk.xcarchive" + local build_products_path="$DERIVED_DATA_PATH/Build/Intermediates.noindex/ArchiveIntermediates/$scheme/BuildProductsPath/$(build_products_suffix "$sdk")" + + rm -rf "$archive_path" + + local xcodebuild_args=( + archive + -workspace "$XCODEWORKSPACE" + -scheme "$scheme" + -configuration Release + -archivePath "$archive_path" + -sdk "$sdk" + -destination "$destination" + -derivedDataPath "$DERIVED_DATA_PATH" + -skipPackagePluginValidation + -skipMacroValidation + INSTALL_PATH=Library/Frameworks + SKIP_INSTALL=NO + BUILD_LIBRARY_FOR_DISTRIBUTION=YES + SWIFT_EMIT_MODULE_INTERFACE=YES + "SWIFT_ACTIVE_COMPILATION_CONDITIONS=\$(inherited) OPENSWIFTUI_XCFRAMEWORK_BUILD" + ENABLE_USER_SCRIPT_SANDBOXING=NO + ) + + if [ -n "$archs" ]; then + xcodebuild_args+=("ARCHS=${archs//,/ }") fi - # Delete private and package swiftinterface (unless --debug) - if [ "$DEBUG_MODE" = false ]; then - rm -f "$FRAMEWORK_MODULES_PATH/$scheme.swiftmodule"/*.package.swiftinterface - rm -f "$FRAMEWORK_MODULES_PATH/$scheme.swiftmodule"/*.private.swiftinterface + xcodebuild "${xcodebuild_args[@]}" + + local framework + framework="$(framework_path "$archive_path" "$scheme")" + if [ ! -d "$framework" ]; then + echo "Error: Archive did not contain $framework." + exit 1 + fi + + local modules_path + modules_path="$(framework_modules_path "$framework")" + mkdir -p "$modules_path" + + if [ "$sdk" = "macosx" ]; then + rm -rf "$framework/Modules" + ln -s Versions/Current/Modules "$framework/Modules" fi - # Capture dependency swiftmodules before next build overwrites DerivedData + copy_swiftmodule "$build_products_path" "$archive_path" "$scheme" "$modules_path" for dep in "${DEP_MODULES[@]}"; do - local dep_swiftmodule="$BUILD_PRODUCTS_PATH/$dep.swiftmodule" - if [ -d "$dep_swiftmodule" ]; then - local dep_cache="$PROJECT_BUILD_DIR/dep-modules/$dep/$sdk" - mkdir -p "$dep_cache" - cp -r "$dep_swiftmodule" "$dep_cache/$dep.swiftmodule" - # Strip private/package interfaces from stubs (unless --debug) - if [ "$DEBUG_MODE" = false ]; then - rm -f "$dep_cache/$dep.swiftmodule"/*.package.swiftinterface - rm -f "$dep_cache/$dep.swiftmodule"/*.private.swiftinterface - fi - fi + copy_swiftmodule "$build_products_path" "$archive_path" "$dep" "$modules_path" done + strip_private_interfaces "$modules_path" } +rm -rf "$DERIVED_DATA_PATH" +mkdir -p "$PROJECT_BUILD_DIR" + for i in "${!SDKS[@]}"; do build_framework "${SDKS[$i]}" "$(sdk_destination "${SDKS[$i]}")" "$PACKAGE_NAME" "${SDK_ARCHS[$i]}" done -echo "Builds completed successfully." +echo "Archives completed successfully." -# Create main xcframework rm -rf "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework" create_args=() for sdk in "${SDKS[@]}"; do - create_args+=(-framework "$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive/Products/Library/Frameworks/$PACKAGE_NAME.framework") + create_args+=(-framework "$(framework_path "$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive" "$PACKAGE_NAME")") done xcodebuild -create-xcframework "${create_args[@]}" -output "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework" -# Copy dSYMs +# Copy dSYMs into each XCFramework slice for consumers that want local symbols. for sdk in "${SDKS[@]}"; do - # Determine the xcframework slice directory name local_dsym_dir="" case "$sdk" in iphonesimulator) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework"/ios-*simulator 2>/dev/null | head -1) ;; iphoneos) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework"/ios-arm64 2>/dev/null | head -1) ;; macosx) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework"/macos-* 2>/dev/null | head -1) ;; esac + if [ -n "$local_dsym_dir" ] && [ -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive/dSYMs" ]; then - cp -r "$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive/dSYMs" "$local_dsym_dir/" + cp -R "$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive/dSYMs" "$local_dsym_dir/" fi done -# Create stub xcframeworks for dependency modules -# These contain only swiftmodule/swiftinterface (no binary) since the code -# is statically linked into the main framework. -for dep in "${DEP_MODULES[@]}"; do - echo "Creating stub xcframework for $dep..." - rm -rf "$PROJECT_BUILD_DIR/$dep.xcframework" - - # Build per-platform stub frameworks - for sdk in "${SDKS[@]}"; do - local_dep_cache="$PROJECT_BUILD_DIR/dep-modules/$dep/$sdk" - if [ ! -d "$local_dep_cache/$dep.swiftmodule" ]; then - echo "Warning: No swiftmodule found for $dep ($sdk), skipping." - continue - fi - - stub_fw="$PROJECT_BUILD_DIR/dep-stubs/$sdk/$dep.framework" - sdk_path="$(xcrun --sdk $sdk --show-sdk-path)" - - # Detect architectures from the main framework binary - main_binary="$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive/Products/Library/Frameworks/$PACKAGE_NAME.framework/$PACKAGE_NAME" - if [ "$sdk" = "macosx" ]; then - main_binary="$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive/Products/Library/Frameworks/$PACKAGE_NAME.framework/Versions/A/$PACKAGE_NAME" - fi - stub_archs=$(lipo -archs "$main_binary" 2>/dev/null || echo "arm64") - - # Determine install_name and clang target suffix per SDK - install_name_path="" - if [ "$sdk" = "macosx" ]; then - mkdir -p "$stub_fw/Versions/A/Modules" - cp -r "$local_dep_cache/$dep.swiftmodule" "$stub_fw/Versions/A/Modules/$dep.swiftmodule" - ln -sfn A "$stub_fw/Versions/Current" - ln -sfn Versions/Current/Modules "$stub_fw/Modules" - install_name_path="@rpath/$dep.framework/Versions/A/$dep" - else - mkdir -p "$stub_fw/Modules" - cp -r "$local_dep_cache/$dep.swiftmodule" "$stub_fw/Modules/$dep.swiftmodule" - install_name_path="@rpath/$dep.framework/$dep" - fi - - # Build stub dylib for each arch then lipo if needed - dylib_files=() - for arch in $stub_archs; do - target_triple="" - case "$sdk" in - iphoneos) target_triple="${arch}-apple-ios18.0" ;; - iphonesimulator) target_triple="${arch}-apple-ios18.0-simulator" ;; - macosx) target_triple="${arch}-apple-macos15.0" ;; - esac - clang -dynamiclib -x c /dev/null -o "/tmp/$dep-$arch.dylib" \ - -install_name "$install_name_path" \ - -isysroot "$sdk_path" -target "$target_triple" 2>/dev/null - dylib_files+=("/tmp/$dep-$arch.dylib") - done - - output_dylib="" - if [ "$sdk" = "macosx" ]; then - output_dylib="$stub_fw/Versions/A/$dep" - else - output_dylib="$stub_fw/$dep" - fi - - if [ ${#dylib_files[@]} -eq 1 ]; then - mv "${dylib_files[0]}" "$output_dylib" - else - lipo -create "${dylib_files[@]}" -output "$output_dylib" - rm -f "${dylib_files[@]}" - fi - - if [ "$sdk" = "macosx" ]; then - ln -sfn "Versions/A/$dep" "$stub_fw/$dep" - fi - - # Create Info.plist with CFBundleExecutable - # macOS frameworks need Info.plist inside Versions/A/Resources/ - plist_dir="$stub_fw" - if [ "$sdk" = "macosx" ]; then - mkdir -p "$stub_fw/Versions/A/Resources" - plist_dir="$stub_fw/Versions/A/Resources" - ln -sfn Versions/Current/Resources "$stub_fw/Resources" - fi - cat > "$plist_dir/Info.plist" << PLIST - - - - - CFBundleExecutable - $dep - CFBundleIdentifier - org.openswiftui.$dep - CFBundleName - $dep - CFBundlePackageType - FMWK - - -PLIST - done - - # Create xcframework from stubs - create_args=() - for sdk in "${SDKS[@]}"; do - stub_fw="$PROJECT_BUILD_DIR/dep-stubs/$sdk/$dep.framework" - if [ -d "$stub_fw" ]; then - create_args+=(-framework "$stub_fw") - fi - done - xcodebuild -create-xcframework "${create_args[@]}" -output "$PROJECT_BUILD_DIR/$dep.xcframework" -done - -# Clean up temp directories -rm -rf "$PROJECT_BUILD_DIR/dep-modules" "$PROJECT_BUILD_DIR/dep-stubs" +echo "Created $PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework" diff --git a/Sources/OpenSwiftUISymbolDualTestsSupport/OpenSwiftUISymbolDualTestsSupport.h b/Sources/OpenSwiftUISymbolDualTestsSupport/OpenSwiftUISymbolDualTestsSupport.h new file mode 100644 index 000000000..898a44886 --- /dev/null +++ b/Sources/OpenSwiftUISymbolDualTestsSupport/OpenSwiftUISymbolDualTestsSupport.h @@ -0,0 +1,4 @@ +#ifndef OPENSWIFTUISYMBOLDUALTESTSSUPPORT_H +#define OPENSWIFTUISYMBOLDUALTESTSSUPPORT_H + +#endif /* OPENSWIFTUISYMBOLDUALTESTSSUPPORT_H */ diff --git a/Tuist.swift b/Tuist.swift new file mode 100644 index 000000000..abf9ffd0e --- /dev/null +++ b/Tuist.swift @@ -0,0 +1,5 @@ +import ProjectDescription + +let tuist = Tuist( + project: .tuist() +) From eacf1dbcb5d65ec2480e82c5dc4a0353c05f9650 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 5 May 2026 18:01:12 +0800 Subject: [PATCH 02/13] Trim release XCFramework debug metadata --- Scripts/build_xcframework.sh | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/Scripts/build_xcframework.sh b/Scripts/build_xcframework.sh index 8f3f7e1b5..368960948 100755 --- a/Scripts/build_xcframework.sh +++ b/Scripts/build_xcframework.sh @@ -137,9 +137,10 @@ framework_modules_path() { fi } -strip_private_interfaces() { +strip_release_metadata() { local modules_path="$1" if [ "$DEBUG_MODE" = false ]; then + find "$modules_path" -name "*.abi.json" -delete find "$modules_path" -name "*.package.swiftinterface" -delete find "$modules_path" -name "*.private.swiftinterface" -delete fi @@ -253,7 +254,7 @@ build_framework() { for dep in "${DEP_MODULES[@]}"; do copy_swiftmodule "$build_products_path" "$archive_path" "$dep" "$modules_path" done - strip_private_interfaces "$modules_path" + strip_release_metadata "$modules_path" } rm -rf "$DERIVED_DATA_PATH" @@ -272,18 +273,22 @@ for sdk in "${SDKS[@]}"; do done xcodebuild -create-xcframework "${create_args[@]}" -output "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework" -# Copy dSYMs into each XCFramework slice for consumers that want local symbols. -for sdk in "${SDKS[@]}"; do - local_dsym_dir="" - case "$sdk" in - iphonesimulator) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework"/ios-*simulator 2>/dev/null | head -1) ;; - iphoneos) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework"/ios-arm64 2>/dev/null | head -1) ;; - macosx) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework"/macos-* 2>/dev/null | head -1) ;; - esac - - if [ -n "$local_dsym_dir" ] && [ -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive/dSYMs" ]; then - cp -R "$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive/dSYMs" "$local_dsym_dir/" - fi -done +if [ "$DEBUG_MODE" = true ]; then + # Copy dSYMs into each XCFramework slice only for debug artifacts. + for sdk in "${SDKS[@]}"; do + local_dsym_dir="" + case "$sdk" in + iphonesimulator) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework"/ios-*simulator 2>/dev/null | head -1) ;; + iphoneos) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework"/ios-arm64 2>/dev/null | head -1) ;; + macosx) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework"/macos-* 2>/dev/null | head -1) ;; + esac + + if [ -n "$local_dsym_dir" ] && [ -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive/dSYMs" ]; then + cp -R "$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive/dSYMs" "$local_dsym_dir/" + fi + done +else + echo "Skipping dSYMs. Pass --debug to include them in the XCFramework." +fi echo "Created $PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework" From cf82efcaaad5d8975f8584694ea65b99c3e7022a Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 5 May 2026 18:19:40 +0800 Subject: [PATCH 03/13] Force ObjC categories into OpenSwiftUI framework --- Package.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Package.swift b/Package.swift index df70357db..7d3f307a5 100644 --- a/Package.swift +++ b/Package.swift @@ -639,6 +639,8 @@ let openSwiftUITarget = Target.target( cxxSettings: sharedCxxSettings, swiftSettings: sharedSwiftSettings, linkerSettings: [ + // Force Objective-C categories from static COpenSwiftUI into the final framework. + .unsafeFlags(["-Xlinker", "-ObjC"], .when(platforms: .darwinPlatforms)), // -framework CoreServices // For CS private API link support .linkedFramework("CoreServices", .when(platforms: [.iOS])), From b327c854b13a6db6037c316f46c7ec4222d2d9b2 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 5 May 2026 18:43:14 +0800 Subject: [PATCH 04/13] Use mise.toml for Tuist tool setup --- .github/actions/build-xcframework/action.yml | 11 ++++++++++- .tool-versions | 1 - mise.toml | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) delete mode 100644 .tool-versions create mode 100644 mise.toml diff --git a/.github/actions/build-xcframework/action.yml b/.github/actions/build-xcframework/action.yml index 92a3b8276..02114ecde 100644 --- a/.github/actions/build-xcframework/action.yml +++ b/.github/actions/build-xcframework/action.yml @@ -34,8 +34,17 @@ runs: uses: OpenSwiftUIProject/setup-xcode@v2 with: xcode-version: ${{ inputs.xcode-version }} - - name: Install Tuist + - name: Set up mise uses: jdx/mise-action@v2 + with: + install: false + cache: false + - name: Install Tuist + run: | + mise trust mise.toml + mise install + tuist version + shell: bash - name: Set up build environment run: Scripts/CI/darwin_setup_build.sh shell: bash diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index a2b8ed8ef..000000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -tuist 4.174.2 diff --git a/mise.toml b/mise.toml new file mode 100644 index 000000000..54b06a41a --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +tuist = "4.174.2" From 07592a14754d36fd47846bc7ba41e7c95e5f3e5d Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 5 May 2026 20:33:14 +0800 Subject: [PATCH 05/13] Update workaround for tuist issue --- Package.resolved | 2 +- Package.swift | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Package.resolved b/Package.resolved index 7d2f6f386..c1038b263 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b03bdf25c5bef5ea9cf20585206be5013c0b53257e9ebab75c9f19d480cd17ed", + "originHash" : "181200ab8951ff06d346194e4de80b86efbead80fa8e5e420013c921e7641cc6", "pins" : [ { "identity" : "darwinprivateframeworks", diff --git a/Package.swift b/Package.swift index 7d3f307a5..dd398068a 100644 --- a/Package.swift +++ b/Package.swift @@ -19,6 +19,7 @@ public struct PackageContextEnvironmentProvider: EnvironmentProvider { public func value(forKey key: String) -> String? { #if TUIST + // FIXME: upstream issue tuist#10616 ProcessInfo.processInfo.environment[key] #else Context.environment[key] @@ -149,6 +150,7 @@ let development = envBoolValue("DEVELOPMENT", default: false) let warningsAsErrorsCondition = envBoolValue("WERROR", default: isXcodeEnv && development) #if TUIST +// FIXME: upstream issue tuist#10616 let packageDirectory = FileManager.default.currentDirectoryPath #else let packageDirectory = Context.packageDirectory From 557f2f81abda9219690e898e57e53f93d066b57d Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 5 May 2026 20:35:57 +0800 Subject: [PATCH 06/13] Fix SymbolDualTestsSupport module map --- .../OpenSwiftUISymbolDualTestsSupport.h | 4 ---- Sources/OpenSwiftUISymbolDualTestsSupport/module.modulemap | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 Sources/OpenSwiftUISymbolDualTestsSupport/OpenSwiftUISymbolDualTestsSupport.h create mode 100644 Sources/OpenSwiftUISymbolDualTestsSupport/module.modulemap diff --git a/Sources/OpenSwiftUISymbolDualTestsSupport/OpenSwiftUISymbolDualTestsSupport.h b/Sources/OpenSwiftUISymbolDualTestsSupport/OpenSwiftUISymbolDualTestsSupport.h deleted file mode 100644 index 898a44886..000000000 --- a/Sources/OpenSwiftUISymbolDualTestsSupport/OpenSwiftUISymbolDualTestsSupport.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef OPENSWIFTUISYMBOLDUALTESTSSUPPORT_H -#define OPENSWIFTUISYMBOLDUALTESTSSUPPORT_H - -#endif /* OPENSWIFTUISYMBOLDUALTESTSSUPPORT_H */ diff --git a/Sources/OpenSwiftUISymbolDualTestsSupport/module.modulemap b/Sources/OpenSwiftUISymbolDualTestsSupport/module.modulemap new file mode 100644 index 000000000..6aed561a7 --- /dev/null +++ b/Sources/OpenSwiftUISymbolDualTestsSupport/module.modulemap @@ -0,0 +1,3 @@ +module OpenSwiftUISymbolDualTestsSupport { + export * +} From fd19b9ae43d00e6ad1aa9f8bcf4f98273379a2cd Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 5 May 2026 22:26:56 +0800 Subject: [PATCH 07/13] Bump DarwinPrivateFrameworks pin --- Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.resolved b/Package.resolved index c1038b263..9d24b6af9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "84f52c13724f8f3725853969a1fa3831b91c7cfb" + "revision" : "abfeee71f12fdd8237726feb93655dcad26cfd8c" } }, { From 6f6f0a7bc1a841ecf415cbc496457143bf4e4afc Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 6 May 2026 00:47:56 +0800 Subject: [PATCH 08/13] Document xcframework packaging tradeoffs --- Docs/XCFrameworkPackaging.md | 250 +++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 Docs/XCFrameworkPackaging.md diff --git a/Docs/XCFrameworkPackaging.md b/Docs/XCFrameworkPackaging.md new file mode 100644 index 000000000..cf93f8de1 --- /dev/null +++ b/Docs/XCFrameworkPackaging.md @@ -0,0 +1,250 @@ +# XCFramework Packaging Notes + +This document records the investigation into distributing OpenSwiftUI as a single +`OpenSwiftUI.xcframework`, the problems found with that shape, and the practical +fallback design. + +## Context + +OpenSwiftUI currently has more than one Swift module in its public build graph. +The important modules for binary distribution are: + +- `OpenSwiftUI` +- `OpenSwiftUICore` +- `OpenObservation` +- `OpenAttributeGraphShims` +- `OpenCoreGraphicsShims` +- `OpenQuartzCoreShims` +- `OpenRenderBoxShims` + +The attempted single-artifact design produced one `OpenSwiftUI.xcframework` +containing one `OpenSwiftUI.framework`. The framework Mach-O linked the object +code from the dependency modules, so the runtime code was present in one binary. +However, the Swift module graph was still multi-module. + +## Root Problem + +Swift binary distribution has two separate concerns: + +- The Mach-O binary must contain or link the implementation code. +- The Swift compiler must be able to resolve every module referenced by public + `.swiftinterface` files. + +The single-framework experiment solved the first concern, but not the second. +The generated `OpenSwiftUI.swiftinterface` still contains public module imports: + +```swift +public import OpenCoreGraphicsShims +public import OpenObservation +@_exported public import OpenSwiftUICore +``` + +`OpenSwiftUICore.swiftinterface` also imports dependency modules: + +```swift +public import OpenCoreGraphicsShims +public import OpenObservation +public import OpenQuartzCoreShims +``` + +Therefore, a client compiling `import OpenSwiftUI` still needs the compiler to +find `OpenSwiftUICore`, `OpenObservation`, and the shim modules as Swift modules, +even when their object code is already linked into `OpenSwiftUI.framework`. + +## Observed Tool Behavior + +### SwiftPM CLI + +For a binary target that points at an xcframework, SwiftPM CLI passes a Swift +include path to the selected xcframework slice root, for example: + +```text +-I Frameworks/OpenSwiftUI.xcframework/macos-arm64 +``` + +If the dependency `.swiftmodule` directories are placed or symlinked at that +slice root, SwiftPM CLI can resolve them without consumer-side `unsafeFlags`. + +### Xcode + +Xcode's package build path is different. `ProcessXCFramework` selects the +matching framework from the xcframework and copies only that framework into the +build products directory: + +```text +Build/Products/Debug/OpenSwiftUI.framework +``` + +The extra files at the xcframework slice root are not copied. Xcode then invokes +Swift with paths similar to: + +```text +-I Build/Products/Debug +-F Build/Products/Debug +``` + +It does not add: + +```text +-I Build/Products/Debug/OpenSwiftUI.framework/Modules +``` + +As a result, dependency modules hidden inside `OpenSwiftUI.framework/Modules` +are not discoverable by Xcode without extra settings. + +## Experiments + +### Consumer Search Paths + +Adding an explicit include path to the consumer works: + +```text +-I Frameworks/OpenSwiftUI.xcframework/macos-arm64/OpenSwiftUI.framework/Modules +``` + +The equivalent Xcode build setting is `SWIFT_INCLUDE_PATHS`. + +This is not a good user-facing integration because every consumer needs a +platform-specific workaround. + +### Slice-Root Module Symlinks + +Adding symlinks at the selected slice root works for SwiftPM CLI: + +```text +OpenSwiftUI.xcframework/macos-arm64/OpenSwiftUICore.swiftmodule + -> OpenSwiftUI.framework/Modules/OpenSwiftUICore.swiftmodule +``` + +This keeps artifact size small and avoids consumer-side `unsafeFlags` for +`swift build`. + +It does not fix Xcode because `ProcessXCFramework` does not copy those slice-root +symlinks into `Build/Products`. + +### Restoring Binary `.swiftmodule` Files + +`xcodebuild -create-xcframework` may drop binary `.swiftmodule` files and keep +textual `.swiftinterface` files. Restoring the binary `.swiftmodule` files into +`OpenSwiftUI.framework/Modules` did not fix Xcode. The binary module still +records dependencies on other Swift modules, and Xcode still needs a search path +that can find them. + +### Removing Imports From `OpenSwiftUI.swiftinterface` + +Removing only: + +```swift +public import OpenCoreGraphicsShims +``` + +from `OpenSwiftUI.swiftinterface` can compile in the simple SwiftPM CLI probe, +because `OpenSwiftUI.swiftinterface` does not directly reference that module. +This is only a cleanup opportunity, not a complete fix, because +`OpenSwiftUICore.swiftinterface` still imports `OpenCoreGraphicsShims`. + +Removing either of these imports is not viable: + +```swift +public import OpenObservation +@_exported public import OpenSwiftUICore +``` + +`OpenSwiftUI.swiftinterface` directly references those modules in public API, for +example `OpenObservation.Observable`, `OpenSwiftUICore.View`, +`OpenSwiftUICore.Binding`, and `OpenSwiftUICore.ViewBuilder`. + +## Possible Workarounds + +### Wrapper Package Search Paths + +A wrapper package could hide the include-path workaround by adding unsafe Swift +flags internally. This keeps the user-facing dependency small, but it is still a +path-sensitive workaround and relies on `unsafeFlags`. + +This should not be the preferred release shape. + +### Module-Only Sidecar Artifacts + +It may be possible to ship module-only or mostly-empty sidecar frameworks while +keeping most object code in `OpenSwiftUI.framework`. This is non-standard and +hard to reason about because Xcode and SwiftPM still need each module to appear +as a normal dependency during compilation. + +This is more fragile than shipping normal static frameworks for each module. + +### True Single Swift Module + +The structural fix for one `OpenSwiftUI.xcframework` is to make the public Swift +module graph truly single-module. That means the distributed +`OpenSwiftUI.swiftinterface` must not reference `OpenSwiftUICore`, +`OpenObservation`, or shim modules as separate modules. + +Possible ways to get there: + +- Move or compile the public distribution sources into one `OpenSwiftUI` module. +- Add a distribution-only target that compiles the relevant sources under the + `OpenSwiftUI` module name. +- Avoid exposing dependency module names in public API and generated + `.swiftinterface` files. + +This is the cleanest single-artifact design, but it is a larger architectural +change because the current source and test structure intentionally uses multiple +modules. + +## Recommended Fallback + +Use multiple xcframeworks, one per Swift module, and expose them through one +Swift package product. + +Prefer static frameworks for these xcframeworks: + +- They preserve the Swift module graph for the compiler. +- They avoid embedding many dynamic frameworks into client apps. +- They let the final app link the implementation code into the app binary. +- They keep the user-facing API as one package product. + +The package shape should be similar to: + +```swift +let package = Package( + name: "OpenSwiftUI", + products: [ + .library( + name: "OpenSwiftUI", + targets: [ + "OpenSwiftUI", + "OpenSwiftUICore", + "OpenObservation", + "OpenAttributeGraphShims", + "OpenCoreGraphicsShims", + "OpenQuartzCoreShims", + "OpenRenderBoxShims", + ] + ), + ], + targets: [ + .binaryTarget(name: "OpenSwiftUI", url: "...", checksum: "..."), + .binaryTarget(name: "OpenSwiftUICore", url: "...", checksum: "..."), + .binaryTarget(name: "OpenObservation", url: "...", checksum: "..."), + .binaryTarget(name: "OpenAttributeGraphShims", url: "...", checksum: "..."), + .binaryTarget(name: "OpenCoreGraphicsShims", url: "...", checksum: "..."), + .binaryTarget(name: "OpenQuartzCoreShims", url: "...", checksum: "..."), + .binaryTarget(name: "OpenRenderBoxShims", url: "...", checksum: "..."), + ] +) +``` + +Consumers still write: + +```swift +import OpenSwiftUI +``` + +and depend on the single `OpenSwiftUI` package product. The distribution uses +multiple binary targets internally only so that Xcode and SwiftPM can resolve the +Swift module graph normally. + +Dynamic frameworks should be avoided unless there is a runtime reason to share +or load the frameworks dynamically. They make embedding, signing, launch-time +loading, and artifact management more complicated. From 9751eba9c3e27c1429471c91b7fcf85a49832693 Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 6 May 2026 01:32:08 +0800 Subject: [PATCH 09/13] Build multiple xcframework artifacts --- .github/actions/build-xcframework/action.yml | 38 ++- .github/workflows/build_xcframework.yml | 18 +- .github/workflows/release.yml | 18 ++ Scripts/build_xcframework.sh | 312 +++++++++++++------ 4 files changed, 281 insertions(+), 105 deletions(-) diff --git a/.github/actions/build-xcframework/action.yml b/.github/actions/build-xcframework/action.yml index 02114ecde..94b319302 100644 --- a/.github/actions/build-xcframework/action.yml +++ b/.github/actions/build-xcframework/action.yml @@ -1,5 +1,5 @@ name: 'Build XCFramework' -description: 'Build OpenSwiftUI.xcframework and generate its binaryTarget entry' +description: 'Build OpenSwiftUI XCFrameworks and generate binaryTarget entries' inputs: xcode-version: @@ -26,6 +26,24 @@ outputs: checksum_OpenSwiftUI: description: 'Checksum for OpenSwiftUI.xcframework.zip' value: ${{ steps.checksums.outputs.checksum_OpenSwiftUI }} + checksum_OpenSwiftUICore: + description: 'Checksum for OpenSwiftUICore.xcframework.zip' + value: ${{ steps.checksums.outputs.checksum_OpenSwiftUICore }} + checksum_OpenAttributeGraphShims: + description: 'Checksum for OpenAttributeGraphShims.xcframework.zip' + value: ${{ steps.checksums.outputs.checksum_OpenAttributeGraphShims }} + checksum_OpenCoreGraphicsShims: + description: 'Checksum for OpenCoreGraphicsShims.xcframework.zip' + value: ${{ steps.checksums.outputs.checksum_OpenCoreGraphicsShims }} + checksum_OpenObservation: + description: 'Checksum for OpenObservation.xcframework.zip' + value: ${{ steps.checksums.outputs.checksum_OpenObservation }} + checksum_OpenQuartzCoreShims: + description: 'Checksum for OpenQuartzCoreShims.xcframework.zip' + value: ${{ steps.checksums.outputs.checksum_OpenQuartzCoreShims }} + checksum_OpenRenderBoxShims: + description: 'Checksum for OpenRenderBoxShims.xcframework.zip' + value: ${{ steps.checksums.outputs.checksum_OpenRenderBoxShims }} runs: using: 'composite' @@ -51,13 +69,23 @@ runs: - name: Build XCFramework run: Scripts/build_xcframework.sh OpenSwiftUI shell: bash + - name: Collect XCFramework Paths + id: xcframeworks + shell: bash + run: | + mapfile -t paths < <(find build -maxdepth 1 -type d -name '*.xcframework' | sort) + if [ ${#paths[@]} -eq 0 ]; then + echo "::error::No XCFrameworks were produced" + exit 1 + fi + echo "paths=${paths[*]}" >> "$GITHUB_OUTPUT" - name: Code sign XCFramework if: ${{ inputs.signing-certificate-base64 != '' }} uses: ./.github/actions/codesign-xcframework with: signing-certificate-base64: ${{ inputs.signing-certificate-base64 }} signing-certificate-password: ${{ inputs.signing-certificate-password }} - xcframework-paths: build/OpenSwiftUI.xcframework + xcframework-paths: ${{ steps.xcframeworks.outputs.paths }} - name: Compute Checksums and Generate Summary id: checksums shell: bash @@ -65,7 +93,11 @@ runs: TAG_NAME: ${{ inputs.tag-name }} run: | cd build - FRAMEWORKS=(OpenSwiftUI) + mapfile -t FRAMEWORKS < <(find . -maxdepth 1 -type d -name '*.xcframework' -exec basename {} .xcframework \; | sort) + if [ ${#FRAMEWORKS[@]} -eq 0 ]; then + echo "::error::No XCFrameworks were produced" + exit 1 + fi BODY="" echo "### XCFramework Binary Targets" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/build_xcframework.yml b/.github/workflows/build_xcframework.yml index b6a2afc72..4c94526f0 100644 --- a/.github/workflows/build_xcframework.yml +++ b/.github/workflows/build_xcframework.yml @@ -18,10 +18,20 @@ jobs: with: signing-certificate-base64: ${{ secrets.SIGNING_CERTIFICATE_BASE_64 }} signing-certificate-password: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }} - - name: Zip XCFramework - run: cd build && zip -ry OpenSwiftUI.xcframework.zip OpenSwiftUI.xcframework - - name: Upload XCFramework + - name: Zip XCFrameworks + run: | + cd build + shopt -s nullglob + frameworks=(*.xcframework) + if [ ${#frameworks[@]} -eq 0 ]; then + echo "::error::No XCFrameworks were produced" + exit 1 + fi + for framework in "${frameworks[@]}"; do + zip -ry "$framework.zip" "$framework" + done + - name: Upload XCFrameworks uses: actions/upload-artifact@v7 with: - path: build/OpenSwiftUI.xcframework.zip + path: build/*.xcframework.zip archive: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f17a57761..282590992 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,12 @@ jobs: runs-on: macos-15 outputs: checksum_OpenSwiftUI: ${{ steps.build.outputs.checksum_OpenSwiftUI }} + checksum_OpenSwiftUICore: ${{ steps.build.outputs.checksum_OpenSwiftUICore }} + checksum_OpenAttributeGraphShims: ${{ steps.build.outputs.checksum_OpenAttributeGraphShims }} + checksum_OpenCoreGraphicsShims: ${{ steps.build.outputs.checksum_OpenCoreGraphicsShims }} + checksum_OpenObservation: ${{ steps.build.outputs.checksum_OpenObservation }} + checksum_OpenQuartzCoreShims: ${{ steps.build.outputs.checksum_OpenQuartzCoreShims }} + checksum_OpenRenderBoxShims: ${{ steps.build.outputs.checksum_OpenRenderBoxShims }} env: OPENSWIFTUI_OPENATTRIBUTESHIMS_ATTRIBUTEGRAPH: 1 OPENSWIFTUI_USE_LOCAL_DEPS: 1 @@ -48,6 +54,12 @@ jobs: env: VERSION: ${{ github.ref_name }} CHECKSUM_OpenSwiftUI: ${{ needs.build-xcframework.outputs.checksum_OpenSwiftUI }} + CHECKSUM_OpenSwiftUICore: ${{ needs.build-xcframework.outputs.checksum_OpenSwiftUICore }} + CHECKSUM_OpenAttributeGraphShims: ${{ needs.build-xcframework.outputs.checksum_OpenAttributeGraphShims }} + CHECKSUM_OpenCoreGraphicsShims: ${{ needs.build-xcframework.outputs.checksum_OpenCoreGraphicsShims }} + CHECKSUM_OpenObservation: ${{ needs.build-xcframework.outputs.checksum_OpenObservation }} + CHECKSUM_OpenQuartzCoreShims: ${{ needs.build-xcframework.outputs.checksum_OpenQuartzCoreShims }} + CHECKSUM_OpenRenderBoxShims: ${{ needs.build-xcframework.outputs.checksum_OpenRenderBoxShims }} run: | if [ -z "$BINARY_REPO_PAT" ]; then echo "::notice::BINARY_REPO_PAT not set, skipping binary repo update" @@ -60,6 +72,12 @@ jobs: sed \ -e "s|{{VERSION}}|${VERSION}|g" \ -e "s|{{CHECKSUM_OpenSwiftUI}}|${CHECKSUM_OpenSwiftUI}|g" \ + -e "s|{{CHECKSUM_OpenSwiftUICore}}|${CHECKSUM_OpenSwiftUICore}|g" \ + -e "s|{{CHECKSUM_OpenAttributeGraphShims}}|${CHECKSUM_OpenAttributeGraphShims}|g" \ + -e "s|{{CHECKSUM_OpenCoreGraphicsShims}}|${CHECKSUM_OpenCoreGraphicsShims}|g" \ + -e "s|{{CHECKSUM_OpenObservation}}|${CHECKSUM_OpenObservation}|g" \ + -e "s|{{CHECKSUM_OpenQuartzCoreShims}}|${CHECKSUM_OpenQuartzCoreShims}|g" \ + -e "s|{{CHECKSUM_OpenRenderBoxShims}}|${CHECKSUM_OpenRenderBoxShims}|g" \ Package.swift.template > Package.swift echo "Generated Package.swift:" head -50 Package.swift diff --git a/Scripts/build_xcframework.sh b/Scripts/build_xcframework.sh index 368960948..d8b60350a 100755 --- a/Scripts/build_xcframework.sh +++ b/Scripts/build_xcframework.sh @@ -18,8 +18,19 @@ XCODEWORKSPACE="$PROJECT_ROOT/Workspace.xcworkspace" SDKS=() SDK_ARCHS=() DEBUG_MODE=false -PACKAGE_NAME="OpenSwiftUI" RUN_TUIST_INSTALL=true +EXPLICIT_FRAMEWORK_NAMES=false +FRAMEWORK_NAMES=() + +DEFAULT_FRAMEWORK_NAMES=( + "OpenAttributeGraphShims" + "OpenCoreGraphicsShims" + "OpenQuartzCoreShims" + "OpenObservation" + "OpenRenderBoxShims" + "OpenSwiftUICore" + "OpenSwiftUI" +) while [[ $# -gt 0 ]]; do case "$1" in @@ -42,13 +53,28 @@ while [[ $# -gt 0 ]]; do RUN_TUIST_INSTALL=false shift ;; + --framework) + EXPLICIT_FRAMEWORK_NAMES=true + FRAMEWORK_NAMES+=("$2") + shift 2 + ;; *) - PACKAGE_NAME="$1" + FRAMEWORK_NAMES+=("$1") shift ;; esac done +# By default, build the full binary distribution set. Keep +# `Scripts/build_xcframework.sh OpenSwiftUI` compatible with the historical CI +# invocation while switching it to the multi-xcframework distribution. +if [ ${#FRAMEWORK_NAMES[@]} -eq 0 ] || + ([ "$EXPLICIT_FRAMEWORK_NAMES" = false ] && + [ ${#FRAMEWORK_NAMES[@]} -eq 1 ] && + [ "${FRAMEWORK_NAMES[0]}" = "OpenSwiftUI" ]); then + FRAMEWORK_NAMES=("${DEFAULT_FRAMEWORK_NAMES[@]}") +fi + # Default: macosx and iphonesimulator. # Note: iphoneos SDK support is blocked by an AG issue. See #835. if [ ${#SDKS[@]} -eq 0 ]; then @@ -61,7 +87,7 @@ if [ "${OPENSWIFTUI_SKIP_TUIST_INSTALL:-0}" = "1" ]; then fi if ! command -v tuist >/dev/null 2>&1; then - echo "Error: tuist is required to generate $PACKAGE_NAME.xcodeproj." + echo "Error: tuist is required to generate $XCODEPROJ." exit 1 fi @@ -85,6 +111,116 @@ if [ ! -d "$XCODEWORKSPACE" ]; then exit 1 fi +remove_generated_macro_references() { + local project_path="$1" + local target_name="$2" + local macro_name="$3" + + if [ ! -f "$project_path/project.pbxproj" ]; then + return + fi + + ruby - "$project_path/project.pbxproj" "$target_name" "$macro_name" <<'RUBY' +path, target_name, macro_name = ARGV +text = File.read(path) +object_pattern = /^ ([A-Za-z0-9]+) \/\* [^*]+ \*\/ = \{\n.*?^ \};\n/m + +dependency_names = {} +text.scan(object_pattern) do |capture| + id = Array(capture).first + block = Regexp.last_match(0) + next unless block.include?("isa = PBXTargetDependency;") + + dependency_names[id] = + block[/^ name = ([^;]+);$/, 1] || + block[/^ target = [A-Za-z0-9]+ \/\* ([^*]+) \*\//, 1] +end + +macro_phase_ids = [] +text.scan(object_pattern) do |capture| + id = Array(capture).first + block = Regexp.last_match(0) + next unless block.include?("isa = PBXShellScriptBuildPhase;") + next unless block.include?(macro_name) + + macro_phase_ids << id +end + +macro_target_ids = [] +text.scan(object_pattern) do |capture| + id = Array(capture).first + block = Regexp.last_match(0) + next unless block.include?("isa = PBXNativeTarget;") + next unless block.match?(/^ name = #{Regexp.escape(macro_name)};$/) + + macro_target_ids << id +end + +target_dependency_ids = [] +text.scan(object_pattern) do |_capture| + block = Regexp.last_match(0) + next unless block.include?("isa = PBXNativeTarget;") + next unless block.match?(/^ name = #{Regexp.escape(target_name)};$/) + + dependencies = block[/^ dependencies = \(\n(.*?)^ \);$/m, 1] + target_dependency_ids = dependencies.to_s.scan(/^\s+([A-Za-z0-9]+) \/\* PBXTargetDependency \*\/,/).flatten + break +end + +dependency_ids_to_remove = target_dependency_ids.select { |id| dependency_names[id] == macro_name } + +# Tuist can leave a standalone macro PBXTargetDependency in derived projects even +# when the framework target does not list it in `dependencies`. Xcode's automatic +# scheme discovery can still pull that target into archive builds, so prune it +# when it is unambiguous. +if dependency_ids_to_remove.empty? + matching_ids = dependency_names.select { |_id, name| name == macro_name }.keys + dependency_ids_to_remove = matching_ids if matching_ids.one? +end + +changed = false + +dependency_ids_to_remove.each do |id| + text.gsub!(/^ #{Regexp.escape(id)} \/\* PBXTargetDependency \*\/,\n/, "") + removed = text.gsub!(/^ #{Regexp.escape(id)} \/\* PBXTargetDependency \*\/ = \{\n.*?^ \};\n/m, "") + changed ||= !!removed +end + +macro_phase_ids.each do |id| + text.gsub!(/^ #{Regexp.escape(id)} \/\* [^*]+ \*\/,\n/, "") + removed = text.gsub!(/^ #{Regexp.escape(id)} \/\* [^*]+ \*\/ = \{\n.*?^ \};\n/m, "") + changed ||= !!removed +end + +macro_target_ids.each do |id| + removed = text.gsub!(/^ #{Regexp.escape(id)} \/\* #{Regexp.escape(macro_name)} \*\/,\n/, "") + changed ||= !!removed +end + +plugin_path = '$BUILD_DIR/Debug$EFFECTIVE_PLATFORM_NAME/' + macro_name + '#' + macro_name +plugin_path_pattern = Regexp.escape(plugin_path) +removed_flags = text.gsub!( + /^(\t+)"-load-plugin-executable",\n\1"#{plugin_path_pattern}",\n/, + "" +) +changed ||= !!removed_flags + +exit unless changed + +File.write(path, text) +puts "Removed #{macro_name} archive references from #{File.dirname(path)}" +RUBY +} + +# The xcframework archive path sets OPENSWIFTUI_XCFRAMEWORK_BUILD, which expands +# macro usages inline where needed. Avoid forcing generated macro tool targets to +# archive for simulator SDKs, where Xcode can try to build them for the target +# platform instead of the host platform. +remove_generated_macro_references "$PROJECT_ROOT/.build/tuist-derived/OpenObservation/OpenObservation.xcodeproj" "OpenObservation" "OpenObservationMacros" +remove_generated_macro_references "$PROJECT_ROOT/../OpenObservation/OpenObservation.xcodeproj" "OpenObservation" "OpenObservationMacros" +remove_generated_macro_references "$XCODEPROJ" "OpenSwiftUICore" "OpenSwiftUIMacros" +remove_generated_macro_references "$XCODEPROJ" "OpenSwiftUICore" "OpenObservationMacros" + echo "SDKs: ${SDKS[*]}" for i in "${!SDKS[@]}"; do if [ -n "${SDK_ARCHS[$i]}" ]; then @@ -94,17 +230,7 @@ for i in "${!SDKS[@]}"; do fi done echo "Debug mode: $DEBUG_MODE" - -# Dependency modules referenced by public swiftinterfaces. They are copied into -# the one distributable framework instead of emitted as separate stub frameworks. -DEP_MODULES=( - "OpenSwiftUICore" - "OpenAttributeGraphShims" - "OpenCoreGraphicsShims" - "OpenObservation" - "OpenQuartzCoreShims" - "OpenRenderBoxShims" -) +echo "Frameworks: ${FRAMEWORK_NAMES[*]}" sdk_destination() { case "$1" in @@ -115,13 +241,6 @@ sdk_destination() { esac } -build_products_suffix() { - case "$1" in - macosx) echo "Release" ;; - *) echo "Release-$1" ;; - esac -} - framework_path() { local archive_path="$1" local scheme="$2" @@ -146,56 +265,36 @@ strip_release_metadata() { fi } -canonical_path() { - local path="$1" - if [ -e "$path" ]; then - (cd "$(dirname "$path")" && printf "%s/%s\n" "$(pwd -P)" "$(basename "$path")") - else - echo "$path" - fi -} - -find_swiftmodule() { - local build_products_path="$1" - local archive_path="$2" - local module_name="$3" - - local candidates=( - "$build_products_path/$module_name.swiftmodule" - "$build_products_path/$module_name.framework/Modules/$module_name.swiftmodule" - "$build_products_path/$module_name.framework/Versions/A/Modules/$module_name.swiftmodule" - "$archive_path/Products/Library/Frameworks/$module_name.framework/Modules/$module_name.swiftmodule" - "$archive_path/Products/Library/Frameworks/$module_name.framework/Versions/A/Modules/$module_name.swiftmodule" - ) - - for candidate in "${candidates[@]}"; do - if [ -d "$candidate" ]; then - echo "$candidate" - return - fi - done -} - -copy_swiftmodule() { - local build_products_path="$1" - local archive_path="$2" - local module_name="$3" - local modules_path="$4" - - local swiftmodule - swiftmodule="$(find_swiftmodule "$build_products_path" "$archive_path" "$module_name")" - if [ -z "$swiftmodule" ]; then - echo "Warning: No swiftmodule found for $module_name." - return - fi +project_args_for_scheme() { + local scheme="$1" - local destination="$modules_path/$module_name.swiftmodule" - if [ "$(canonical_path "$swiftmodule")" = "$(canonical_path "$destination")" ]; then - return - fi - - rm -rf "$destination" - cp -R "$swiftmodule" "$destination" + case "$scheme" in + COpenSwiftUI|OpenSwiftUI|OpenSwiftUICore|OpenSwiftUI_SPI|OpenSwiftUISymbolDualTestsSupport) + echo "-workspace" "$XCODEWORKSPACE" + ;; + _AttributeGraphDeviceSwiftShims) + echo "-project" "$PROJECT_ROOT/.build/tuist-derived/DarwinPrivateFrameworks/DarwinPrivateFrameworks.xcodeproj" + ;; + OpenAttributeGraphShims) + echo "-project" "$PROJECT_ROOT/.build/tuist-derived/OpenAttributeGraph/OpenAttributeGraph.xcodeproj" + ;; + OpenCoreGraphics|OpenCoreGraphicsShims|OpenQuartzCore|OpenQuartzCoreShims) + echo "-project" "$PROJECT_ROOT/.build/tuist-derived/OpenCoreGraphics/OpenCoreGraphics.xcodeproj" + ;; + OpenObservation|OpenObservationCxx) + echo "-project" "$PROJECT_ROOT/.build/tuist-derived/OpenObservation/OpenObservation.xcodeproj" + ;; + OpenRenderBoxShims) + echo "-project" "$PROJECT_ROOT/.build/tuist-derived/OpenRenderBox/OpenRenderBox.xcodeproj" + ;; + SymbolLocator) + echo "-project" "$PROJECT_ROOT/.build/tuist-derived/SymbolLocator/SymbolLocator.xcodeproj" + ;; + *) + echo "Error: No Xcode project mapping for $scheme." >&2 + exit 1 + ;; + esac } build_framework() { @@ -205,13 +304,14 @@ build_framework() { local archs="$4" local archive_path="$PROJECT_BUILD_DIR/$scheme-$sdk.xcarchive" - local build_products_path="$DERIVED_DATA_PATH/Build/Intermediates.noindex/ArchiveIntermediates/$scheme/BuildProductsPath/$(build_products_suffix "$sdk")" + local project_args + read -r -a project_args <<<"$(project_args_for_scheme "$scheme")" rm -rf "$archive_path" local xcodebuild_args=( archive - -workspace "$XCODEWORKSPACE" + "${project_args[@]}" -scheme "$scheme" -configuration Release -archivePath "$archive_path" @@ -250,45 +350,61 @@ build_framework() { ln -s Versions/Current/Modules "$framework/Modules" fi - copy_swiftmodule "$build_products_path" "$archive_path" "$scheme" "$modules_path" - for dep in "${DEP_MODULES[@]}"; do - copy_swiftmodule "$build_products_path" "$archive_path" "$dep" "$modules_path" - done strip_release_metadata "$modules_path" } -rm -rf "$DERIVED_DATA_PATH" -mkdir -p "$PROJECT_BUILD_DIR" +create_xcframework() { + local scheme="$1" -for i in "${!SDKS[@]}"; do - build_framework "${SDKS[$i]}" "$(sdk_destination "${SDKS[$i]}")" "$PACKAGE_NAME" "${SDK_ARCHS[$i]}" -done + rm -rf "$PROJECT_BUILD_DIR/$scheme.xcframework" + + local create_args=() + for sdk in "${SDKS[@]}"; do + create_args+=(-framework "$(framework_path "$PROJECT_BUILD_DIR/$scheme-$sdk.xcarchive" "$scheme")") + done -echo "Archives completed successfully." + xcodebuild -create-xcframework "${create_args[@]}" -output "$PROJECT_BUILD_DIR/$scheme.xcframework" +} -rm -rf "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework" -create_args=() -for sdk in "${SDKS[@]}"; do - create_args+=(-framework "$(framework_path "$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive" "$PACKAGE_NAME")") -done -xcodebuild -create-xcframework "${create_args[@]}" -output "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework" +copy_debug_symbols() { + local scheme="$1" + + if [ "$DEBUG_MODE" = false ]; then + return + fi -if [ "$DEBUG_MODE" = true ]; then - # Copy dSYMs into each XCFramework slice only for debug artifacts. + local sdk for sdk in "${SDKS[@]}"; do - local_dsym_dir="" + local local_dsym_dir="" case "$sdk" in - iphonesimulator) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework"/ios-*simulator 2>/dev/null | head -1) ;; - iphoneos) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework"/ios-arm64 2>/dev/null | head -1) ;; - macosx) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework"/macos-* 2>/dev/null | head -1) ;; + iphonesimulator) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$scheme.xcframework"/ios-*simulator 2>/dev/null | head -1) ;; + iphoneos) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$scheme.xcframework"/ios-arm64 2>/dev/null | head -1) ;; + macosx) local_dsym_dir=$(ls -d "$PROJECT_BUILD_DIR/$scheme.xcframework"/macos-* 2>/dev/null | head -1) ;; esac - if [ -n "$local_dsym_dir" ] && [ -d "$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive/dSYMs" ]; then - cp -R "$PROJECT_BUILD_DIR/$PACKAGE_NAME-$sdk.xcarchive/dSYMs" "$local_dsym_dir/" + if [ -n "$local_dsym_dir" ] && [ -d "$PROJECT_BUILD_DIR/$scheme-$sdk.xcarchive/dSYMs" ]; then + cp -R "$PROJECT_BUILD_DIR/$scheme-$sdk.xcarchive/dSYMs" "$local_dsym_dir/" fi done +} + +rm -rf "$DERIVED_DATA_PATH" +mkdir -p "$PROJECT_BUILD_DIR" + +for scheme in "${FRAMEWORK_NAMES[@]}"; do + echo "Building $scheme..." + for i in "${!SDKS[@]}"; do + build_framework "${SDKS[$i]}" "$(sdk_destination "${SDKS[$i]}")" "$scheme" "${SDK_ARCHS[$i]}" + done + create_xcframework "$scheme" + copy_debug_symbols "$scheme" + echo "Created $PROJECT_BUILD_DIR/$scheme.xcframework" +done + +if [ "$DEBUG_MODE" = false ]; then + echo "Skipping dSYMs. Pass --debug to include them in the XCFrameworks." else - echo "Skipping dSYMs. Pass --debug to include them in the XCFramework." + echo "Copied dSYMs into the XCFrameworks." fi -echo "Created $PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework" +echo "Created ${#FRAMEWORK_NAMES[@]} XCFrameworks in $PROJECT_BUILD_DIR." From 4a094c2fac9eec1bb1687470479f7e5e722f517e Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 6 May 2026 01:38:20 +0800 Subject: [PATCH 10/13] Resolve local dependency projects for xcframework builds --- Scripts/build_xcframework.sh | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Scripts/build_xcframework.sh b/Scripts/build_xcframework.sh index d8b60350a..defcb3007 100755 --- a/Scripts/build_xcframework.sh +++ b/Scripts/build_xcframework.sh @@ -265,6 +265,18 @@ strip_release_metadata() { fi } +first_existing_project() { + local candidate + for candidate in "$@"; do + if [ -d "$candidate" ]; then + echo "$candidate" + return + fi + done + + echo "$1" +} + project_args_for_scheme() { local scheme="$1" @@ -273,19 +285,19 @@ project_args_for_scheme() { echo "-workspace" "$XCODEWORKSPACE" ;; _AttributeGraphDeviceSwiftShims) - echo "-project" "$PROJECT_ROOT/.build/tuist-derived/DarwinPrivateFrameworks/DarwinPrivateFrameworks.xcodeproj" + echo "-project" "$(first_existing_project "$PROJECT_ROOT/../DarwinPrivateFrameworks/DarwinPrivateFrameworks.xcodeproj" "$PROJECT_ROOT/.build/tuist-derived/DarwinPrivateFrameworks/DarwinPrivateFrameworks.xcodeproj")" ;; OpenAttributeGraphShims) - echo "-project" "$PROJECT_ROOT/.build/tuist-derived/OpenAttributeGraph/OpenAttributeGraph.xcodeproj" + echo "-project" "$(first_existing_project "$PROJECT_ROOT/../OpenAttributeGraph/OpenAttributeGraph.xcodeproj" "$PROJECT_ROOT/.build/tuist-derived/OpenAttributeGraph/OpenAttributeGraph.xcodeproj")" ;; OpenCoreGraphics|OpenCoreGraphicsShims|OpenQuartzCore|OpenQuartzCoreShims) - echo "-project" "$PROJECT_ROOT/.build/tuist-derived/OpenCoreGraphics/OpenCoreGraphics.xcodeproj" + echo "-project" "$(first_existing_project "$PROJECT_ROOT/../OpenCoreGraphics/OpenCoreGraphics.xcodeproj" "$PROJECT_ROOT/.build/tuist-derived/OpenCoreGraphics/OpenCoreGraphics.xcodeproj")" ;; OpenObservation|OpenObservationCxx) - echo "-project" "$PROJECT_ROOT/.build/tuist-derived/OpenObservation/OpenObservation.xcodeproj" + echo "-project" "$(first_existing_project "$PROJECT_ROOT/../OpenObservation/OpenObservation.xcodeproj" "$PROJECT_ROOT/.build/tuist-derived/OpenObservation/OpenObservation.xcodeproj")" ;; OpenRenderBoxShims) - echo "-project" "$PROJECT_ROOT/.build/tuist-derived/OpenRenderBox/OpenRenderBox.xcodeproj" + echo "-project" "$(first_existing_project "$PROJECT_ROOT/../OpenRenderBox/OpenRenderBox.xcodeproj" "$PROJECT_ROOT/.build/tuist-derived/OpenRenderBox/OpenRenderBox.xcodeproj")" ;; SymbolLocator) echo "-project" "$PROJECT_ROOT/.build/tuist-derived/SymbolLocator/SymbolLocator.xcodeproj" From 069aea678e65e344c74c34da7b390916a5c8f79f Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 6 May 2026 02:01:03 +0800 Subject: [PATCH 11/13] Use bash 3 compatible xcframework collection --- .github/actions/build-xcframework/action.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-xcframework/action.yml b/.github/actions/build-xcframework/action.yml index 94b319302..dbdd58fd1 100644 --- a/.github/actions/build-xcframework/action.yml +++ b/.github/actions/build-xcframework/action.yml @@ -73,7 +73,10 @@ runs: id: xcframeworks shell: bash run: | - mapfile -t paths < <(find build -maxdepth 1 -type d -name '*.xcframework' | sort) + paths=() + while IFS= read -r path; do + paths+=("$path") + done < <(find build -maxdepth 1 -type d -name '*.xcframework' | sort) if [ ${#paths[@]} -eq 0 ]; then echo "::error::No XCFrameworks were produced" exit 1 @@ -93,7 +96,10 @@ runs: TAG_NAME: ${{ inputs.tag-name }} run: | cd build - mapfile -t FRAMEWORKS < <(find . -maxdepth 1 -type d -name '*.xcframework' -exec basename {} .xcframework \; | sort) + FRAMEWORKS=() + while IFS= read -r framework; do + FRAMEWORKS+=("$framework") + done < <(find . -maxdepth 1 -type d -name '*.xcframework' -exec basename {} .xcframework \; | sort) if [ ${#FRAMEWORKS[@]} -eq 0 ]; then echo "::error::No XCFrameworks were produced" exit 1 From eb8c697483a976759d25a8eb2d1353d4b1b3a306 Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 6 May 2026 02:22:58 +0800 Subject: [PATCH 12/13] Upload multi xcframework artifacts together --- .github/workflows/build_xcframework.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_xcframework.yml b/.github/workflows/build_xcframework.yml index 4c94526f0..06b1e69f5 100644 --- a/.github/workflows/build_xcframework.yml +++ b/.github/workflows/build_xcframework.yml @@ -33,5 +33,5 @@ jobs: - name: Upload XCFrameworks uses: actions/upload-artifact@v7 with: + name: xcframeworks path: build/*.xcframework.zip - archive: false From e896965443a6a1c6b91b22f1db9f1a64b8992b40 Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 6 May 2026 02:27:01 +0800 Subject: [PATCH 13/13] Simplify xcframework CI packaging --- .github/actions/build-xcframework/action.yml | 47 +++++++++----------- .github/workflows/build_xcframework.yml | 20 +++------ .github/workflows/release.yml | 22 ++++----- 3 files changed, 37 insertions(+), 52 deletions(-) diff --git a/.github/actions/build-xcframework/action.yml b/.github/actions/build-xcframework/action.yml index dbdd58fd1..e8af2ac9b 100644 --- a/.github/actions/build-xcframework/action.yml +++ b/.github/actions/build-xcframework/action.yml @@ -1,5 +1,5 @@ -name: 'Build XCFramework' -description: 'Build OpenSwiftUI XCFrameworks and generate binaryTarget entries' +name: 'Build XCFrameworks' +description: 'Build OpenSwiftUI xcframeworks and generate binaryTarget entries' inputs: xcode-version: @@ -66,29 +66,23 @@ runs: - name: Set up build environment run: Scripts/CI/darwin_setup_build.sh shell: bash - - name: Build XCFramework + - name: Build XCFrameworks run: Scripts/build_xcframework.sh OpenSwiftUI shell: bash - - name: Collect XCFramework Paths - id: xcframeworks - shell: bash - run: | - paths=() - while IFS= read -r path; do - paths+=("$path") - done < <(find build -maxdepth 1 -type d -name '*.xcframework' | sort) - if [ ${#paths[@]} -eq 0 ]; then - echo "::error::No XCFrameworks were produced" - exit 1 - fi - echo "paths=${paths[*]}" >> "$GITHUB_OUTPUT" - - name: Code sign XCFramework + - name: Code sign XCFrameworks if: ${{ inputs.signing-certificate-base64 != '' }} uses: ./.github/actions/codesign-xcframework with: signing-certificate-base64: ${{ inputs.signing-certificate-base64 }} signing-certificate-password: ${{ inputs.signing-certificate-password }} - xcframework-paths: ${{ steps.xcframeworks.outputs.paths }} + xcframework-paths: >- + build/OpenSwiftUI.xcframework + build/OpenSwiftUICore.xcframework + build/OpenAttributeGraphShims.xcframework + build/OpenCoreGraphicsShims.xcframework + build/OpenObservation.xcframework + build/OpenQuartzCoreShims.xcframework + build/OpenRenderBoxShims.xcframework - name: Compute Checksums and Generate Summary id: checksums shell: bash @@ -96,14 +90,15 @@ runs: TAG_NAME: ${{ inputs.tag-name }} run: | cd build - FRAMEWORKS=() - while IFS= read -r framework; do - FRAMEWORKS+=("$framework") - done < <(find . -maxdepth 1 -type d -name '*.xcframework' -exec basename {} .xcframework \; | sort) - if [ ${#FRAMEWORKS[@]} -eq 0 ]; then - echo "::error::No XCFrameworks were produced" - exit 1 - fi + FRAMEWORKS=( + OpenSwiftUI + OpenSwiftUICore + OpenAttributeGraphShims + OpenCoreGraphicsShims + OpenObservation + OpenQuartzCoreShims + OpenRenderBoxShims + ) BODY="" echo "### XCFramework Binary Targets" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/build_xcframework.yml b/.github/workflows/build_xcframework.yml index 06b1e69f5..8b1ba45f3 100644 --- a/.github/workflows/build_xcframework.yml +++ b/.github/workflows/build_xcframework.yml @@ -5,7 +5,7 @@ on: jobs: build_xcframework: - name: Build XCFramework + name: Build XCFrameworks runs-on: macos-15 env: OPENSWIFTUI_OPENATTRIBUTESHIMS_ATTRIBUTEGRAPH: 1 @@ -13,25 +13,15 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v4 - - name: Build XCFramework + - name: Build XCFrameworks uses: ./.github/actions/build-xcframework with: signing-certificate-base64: ${{ secrets.SIGNING_CERTIFICATE_BASE_64 }} signing-certificate-password: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }} - name: Zip XCFrameworks - run: | - cd build - shopt -s nullglob - frameworks=(*.xcframework) - if [ ${#frameworks[@]} -eq 0 ]; then - echo "::error::No XCFrameworks were produced" - exit 1 - fi - for framework in "${frameworks[@]}"; do - zip -ry "$framework.zip" "$framework" - done + run: cd build && zip -ry xcframeworks.zip *.xcframework - name: Upload XCFrameworks uses: actions/upload-artifact@v7 with: - name: xcframeworks - path: build/*.xcframework.zip + path: build/xcframeworks.zip + archive: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 282590992..ea85965e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,8 +15,8 @@ jobs: tag-name: ${{ github.ref_name }} secrets: inherit - build-xcframework: - name: Build and Upload XCFramework + build-xcframeworks: + name: Build and Upload XCFrameworks needs: release-notes runs-on: macos-15 outputs: @@ -33,7 +33,7 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v4 - - name: Build XCFramework + - name: Build XCFrameworks id: build uses: ./.github/actions/build-xcframework with: @@ -45,7 +45,7 @@ jobs: update-binary-repo: name: Update OpenSwiftUI-spm - needs: build-xcframework + needs: build-xcframeworks runs-on: ubuntu-latest env: BINARY_REPO_PAT: ${{ secrets.BINARY_REPO_PAT }} @@ -53,13 +53,13 @@ jobs: - name: Update binary repo env: VERSION: ${{ github.ref_name }} - CHECKSUM_OpenSwiftUI: ${{ needs.build-xcframework.outputs.checksum_OpenSwiftUI }} - CHECKSUM_OpenSwiftUICore: ${{ needs.build-xcframework.outputs.checksum_OpenSwiftUICore }} - CHECKSUM_OpenAttributeGraphShims: ${{ needs.build-xcframework.outputs.checksum_OpenAttributeGraphShims }} - CHECKSUM_OpenCoreGraphicsShims: ${{ needs.build-xcframework.outputs.checksum_OpenCoreGraphicsShims }} - CHECKSUM_OpenObservation: ${{ needs.build-xcframework.outputs.checksum_OpenObservation }} - CHECKSUM_OpenQuartzCoreShims: ${{ needs.build-xcframework.outputs.checksum_OpenQuartzCoreShims }} - CHECKSUM_OpenRenderBoxShims: ${{ needs.build-xcframework.outputs.checksum_OpenRenderBoxShims }} + CHECKSUM_OpenSwiftUI: ${{ needs.build-xcframeworks.outputs.checksum_OpenSwiftUI }} + CHECKSUM_OpenSwiftUICore: ${{ needs.build-xcframeworks.outputs.checksum_OpenSwiftUICore }} + CHECKSUM_OpenAttributeGraphShims: ${{ needs.build-xcframeworks.outputs.checksum_OpenAttributeGraphShims }} + CHECKSUM_OpenCoreGraphicsShims: ${{ needs.build-xcframeworks.outputs.checksum_OpenCoreGraphicsShims }} + CHECKSUM_OpenObservation: ${{ needs.build-xcframeworks.outputs.checksum_OpenObservation }} + CHECKSUM_OpenQuartzCoreShims: ${{ needs.build-xcframeworks.outputs.checksum_OpenQuartzCoreShims }} + CHECKSUM_OpenRenderBoxShims: ${{ needs.build-xcframeworks.outputs.checksum_OpenRenderBoxShims }} run: | if [ -z "$BINARY_REPO_PAT" ]; then echo "::notice::BINARY_REPO_PAT not set, skipping binary repo update"