diff --git a/.github/actions/build-xcframework/action.yml b/.github/actions/build-xcframework/action.yml index 340ea75f8..e8af2ac9b 100644 --- a/.github/actions/build-xcframework/action.yml +++ b/.github/actions/build-xcframework/action.yml @@ -52,6 +52,17 @@ runs: uses: OpenSwiftUIProject/setup-xcode@v2 with: xcode-version: ${{ inputs.xcode-version }} + - 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/.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/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. diff --git a/Package.resolved b/Package.resolved index 7d2f6f386..9d24b6af9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b03bdf25c5bef5ea9cf20585206be5013c0b53257e9ebab75c9f19d480cd17ed", + "originHash" : "181200ab8951ff06d346194e4de80b86efbead80fa8e5e420013c921e7641cc6", "pins" : [ { "identity" : "darwinprivateframeworks", @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "84f52c13724f8f3725853969a1fa3831b91c7cfb" + "revision" : "abfeee71f12fdd8237726feb93655dcad26cfd8c" } }, { diff --git a/Package.swift b/Package.swift index ea04063a0..dd398068a 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,12 @@ public struct PackageContextEnvironmentProvider: EnvironmentProvider { public init() {} public func value(forKey key: String) -> String? { + #if TUIST + // FIXME: upstream issue tuist#10616 + ProcessInfo.processInfo.environment[key] + #else Context.environment[key] + #endif } } @@ -120,6 +125,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 +149,26 @@ 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 +// FIXME: upstream issue tuist#10616 +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 +201,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 @@ -623,6 +641,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])), @@ -731,8 +751,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 +762,7 @@ default: libraryType = nil } -var products: [Product] = [ +var products: [PackageDescription.Product] = [ .library(name: "OpenSwiftUI", type: libraryType, targets: ["OpenSwiftUI"]) ] if supportMultiProducts { @@ -908,3 +929,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..defcb3007 100755 --- a/Scripts/build_xcframework.sh +++ b/Scripts/build_xcframework.sh @@ -1,32 +1,36 @@ #!/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="" +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 @@ -36,9 +40,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,34 +49,178 @@ while [[ $# -gt 0 ]]; do DEBUG_MODE=true shift ;; + --skip-tuist-install) + RUN_TUIST_INSTALL=false + shift + ;; + --framework) + EXPLICIT_FRAMEWORK_NAMES=true + FRAMEWORK_NAMES+=("$2") + shift 2 + ;; *) - if [ -z "$PACKAGE_NAME" ]; then - PACKAGE_NAME="$1" - fi + FRAMEWORK_NAMES+=("$1") shift ;; esac done -# Default: macosx and iphonesimulator -# Note: iphoneos SDK support is blocked by an AG issue. See #835 +# 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 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 + +if ! command -v tuist >/dev/null 2>&1; then + echo "Error: tuist is required to generate $XCODEPROJ." + exit 1 fi -# Helper: get archs for a given SDK by index -get_sdk_archs() { - local idx="$1" - echo "${SDK_ARCHS[$idx]}" +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 + +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 @@ -83,9 +230,7 @@ for i in "${!SDKS[@]}"; do fi done echo "Debug mode: $DEBUG_MODE" - -# Dependency modules that need stub xcframeworks (referenced in public swiftinterface) -DEP_MODULES=("OpenSwiftUICore" "OpenAttributeGraphShims" "OpenCoreGraphicsShims" "OpenObservation" "OpenQuartzCoreShims" "OpenRenderBoxShims") +echo "Frameworks: ${FRAMEWORK_NAMES[*]}" sdk_destination() { case "$1" in @@ -96,222 +241,182 @@ sdk_destination() { esac } -build_framework() { - local sdk="$1" - local destination="$2" - local scheme="$3" - local archs="$4" # comma-separated or empty for default - - local XCODEBUILD_ARCHIVE_PATH="$PROJECT_BUILD_DIR/$scheme-$sdk.xcarchive" - - rm -rf "$XCODEBUILD_ARCHIVE_PATH" - - local archs_arg="" - if [ -n "$archs" ]; then - # Replace commas with spaces for ARCHS setting - archs_arg="ARCHS=${archs//,/ }" - 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 - fi - fi - - # Determine the build products path suffix - local build_products_suffix="Release-$sdk" - if [ "$sdk" = "macosx" ]; then - build_products_suffix="Release" - fi - local BUILD_PRODUCTS_PATH="$XCODEBUILD_DERIVED_DATA_PATH/Build/Intermediates.noindex/ArchiveIntermediates/$scheme/BuildProductsPath/$build_products_suffix" +framework_path() { + local archive_path="$1" + local scheme="$2" + echo "$archive_path/Products/Library/Frameworks/$scheme.framework" +} - # 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" +framework_modules_path() { + local framework="$1" + if [ -d "$framework/Versions" ]; then + echo "$framework/Versions/Current/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" + echo "$framework/Modules" fi +} - # Delete private and package swiftinterface (unless --debug) +strip_release_metadata() { + local modules_path="$1" if [ "$DEBUG_MODE" = false ]; then - rm -f "$FRAMEWORK_MODULES_PATH/$scheme.swiftmodule"/*.package.swiftinterface - rm -f "$FRAMEWORK_MODULES_PATH/$scheme.swiftmodule"/*.private.swiftinterface + find "$modules_path" -name "*.abi.json" -delete + find "$modules_path" -name "*.package.swiftinterface" -delete + find "$modules_path" -name "*.private.swiftinterface" -delete fi +} - # Capture dependency swiftmodules before next build overwrites DerivedData - 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 +first_existing_project() { + local candidate + for candidate in "$@"; do + if [ -d "$candidate" ]; then + echo "$candidate" + return fi done -} -for i in "${!SDKS[@]}"; do - build_framework "${SDKS[$i]}" "$(sdk_destination "${SDKS[$i]}")" "$PACKAGE_NAME" "${SDK_ARCHS[$i]}" -done + echo "$1" +} -echo "Builds completed successfully." +project_args_for_scheme() { + local scheme="$1" -# 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") -done -xcodebuild -create-xcframework "${create_args[@]}" -output "$PROJECT_BUILD_DIR/$PACKAGE_NAME.xcframework" - -# Copy dSYMs -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) ;; + case "$scheme" in + COpenSwiftUI|OpenSwiftUI|OpenSwiftUICore|OpenSwiftUI_SPI|OpenSwiftUISymbolDualTestsSupport) + echo "-workspace" "$XCODEWORKSPACE" + ;; + _AttributeGraphDeviceSwiftShims) + echo "-project" "$(first_existing_project "$PROJECT_ROOT/../DarwinPrivateFrameworks/DarwinPrivateFrameworks.xcodeproj" "$PROJECT_ROOT/.build/tuist-derived/DarwinPrivateFrameworks/DarwinPrivateFrameworks.xcodeproj")" + ;; + OpenAttributeGraphShims) + echo "-project" "$(first_existing_project "$PROJECT_ROOT/../OpenAttributeGraph/OpenAttributeGraph.xcodeproj" "$PROJECT_ROOT/.build/tuist-derived/OpenAttributeGraph/OpenAttributeGraph.xcodeproj")" + ;; + OpenCoreGraphics|OpenCoreGraphicsShims|OpenQuartzCore|OpenQuartzCoreShims) + echo "-project" "$(first_existing_project "$PROJECT_ROOT/../OpenCoreGraphics/OpenCoreGraphics.xcodeproj" "$PROJECT_ROOT/.build/tuist-derived/OpenCoreGraphics/OpenCoreGraphics.xcodeproj")" + ;; + OpenObservation|OpenObservationCxx) + echo "-project" "$(first_existing_project "$PROJECT_ROOT/../OpenObservation/OpenObservation.xcodeproj" "$PROJECT_ROOT/.build/tuist-derived/OpenObservation/OpenObservation.xcodeproj")" + ;; + OpenRenderBoxShims) + 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" + ;; + *) + echo "Error: No Xcode project mapping for $scheme." >&2 + exit 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/" +} + +build_framework() { + local sdk="$1" + local destination="$2" + local scheme="$3" + local archs="$4" + + local archive_path="$PROJECT_BUILD_DIR/$scheme-$sdk.xcarchive" + local project_args + read -r -a project_args <<<"$(project_args_for_scheme "$scheme")" + + rm -rf "$archive_path" + + local xcodebuild_args=( + archive + "${project_args[@]}" + -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 -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" + xcodebuild "${xcodebuild_args[@]}" - # 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 + local framework + framework="$(framework_path "$archive_path" "$scheme")" + if [ ! -d "$framework" ]; then + echo "Error: Archive did not contain $framework." + exit 1 + fi - stub_fw="$PROJECT_BUILD_DIR/dep-stubs/$sdk/$dep.framework" - sdk_path="$(xcrun --sdk $sdk --show-sdk-path)" + local modules_path + modules_path="$(framework_modules_path "$framework")" + mkdir -p "$modules_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 + if [ "$sdk" = "macosx" ]; then + rm -rf "$framework/Modules" + ln -s Versions/Current/Modules "$framework/Modules" + 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 + strip_release_metadata "$modules_path" +} - 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 +create_xcframework() { + local scheme="$1" - if [ "$sdk" = "macosx" ]; then - ln -sfn "Versions/A/$dep" "$stub_fw/$dep" - fi + rm -rf "$PROJECT_BUILD_DIR/$scheme.xcframework" - # 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 + local create_args=() + for sdk in "${SDKS[@]}"; do + create_args+=(-framework "$(framework_path "$PROJECT_BUILD_DIR/$scheme-$sdk.xcarchive" "$scheme")") done - # Create xcframework from stubs - create_args=() + xcodebuild -create-xcframework "${create_args[@]}" -output "$PROJECT_BUILD_DIR/$scheme.xcframework" +} + +copy_debug_symbols() { + local scheme="$1" + + if [ "$DEBUG_MODE" = false ]; then + return + fi + + local sdk 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") + local local_dsym_dir="" + case "$sdk" in + 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/$scheme-$sdk.xcarchive/dSYMs" ]; then + cp -R "$PROJECT_BUILD_DIR/$scheme-$sdk.xcarchive/dSYMs" "$local_dsym_dir/" fi done - xcodebuild -create-xcframework "${create_args[@]}" -output "$PROJECT_BUILD_DIR/$dep.xcframework" +} + +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 -# Clean up temp directories -rm -rf "$PROJECT_BUILD_DIR/dep-modules" "$PROJECT_BUILD_DIR/dep-stubs" +if [ "$DEBUG_MODE" = false ]; then + echo "Skipping dSYMs. Pass --debug to include them in the XCFrameworks." +else + echo "Copied dSYMs into the XCFrameworks." +fi + +echo "Created ${#FRAMEWORK_NAMES[@]} XCFrameworks in $PROJECT_BUILD_DIR." 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 * +} 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() +) 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"