diff --git a/.swiftlint.yml b/.swiftlint.yml index 6a66e72e10..a272a8a5e5 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -22,6 +22,8 @@ disabled_rules: - no_fallthrough_only # https://github.com/realm/SwiftLint/issues/2276 - redundant_string_enum_value - superfluous_disable_command +- large_tuple +- switch_case_on_newline included: - Source diff --git a/.travis.yml b/.travis.yml index a237afcd68..fbf614f758 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,8 @@ cache: directories: - .build/checkouts # SwiftPM - .build/repositories # SwiftPM + - script # With Travis’ macOS specifics in the following link, the below no longer launches any Travis runs: https://www.travis-ci.com/blog/2020-11-02-travis-ci-new-billing/ + - Logo # For our future use on other CI platforms, I’ve included `for_use_during_unit_testing_for_carthage_on_intel_host_os.xcconfig` scripting. branches: only: - master @@ -14,6 +16,8 @@ matrix: language: objective-c osx_image: xcode10.1 script: + - echo 'NON_BLANK_FOR_XCODE_VERSION_MAJOR_0100=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0200=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0300=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0400=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0500=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0600=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0700=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0800=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0900=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_1000=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_1100=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0000=YES•THIS_SPOT_LEFT_BLANK_IF_XCODE_12_OR_HIGHER=$(NON_BLANK_FOR_XCODE_VERSION_MAJOR_$(XCODE_VERSION_MAJOR))•EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__THIS_SPOT_LEFT_BLANK_IF_XCODE_12_OR_HIGHER___NATIVE_ARCH_64_BIT_x86_64=arm64 arm64e armv7 armv7s armv6 armv8•EXCLUDED_ARCHS=$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__THIS_SPOT_LEFT_BLANK_IF_XCODE_12_OR_HIGHER_$(THIS_SPOT_LEFT_BLANK_IF_XCODE_12_OR_HIGHER)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT))' | tr • '\n' | tee ~/for_use_during_unit_testing_for_carthage_on_intel_host_os.xcconfig + - export XCODE_XCCONFIG_FILE=~/for_use_during_unit_testing_for_carthage_on_intel_host_os.xcconfig - make test env: JOB=CI_TEST_10_1 - os: osx @@ -23,6 +27,8 @@ matrix: - brew uninstall carthage - HOMEBREW_NO_AUTO_UPDATE=1 brew install bats - make install + - echo 'NON_BLANK_FOR_XCODE_VERSION_MAJOR_0100=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0200=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0300=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0400=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0500=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0600=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0700=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0800=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0900=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_1000=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_1100=YES•NON_BLANK_FOR_XCODE_VERSION_MAJOR_0000=YES•THIS_SPOT_LEFT_BLANK_IF_XCODE_12_OR_HIGHER=$(NON_BLANK_FOR_XCODE_VERSION_MAJOR_$(XCODE_VERSION_MAJOR))•EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__THIS_SPOT_LEFT_BLANK_IF_XCODE_12_OR_HIGHER___NATIVE_ARCH_64_BIT_x86_64=arm64 arm64e armv7 armv7s armv6 armv8•EXCLUDED_ARCHS=$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__THIS_SPOT_LEFT_BLANK_IF_XCODE_12_OR_HIGHER_$(THIS_SPOT_LEFT_BLANK_IF_XCODE_12_OR_HIGHER)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT))' | tr • '\n' | tee ~/for_use_during_unit_testing_for_carthage_on_intel_host_os.xcconfig + - export XCODE_XCCONFIG_FILE=~/for_use_during_unit_testing_for_carthage_on_intel_host_os.xcconfig - bats IntegrationTests env: - JOB=CI_INTEGRATION_TESTS diff --git a/Documentation/Artifacts.md b/Documentation/Artifacts.md index 237bf5081c..6ae217e8d7 100644 --- a/Documentation/Artifacts.md +++ b/Documentation/Artifacts.md @@ -148,12 +148,22 @@ For dependencies that do not have source code available, a binary project specif * The version **must** be a semantic version. Git branches, tags and commits are not valid. * The location **must** be an `https` url. +#### Publish an XCFramework build alongside the framework build using an `alt=` query parameter + +To support users who build with `--use-xcframework`, create two zips: one containing the framework bundle(s) for your dependency, the other containing xcframework(s). Include "framework" or "xcframework" in the names of the zips, for example: `MyFramework.framework.zip` and `MyFramework.xcframework.zip`. In your project specification, join the two URLs into one using a query string: + + https://my.domain.com/release/1.0.0/MyFramework.framework.zip?alt=https://my.domain.com/release/1.0.0/MyFramework.xcframework.zip + +Starting in version 0.38.0, Carthage extracts any `alt=` URLs from the version specification. When `--use-xcframeworks` is passed, it prefers downloading URLs with "xcframework" in the name. + +**For backwards compatibility,** provide the plain frameworks build _first_ (i.e. not as an alt URL), so that older versions of Carthage use it. Carthage versions prior to 0.38.0 fail to download and extract XCFrameworks. + #### Example binary project specification ``` { "1.0": "https://my.domain.com/release/1.0.0/framework.zip", - "1.0.1": "https://my.domain.com/release/1.0.1/framework.zip" + "1.0.1": "https://my.domain.com/release/1.0.1/MyFramework.framework.zip?alt=https://my.domain.com/release/1.0.1/MyFramework.xcframework.zip" } ``` diff --git a/Documentation/Xcode12Workaround.md b/Documentation/Xcode12Workaround.md new file mode 100644 index 0000000000..f10ee5c62f --- /dev/null +++ b/Documentation/Xcode12Workaround.md @@ -0,0 +1,59 @@ +# Using Carthage with Xcode 12 + +As Carthage doesn't work out of the box with Xcode 12, this document will guide through a workaround that works for most cases. + +## Why Carthage compilation fails + +Well, shortly, Carthage builds fat frameworks, which means that the framework contains binaries for all supported architectures. +Until Apple Silicon was introduced it all worked just fine, but now there is a conflict as there are duplicate architectures (arm64 for devices and arm64 for simulator). +This means that Carthage cannot link architecture specific frameworks to a single fat framework. + +You can find more info in [respective issue #3019](https://github.com/Carthage/Carthage/issues/3019). + +## Workaround + +As a workaround you can invoke carthage using this script, it will remove the arm64 architecture for simulator, so the above mentioned conflict doesn't exist. + +## How to make it work + +1. place this script somewhere to your `PATH` (I personally have it in `/usr/local/bin/carthage.sh`) +2. make it the script executable, so open your _Terminal_ and run + ```bash + chmod +x /usr/local/bin/carthage.sh + ``` +3. from now on instead of running e.g. + ``` + carthage bootstrap --platform iOS --cache-builds + ``` + you need to run our script + ``` + carthage.sh bootstrap --platform iOS --cache-builds + ``` + +### Workaround script + +This script has a known limitation - it will remove arm64 simulator architecture from compiled framework, so frameworks compiled using it cannot be used on Macs running Apple Silicon. + +```bash +# carthage.sh +# Usage example: ./carthage.sh build --platform iOS + +set -euo pipefail + +xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX) +trap 'rm -f "$xcconfig"' INT TERM HUP EXIT + +# For Xcode 12 make sure EXCLUDED_ARCHS is set to arm architectures otherwise +# the build will fail on lipo due to duplicate architectures. + +CURRENT_XCODE_VERSION="$(xcodebuild -version | grep "Xcode" | cut -d' ' -f2 | cut -d'.' -f1)00" +CURRENT_XCODE_BUILD=$(xcodebuild -version | grep "Build version" | cut -d' ' -f3) + +echo "EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_${CURRENT_XCODE_VERSION}__BUILD_${CURRENT_XCODE_BUILD} = arm64 arm64e armv7 armv7s armv6 armv8" >> $xcconfig + +echo 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_'${CURRENT_XCODE_VERSION}' = $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_$(XCODE_VERSION_MAJOR)__BUILD_$(XCODE_PRODUCT_BUILD_VERSION))' >> $xcconfig +echo 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >> $xcconfig + +export XCODE_XCCONFIG_FILE="$xcconfig" +carthage "$@" +``` diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 703efcfddc..5affed8a35 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -7,6 +7,7 @@ * Are you using `--use-submodules`? * Are you using `--cache-builds`? * Are you using `--new-resolver`? +* Are you using `--use-xcframeworks`? **Cartfile** ``` diff --git a/IntegrationTests/copy-frameworks.bats b/IntegrationTests/copy-frameworks.bats index 612c040bbf..66c3e826d3 100755 --- a/IntegrationTests/copy-frameworks.bats +++ b/IntegrationTests/copy-frameworks.bats @@ -15,7 +15,7 @@ teardown() { run carthage update --platform ios [ "$status" -eq 0 ] - run xcodebuild clean build-for-testing -scheme CarthageCopyFrameworksFixture -sdk iphonesimulator -destination "name=iPhone 6s" + run xcodebuild clean build-for-testing -scheme CarthageCopyFrameworksFixture -sdk iphonesimulator -destination "name=iPhone 8" [ "$status" -eq 0 ] ARCHIVE_APP_DIR=CarthageCopyFrameworksFixture.xcarchive/Products/Applications diff --git a/IntegrationTests/relink-conflicting-not-checked-in-symlink.bats b/IntegrationTests/relink-conflicting-not-checked-in-symlink.bats index 9a173f8cde..190681fb35 100755 --- a/IntegrationTests/relink-conflicting-not-checked-in-symlink.bats +++ b/IntegrationTests/relink-conflicting-not-checked-in-symlink.bats @@ -72,3 +72,24 @@ carthage-and-check-project-symlink() { carthage-and-check-project-symlink update --no-build --no-use-binaries } + +@test "with conflicting not-checked-in symlink in «Carthage/Checkouts» of dependency pathologically named «...git», carthage «bootstrap, update, update» should — sanitizing throughout — unlink, then write symlink there" { + + mv "${extracted_directory:?}/SourceRepos/TestFramework1" "${extracted_directory}/SourceRepos/...git" + + rm Cartfile + + cat > Cartfile <<-EOF + git "file://${extracted_directory}/SourceRepos/...git" "relink-conflicting-not-checked-in-syminks" + EOF + + carthage bootstrap --no-build --no-use-binaries + check-symlink "$(project_directory)/Carthage/Checkouts/../Carthage/Checkouts/TestFramework2" + # carthage should have sanitized the former «TestFramework1» from «...git» to non–path-traversing «..»… + + carthage update --no-build --no-use-binaries + check-symlink "$(project_directory)/Carthage/Checkouts/../Carthage/Checkouts/TestFramework2" + + carthage update --no-build --no-use-binaries + check-symlink "$(project_directory)/Carthage/Checkouts/../Carthage/Checkouts/TestFramework2" +} diff --git a/Makefile b/Makefile index 7657517f96..6f32d766c9 100644 --- a/Makefile +++ b/Makefile @@ -1,38 +1,50 @@ #!/usr/bin/xcrun make -f CARTHAGE_TEMPORARY_FOLDER?=/tmp/Carthage.dst +export CARTHAGE_TEMPORARY_FOLDER PREFIX?=/usr/local +export PREFIX INTERNAL_PACKAGE=CarthageApp.pkg OUTPUT_PACKAGE=Carthage.pkg CARTHAGE_EXECUTABLE=./.build/release/carthage -BINARIES_FOLDER=/usr/local/bin +BINARIES_FOLDER=$(PREFIX)/bin +export BINARIES_FOLDER SWIFT_BUILD_FLAGS=--configuration release -Xswiftc -suppress-warnings +SWIFT_TEST_FLAGS=--skip-update SWIFTPM_DISABLE_SANDBOX_SHOULD_BE_FLAGGED:=$(shell test -n "$${HOMEBREW_SDKROOT}" && echo should_be_flagged) ifeq ($(SWIFTPM_DISABLE_SANDBOX_SHOULD_BE_FLAGGED), should_be_flagged) SWIFT_BUILD_FLAGS+= --disable-sandbox endif -SWIFT_STATIC_STDLIB_SHOULD_BE_FLAGGED:=$(shell test -d $$(dirname $$(xcrun --find swift))/../lib/swift_static/macosx && echo should_be_flagged) +SWIFT_BUILD_SHOULD_BE_FLAGGED_VERY_VERBOSE:=$(shell (/usr/bin/xcrun --find swift-package | /bin/zsh --no-globalrcs --no-rcs -c '/usr/bin/strings "$$(cat)"' | grep --quiet -e '^veryVerbose') && echo should_be_flagged) +ifeq ($(SWIFT_BUILD_SHOULD_BE_FLAGGED_VERY_VERBOSE), should_be_flagged) +SWIFT_BUILD_FLAGS+= --very-verbose +SWIFT_TEST_FLAGS+= --very-verbose +endif +SWIFT_STATIC_STDLIB_SHOULD_BE_FLAGGED:=$(shell test -d $$(dirname $$(xcrun --find swift))/../lib/swift_static/macosx && (./script/strings_of_xcrun_find_ld.zsh | grep --quiet -e '^only one snapshot supported') && echo should_be_flagged) ifeq ($(SWIFT_STATIC_STDLIB_SHOULD_BE_FLAGGED), should_be_flagged) SWIFT_BUILD_FLAGS+= -Xswiftc -static-stdlib endif # ZSH_COMMAND · run single command in `zsh` shell, ignoring most `zsh` startup files. -ZSH_COMMAND := ZDOTDIR='/var/empty' zsh -o NO_GLOBAL_RCS -c +ZSH_COMMAND = ZDOTDIR='/var/empty' zsh --no-globalrcs --no-rcs -c # RM_SAFELY · `rm -rf` ensuring first and only parameter is non-null, contains more than whitespace, non-root if resolving absolutely. -RM_SAFELY := $(ZSH_COMMAND) '[[ ! $${1:?} =~ "^[[:space:]]+\$$" ]] && [[ $${1:A} != "/" ]] && [[ $${\#} == "1" ]] && noglob rm -rf $${1:A}' -- +RM_SAFELY = $(ZSH_COMMAND) '[[ ! $${1:?} =~ "^[[:space:]]+\$$" ]] && [[ $${1:A} != "/" ]] && [[ $${\#} == "1" ]] && noglob rm -rf $${1:A}' -- VERSION_STRING=$(shell git describe --abbrev=0 --tags) DISTRIBUTION_PLIST=Source/carthage/Distribution.plist RM=rm -f -MKDIR=mkdir -p SUDO=sudo CP=cp +ifdef DISABLE_SUDO +override SUDO= +endif + .PHONY: all clean install package test uninstall xcconfig xcodeproj all: installables @@ -46,19 +58,20 @@ test: $(CP) -R Tests/CarthageKitTests/Resources ./.build/debug/CarthagePackageTests.xctest/Contents $(CP) Tests/CarthageKitTests/fixtures/CartfilePrivateOnly.zip ./.build/debug/CarthagePackageTests.xctest/Contents/Resources script/copy-fixtures ./.build/debug/CarthagePackageTests.xctest/Contents/Resources - swift test --skip-build + swift test --skip-build $(SWIFT_TEST_FLAGS) installables: swift build $(SWIFT_BUILD_FLAGS) + /bin/zsh --no-globalrcs --no-rcs -c 'set -x; print -r "$${CARTHAGE_TEMPORARY_FOLDER:?}$${BINARIES_FOLDER:?}"' package: installables - $(MKDIR) "$(CARTHAGE_TEMPORARY_FOLDER)$(BINARIES_FOLDER)" - $(CP) "$(CARTHAGE_EXECUTABLE)" "$(CARTHAGE_TEMPORARY_FOLDER)$(BINARIES_FOLDER)" + /bin/zsh --no-globalrcs --no-rcs -c 'set -x; mkdir -p "$${CARTHAGE_TEMPORARY_FOLDER:?}$${BINARIES_FOLDER:?}"' + $(CP) -v "$(CARTHAGE_EXECUTABLE)" "$${CARTHAGE_TEMPORARY_FOLDER:?}$${BINARIES_FOLDER:?}" pkgbuild \ --identifier "org.carthage.carthage" \ --install-location "/" \ - --root "$(CARTHAGE_TEMPORARY_FOLDER)" \ + --root "$${CARTHAGE_TEMPORARY_FOLDER:?}" \ --version "$(VERSION_STRING)" \ "$(INTERNAL_PACKAGE)" @@ -68,14 +81,15 @@ package: installables "$(OUTPUT_PACKAGE)" prefix_install: installables - $(MKDIR) "$(PREFIX)/bin" - $(CP) -f "$(CARTHAGE_EXECUTABLE)" "$(PREFIX)/bin/" + /bin/zsh --no-globalrcs --no-rcs -c 'set -x; mkdir -p "$${BINARIES_FOLDER:?}"' + $(CP) -v -f "$(CARTHAGE_EXECUTABLE)" "$${BINARIES_FOLDER:?}" install: installables - $(SUDO) $(CP) -f "$(CARTHAGE_EXECUTABLE)" "$(BINARIES_FOLDER)" + if [ ! -d "$${BINARIES_FOLDER:?}" ]; then $(SUDO) mkdir -p "$${BINARIES_FOLDER:?}"; fi + $(SUDO) $(CP) -v -f "$(CARTHAGE_EXECUTABLE)" "$${BINARIES_FOLDER:?}" uninstall: - $(RM) "$(BINARIES_FOLDER)/carthage" + /bin/zsh --no-globalrcs --no-rcs -c 'set -x; rm -f -v "$${BINARIES_FOLDER:?}/carthage"' xcodeproj: swift package generate-xcodeproj diff --git a/Package.resolved b/Package.resolved index 2e82fa2143..c424494556 100644 --- a/Package.resolved +++ b/Package.resolved @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/Quick/Nimble.git", "state": { "branch": null, - "revision": "f8657642dfdec9973efc79cc68bcef43a653a2bc", - "version": "8.0.2" + "revision": "7a46a5fc86cb917f69e3daf79fcb045283d8f008", + "version": "8.1.2" } }, { @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/Quick/Quick.git", "state": { "branch": null, - "revision": "94df9b449508344667e5afc7e80f8bcbff1e4c37", - "version": "2.1.0" + "revision": "09b3becb37cb2163919a3842a4c5fa6ec7130792", + "version": "2.2.1" } }, { diff --git a/Package.swift b/Package.swift index 9044d48365..bcec7b0c70 100644 --- a/Package.swift +++ b/Package.swift @@ -16,8 +16,8 @@ let package = Package( .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "5.0.0"), .package(url: "https://github.com/mdiep/Tentacle.git", from: "0.13.1"), .package(url: "https://github.com/thoughtbot/Curry.git", from: "4.0.2"), - .package(url: "https://github.com/Quick/Quick.git", from: "2.1.0"), - .package(url: "https://github.com/Quick/Nimble.git", from: "8.0.1"), + .package(url: "https://github.com/Quick/Quick.git", from: "2.2.1"), + .package(url: "https://github.com/Quick/Nimble.git", from: "8.0.9"), ], targets: [ .target( diff --git a/README.md b/README.md index 02e6f4e3bd..d7d461c5a9 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,12 @@ Carthage builds your dependencies and provides you with binary frameworks, but y - [Installing Carthage](#installing-carthage) - [Adding frameworks to an application](#adding-frameworks-to-an-application) - [Getting started](#getting-started) - - [If you're building for macOS](#if-youre-building-for-macos) - - [If you're building for iOS, tvOS, or watchOS](#if-youre-building-for-ios-tvos-or-watchos) - - [For both platforms](#for-both-platforms) + - [Building platform-independent XCFrameworks](#building-platform-independent-xcframeworks-xcode-12-and-above) + - [Migrating a project from framework bundles to XCFrameworks](#migrating-a-project-from-framework-bundles-to-xcframeworks) + - [Building platform-specific framework bundles](#building-platform-specific-framework-bundles-default-for-xcode-11-and-below) + - [If you're building for macOS](#if-youre-building-for-macos) + - [If you're building for iOS, tvOS, or watchOS](#if-youre-building-for-ios-tvos-or-watchos) + - [For all platforms](#for-all-platforms) - [(Optionally) Add build phase to warn about outdated dependencies](#optionally-add-build-phase-to-warn-about-outdated-dependencies) - [Swift binary framework download compatibility](#swift-binary-framework-download-compatibility) - [Running a project that uses Carthage](#running-a-project-that-uses-carthage) @@ -28,7 +31,7 @@ Carthage builds your dependencies and provides you with binary frameworks, but y - [Share your Xcode schemes](#share-your-xcode-schemes) - [Resolve build failures](#resolve-build-failures) - [Tag stable releases](#tag-stable-releases) - - [Archive prebuilt frameworks into one zip file](#archive-prebuilt-frameworks-into-one-zip-file) + - [Archive prebuilt frameworks into zip files](#archive-prebuilt-frameworks-into-zip-files) - [Use travis-ci to upload your tagged prebuilt frameworks](#use-travis-ci-to-upload-your-tagged-prebuilt-frameworks) - [Build static frameworks to speed up your app’s launch times](#build-static-frameworks-to-speed-up-your-apps-launch-times) - [Declare your compatibility](#declare-your-compatibility) @@ -45,31 +48,13 @@ Carthage builds your dependencies and provides you with binary frameworks, but y 1. List the desired dependencies in the [Cartfile][], for example: ``` - github "Alamofire/Alamofire" ~> 4.7.2 + github "Alamofire/Alamofire" ~> 5.5 ``` - -1. Run `carthage update` -1. A `Cartfile.resolved` file and a `Carthage` directory will appear in the same directory where your `.xcodeproj` or `.xcworkspace` is -1. Drag the built `.framework` binaries from `Carthage/Build/` into your application’s Xcode project. -1. If you are using Carthage for an application, follow the remaining steps, otherwise stop here. -1. On your application targets’ _Build Phases_ settings tab, click the _+_ icon and choose _New Run Script Phase_. Create a Run Script in which you specify your shell (ex: `/bin/sh`), add the following contents to the script area below the shell: - - ```sh - /usr/local/bin/carthage copy-frameworks - ``` -- Add the paths to the frameworks you want to use under “Input Files". For example: - - ``` - $(SRCROOT)/Carthage/Build/iOS/Alamofire.framework - ``` - -- Add the paths to the copied frameworks to the “Output Files”. For example: - - ``` - $(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Alamofire.framework - ``` -Another approach when having multiple dependencies is to use `.xcfilelist`s. This is covered in [If you're building for iOS, tvOS or watchOS](#if-youre-building-for-ios-tvos-or-watchos) +1. Run `carthage update --use-xcframeworks` +1. A `Cartfile.resolved` file and a `Carthage` directory will appear in the same directory where your `.xcodeproj` or `.xcworkspace` is +1. Drag the built `.xcframework` bundles from `Carthage/Build` into the "Frameworks and Libraries" section of your application’s Xcode project. +1. If you are using Carthage for an application, select "Embed & Sign", otherwise "Do Not Embed". For an in depth guide, read on from [Adding frameworks to an application](#adding-frameworks-to-an-application) @@ -79,11 +64,11 @@ There are multiple options for installing Carthage: * **Installer:** Download and run the `Carthage.pkg` file for the latest [release](https://github.com/Carthage/Carthage/releases), then follow the on-screen instructions. If you are installing the pkg via CLI, you might need to run `sudo chown -R $(whoami) /usr/local` first. -* **Homebrew:** You can use [Homebrew](http://brew.sh) and install the `carthage` tool on your system simply by running `brew update` and `brew install carthage`. (note: if you previously installed the binary version of Carthage, you should delete `/Library/Frameworks/CarthageKit.framework`). +* **Homebrew:** You can use [Homebrew](https://brew.sh) and install the `carthage` tool on your system simply by running `brew update` and `brew install carthage`. (note: if you previously installed the binary version of Carthage, you should delete `/Library/Frameworks/CarthageKit.framework`). * **MacPorts:** You can use [MacPorts](https://www.macports.org/) and install the `carthage` tool on your system simply by running `sudo port selfupdate` and `sudo port install carthage`. (note: if you previously installed the binary version of Carthage, you should delete `/Library/Frameworks/CarthageKit.framework`). -* **From source:** If you’d like to run the latest development version (which may be highly unstable or incompatible), simply clone the `master` branch of the repository, then run `make install`. Requires Xcode 9.4 (Swift 4.1). +* **From source:** If you’d like to run the latest development version (which may be highly unstable or incompatible), simply clone the `master` branch of the repository, then run `make install`. Requires Xcode 10.0 (Swift 4.2). ## Adding frameworks to an application @@ -91,8 +76,40 @@ Once you have Carthage [installed](#installing-carthage), you can begin adding f ### Getting started +#### Building platform-independent XCFrameworks (Xcode 12 and above) + +1. Create a [Cartfile][] that lists the frameworks you’d like to use in your project. +1. Run `carthage update --use-xcframeworks`. This will fetch dependencies into a [Carthage/Checkouts][] folder and build each one or download a pre-compiled XCFramework. +1. On your application targets’ _General_ settings tab, in the _Frameworks, Libraries, and Embedded Content_ section, drag and drop each XCFramework you want to use from the [Carthage/Build][] folder on disk. + +##### Migrating a project from framework bundles to XCFrameworks + +We encourage using XCFrameworks as of version 0.37.0 (January 2021), and require XCFrameworks when building on an Apple Silicon Mac. Switching from discrete framework bundles to XCFrameworks requires a few changes to your project: + +
+ Migration steps + +1. Delete your `Carthage/Build` folder to remove any existing framework bundles. +1. Build new XCFrameworks by running `carthage build --use-xcframeworks`. Any other arguments you build with can be provided like normal. +1. Remove references to the old frameworks in each of your targets: + - Delete references to Carthage frameworks from the target's _Frameworks, Libraries, and Embedded Content_ section and/or its _Link Binary with Libraries_ build phase. + - Delete references to Carthage frameworks from any _Copy Files_ build phases. + - Delete the target's `carthage copy-frameworks` build phase, if present. +1. Add references to XCFrameworks in each of your targets: + - For an application target: In the _General_ settings tab, in the _Frameworks, Libraries, and Embedded Content_ section, drag and drop each XCFramework you use from the [Carthage/Build][] folder on disk. + - For a framework target: In the _Build Phases_ tab, in a _Link Binary with Libraries_ phase, drag and drop each XCFramework you use from the [Carthage/Build][] folder on disk. + +
+ +#### Building platform-specific framework bundles (default for Xcode 11 and below) + +**Xcode 12+ incompatibility**: Multi-architecture platforms are not supported when building framework bundles in Xcode 12 and above. Prefer [building with XCFrameworks](#building-platform-independent-xcframeworks-xcode-12-and-above). If you need to build discrete framework bundles, [use a workaround xcconfig file](Documentation/Xcode12Workaround.md). + ##### If you're building for macOS +
+ macOS-specific instructions + 1. Create a [Cartfile][] that lists the frameworks you’d like to use in your project. 1. Run `carthage update --platform macOS`. This will fetch dependencies into a [Carthage/Checkouts][] folder and build each one or download a pre-compiled framework. 1. On your application targets’ _General_ settings tab, in the _Embedded Binaries_ section, drag and drop each framework you want to use from the [Carthage/Build][] folder on disk. @@ -103,11 +120,16 @@ Additionally, you'll need to copy debug symbols for debugging and crash reportin 1. Click the _Destination_ drop-down menu and select _Products Directory_. 1. For each framework you’re using, drag and drop its corresponding dSYM file. +
+ ##### If you're building for iOS, tvOS, or watchOS +
+ Platform-specific instructions + 1. Create a [Cartfile][] that lists the frameworks you’d like to use in your project. 1. Run `carthage update`. This will fetch dependencies into a [Carthage/Checkouts][] folder, then build each one or download a pre-compiled framework. -1. On your application targets’ _General_ settings tab, in the “Linked Frameworks and Libraries” section, drag and drop each framework you want to use from the [Carthage/Build][] folder on disk. +1. Open your application targets’ _General_ settings tab. For Xcode 11.0 and higher, in the "Frameworks, Libraries, and Embedded Content" section, drag and drop each framework you want to use from the [Carthage/Build][] folder on disk. Then, in the "Embed" section, select "Do Not Embed" from the pulldown menu for each item added. For Xcode 10.x and lower, in the "Linked Frameworks and Libraries" section, drag and drop each framework you want to use from the [Carthage/Build][] folder on disk. 1. On your application targets’ _Build Phases_ settings tab, click the _+_ icon and choose _New Run Script Phase_. Create a Run Script in which you specify your shell (ex: `/bin/sh`), add the following contents to the script area below the shell: ```sh @@ -141,12 +163,13 @@ This script works around an [App Store submission bug](http://www.openradar.me/r With the debug information copied into the built products directory, Xcode will be able to symbolicate the stack trace whenever you stop at a breakpoint. This will also enable you to step through third-party code in the debugger. When archiving your application for submission to the App Store or TestFlight, Xcode will also copy these files into the dSYMs subdirectory of your application’s `.xcarchive` bundle. +
-##### For both platforms +#### For all platforms Along the way, Carthage will have created some [build artifacts][Artifacts]. The most important of these is the [Cartfile.resolved][] file, which lists the versions that were actually built for each framework. **Make sure to commit your [Cartfile.resolved][]**, because anyone else using the project will need that file to build the same framework versions. -##### (Optionally) Add build phase to warn about outdated dependencies +#### (Optionally) Add build phase to warn about outdated dependencies You can add a Run Script phase to automatically warn you when one of your dependencies is out of date. @@ -156,7 +179,7 @@ You can add a Run Script phase to automatically warn you when one of your depend /usr/local/bin/carthage outdated --xcode-warnings 2>/dev/null ``` -##### Swift binary framework download compatibility +#### Swift binary framework download compatibility Carthage will check to make sure that downloaded Swift (and mixed Objective-C/Swift) frameworks were built with the same version of Swift that is in use locally. If there is a version mismatch, Carthage will proceed to build the framework from source. If the framework cannot be built from source, Carthage will fail. @@ -268,13 +291,15 @@ Carthage determines which versions of your framework are available by searching Tags without any version number, or with any characters following the version number (e.g., `1.2-alpha-1`) are currently unsupported, and will be ignored. -### Archive prebuilt frameworks into one zip file +### Archive prebuilt frameworks into zip files Carthage can automatically use prebuilt frameworks, instead of building from scratch, if they are attached to a [GitHub Release](https://help.github.com/articles/about-releases/) on your project’s repository or via a binary project definition file. -To offer prebuilt frameworks for a specific tag, the binaries for _all_ supported platforms should be zipped up together into _one_ archive, and that archive should be attached to a published Release corresponding to that tag. The attachment should include `.framework` in its name (e.g., `ReactiveCocoa.framework.zip`), to indicate to Carthage that it contains binaries. The directory structure of the acthive is free form but, __frameworks should only appear once in the archive__ as they will be copied +To offer prebuilt frameworks for a specific tag, the binaries for _all_ supported platforms should be zipped up together into _one_ archive, and that archive should be attached to a published Release corresponding to that tag. The attachment should include `.framework` in its name (e.g., `ReactiveCocoa.framework.zip`), to indicate to Carthage that it contains binaries. The directory structure of the archive is free form but, __frameworks should only appear once in the archive__ as they will be copied to `Carthage/Build/` based on their name (e.g. `ReactiveCocoa.framework`). +To offer prebuilt XCFrameworks, build with `--use-xcframeworks` and follow the same process to zip up all XCFrameworks into one archive. Include `.xcframework` in the attachment name. Starting in version 0.38.0, Carthage prefers downloading `.xcframework` attachments when `--use-xcframeworks` is passed. + You can perform the archiving operation with carthage itself using: ```sh @@ -407,7 +432,7 @@ If you’re interested in using Carthage as part of another tool, or perhaps ext ## Differences between Carthage and CocoaPods -[CocoaPods](http://cocoapods.org/) is a long-standing dependency manager for Cocoa. So why was Carthage created? +[CocoaPods](https://cocoapods.org/) is a long-standing dependency manager for Cocoa. So why was Carthage created? Firstly, CocoaPods (by default) automatically creates and updates an Xcode workspace for your application and all dependencies. Carthage builds framework binaries using `xcodebuild`, but leaves the responsibility of integrating them up to the user. CocoaPods’ approach is easier to use, while Carthage’s is flexible and unintrusive. @@ -417,7 +442,7 @@ The goal of CocoaPods is listed in its [README](https://github.com/CocoaPods/Coc By contrast, Carthage has been created as a _decentralized_ dependency manager. There is no central list of projects, which reduces maintenance work and avoids any central point of failure. However, project discovery is more difficult—users must resort to GitHub’s [Trending](https://github.com/trending?l=swift) pages or similar. -CocoaPods projects must also have what’s known as a [podspec](http://guides.cocoapods.org/syntax/podspec.html) file, which includes metadata about the project and specifies how it should be built. Carthage uses `xcodebuild` to build dependencies, instead of integrating them into a single workspace, it doesn’t have a similar specification file but your dependencies must include their own Xcode project that describes how to build their products. +CocoaPods projects must also have what’s known as a [podspec](https://guides.cocoapods.org/syntax/podspec.html) file, which includes metadata about the project and specifies how it should be built. Carthage uses `xcodebuild` to build dependencies, instead of integrating them into a single workspace, it doesn’t have a similar specification file but your dependencies must include their own Xcode project that describes how to build their products. Ultimately, we created Carthage because we wanted the simplest tool possible—a dependency manager that gets the job done without taking over the responsibility of Xcode, and without creating extra work for framework authors. CocoaPods offers many amazing features that Carthage will never have, at the expense of additional complexity. @@ -436,4 +461,3 @@ Header backdrop photo is released under the [CC BY-NC-SA 2.0](https://creativeco [CarthageKit]: Source/CarthageKit [VersionFile]: Documentation/VersionFile.md [StaticFrameworks]: Documentation/StaticFrameworks.md - diff --git a/Source/CarthageKit/BinaryProject.swift b/Source/CarthageKit/BinaryProject.swift index 6cce6a3b79..d0f35092a1 100644 --- a/Source/CarthageKit/BinaryProject.swift +++ b/Source/CarthageKit/BinaryProject.swift @@ -5,13 +5,13 @@ import Result public struct BinaryProject: Equatable { private static let jsonDecoder = JSONDecoder() - public var versions: [PinnedVersion: URL] + public var versions: [PinnedVersion: [URL]] public static func from(jsonData: Data) -> Result { return Result<[String: String], AnyError>(attempt: { try jsonDecoder.decode([String: String].self, from: jsonData) }) .mapError { .invalidJSON($0.error) } .flatMap { json -> Result in - var versions = [PinnedVersion: URL]() + var versions = [PinnedVersion: [URL]]() for (key, value) in json { let pinnedVersion: PinnedVersion @@ -21,15 +21,47 @@ public struct BinaryProject: Equatable { case let .failure(error): return .failure(BinaryJSONError.invalidVersion(error)) } + + guard var components = URLComponents(string: value) else { + return .failure(BinaryJSONError.invalidURL(value)) + } + + struct ExtractedURLs { + var remainingQueryItems: [URLQueryItem]? = nil + var urlStrings: [String] = [] + } + let extractedURLs = components.queryItems?.reduce(into: ExtractedURLs()) { state, item in + if item.name == "alt", let value = item.value { + state.urlStrings.append(value) + } else if state.remainingQueryItems == nil { + state.remainingQueryItems = [item] + } else { + state.remainingQueryItems!.append(item) + } + } + components.queryItems = extractedURLs?.remainingQueryItems - guard let binaryURL = URL(string: value) else { + guard let firstURL = components.url else { return .failure(BinaryJSONError.invalidURL(value)) } - guard binaryURL.scheme == "file" || binaryURL.scheme == "https" else { - return .failure(BinaryJSONError.nonHTTPSURL(binaryURL)) + guard firstURL.scheme == "file" || firstURL.scheme == "https" else { + return .failure(BinaryJSONError.nonHTTPSURL(firstURL)) } + var binaryURLs: [URL] = [firstURL] - versions[pinnedVersion] = binaryURL + if let extractedURLs = extractedURLs { + for string in extractedURLs.urlStrings { + guard let binaryURL = URL(string: string) else { + return .failure(BinaryJSONError.invalidURL(string)) + } + guard binaryURL.scheme == "file" || binaryURL.scheme == "https" else { + return .failure(BinaryJSONError.nonHTTPSURL(binaryURL)) + } + binaryURLs.append(binaryURL) + } + } + + versions[pinnedVersion] = binaryURLs } return .success(BinaryProject(versions: versions)) diff --git a/Source/CarthageKit/BuildOptions.swift b/Source/CarthageKit/BuildOptions.swift index 2a21ef0395..133dbbd70e 100644 --- a/Source/CarthageKit/BuildOptions.swift +++ b/Source/CarthageKit/BuildOptions.swift @@ -1,11 +1,13 @@ import XCDBLD +import Result +import ReactiveSwift /// The build options used for building `xcodebuild` command. public struct BuildOptions { /// The Xcode configuration to build. public var configuration: String /// The platforms to build for. - public var platforms: Set + public var platforms: Set? /// The toolchain to build with. public var toolchain: String? /// The path to the custom derived data folder. @@ -14,14 +16,17 @@ public struct BuildOptions { public var cacheBuilds: Bool /// Whether to use downloaded binaries if possible. public var useBinaries: Bool + /// Whether to create an XCFramework instead of lipoing built products. + public var useXCFrameworks: Bool public init( configuration: String, - platforms: Set = [], + platforms: Set? = nil, toolchain: String? = nil, derivedDataPath: String? = nil, cacheBuilds: Bool = true, - useBinaries: Bool = true + useBinaries: Bool = true, + useXCFrameworks: Bool = false ) { self.configuration = configuration self.platforms = platforms @@ -29,5 +34,6 @@ public struct BuildOptions { self.derivedDataPath = derivedDataPath self.cacheBuilds = cacheBuilds self.useBinaries = useBinaries + self.useXCFrameworks = useXCFrameworks } } diff --git a/Source/CarthageKit/BuildSettings.swift b/Source/CarthageKit/BuildSettings.swift index d84d173f30..308c9c1fe6 100644 --- a/Source/CarthageKit/BuildSettings.swift +++ b/Source/CarthageKit/BuildSettings.swift @@ -1,5 +1,6 @@ import Foundation import Result +import ReactiveTask import ReactiveSwift import XCDBLD @@ -38,12 +39,14 @@ public struct BuildSettings { options: [ .caseInsensitive, .anchorsMatchLines ] ) + private static let xcodeVersion: XcodeVersion? = XcodeVersion.make() + /// Invokes `xcodebuild` to retrieve build settings for the given build /// arguments. /// /// Upon .success, sends one BuildSettings value for each target included in /// the referenced scheme. - public static func load(with arguments: BuildArguments, for action: BuildArguments.Action? = nil) -> SignalProducer { + public static func load(with arguments: BuildArguments, for action: BuildArguments.Action? = nil, with environment: [String: String]? = nil) -> SignalProducer { // xcodebuild (in Xcode 8.0) has a bug where xcodebuild -showBuildSettings // can hang indefinitely on projects that contain core data models. // rdar://27052195 @@ -52,11 +55,25 @@ public struct BuildSettings { // // "archive" also works around the issue above so use it to determine if // it is configured for the archive action. - let task = xcodebuildTask(["archive", "-showBuildSettings", "-skipUnavailableActions"], arguments) + let xcodeIsBelowVersion16 = (BuildSettings.xcodeVersion?.majorVersionNumber ?? 0) < 16 + let xcodebuildAction: BuildArguments.Action = (xcodeIsBelowVersion16 || arguments.sdk?.isDevice ?? false) ? .archive : .build + + let task = xcodebuildTask([xcodebuildAction.rawValue, "-showBuildSettings", "-skipUnavailableActions"], arguments, environment: environment) return task.launch() .ignoreTaskData() - .mapError(CarthageError.taskError) + .flatMapError { error in + switch error { + case let .shellTaskFailed(_, _, standardError): + let pattern = "error[:] [^\n]*Found no destinations for the scheme [^\n]+ and action [^\n]+[.]\n" + guard Foundation.NSNotFound == NSRegularExpression.rangeOfFirstMatch(pattern: pattern, within: standardError).location else { + return SignalProducer.empty + } + default: break + } + + return SignalProducer(error: CarthageError.taskError(error)) + } // xcodebuild has a bug where xcodebuild -showBuildSettings // can sometimes hang indefinitely on projects that don't // share any schemes, so automatically bail out if it looks @@ -122,10 +139,30 @@ public struct BuildSettings { /// sent on the returned signal. public static func SDKsForScheme(_ scheme: Scheme, inProject project: ProjectLocator) -> SignalProducer { return load(with: BuildArguments(project: project, scheme: scheme)) + .zip(with: SDK.setsFromJSONShowSDKsWithFallbacks.promoteError(CarthageError.self)) .take(first: 1) - .flatMap(.merge) { $0.buildSDKs } + .map { $1.intersection($0.buildSDKRawNames.map { sdk in SDK(name: sdk, simulatorHeuristic: "") }) } + .flatten() } + /// We `deny` — not in the sense of taking effect — but informing via `Bool`. + public static func denySDKUnderXcodesSub14IfWatchOSOrTVOSAndProjectsDoesNotSpecifySatisfactoryBitcodeSetting( + _ sdk: SDK, + under arguments: BuildArguments, + for action: BuildArguments.Action? = nil, + with environment: [String: String]? = nil + ) -> SignalProducer<(SDK, Bool), CarthageError> { + if BuildSettings.xcodeVersion?.majorVersionNumber ?? 0 >= 14 { return .init(value: (sdk, true)) } + + if !["appletvos", "watchos"].contains(sdk.rawValue) { return .init(value: (sdk, true)) } + + return BuildSettings + .load(with: arguments, for: action, with: environment) + .map { settings in + (sdk, settings.bitcodeEnabled.value == true) + } + } + /// Returns the value for the given build setting, or an error if it could /// not be determined. public subscript(key: String) -> Result { @@ -137,18 +174,22 @@ public struct BuildSettings { } /// Attempts to determine the SDKs this scheme builds for. - public var buildSDKs: SignalProducer { + public var buildSDKRawNames: Set { let supportedPlatforms = self["SUPPORTED_PLATFORMS"] if let supportedPlatforms = supportedPlatforms.value { - let platforms = supportedPlatforms.split { $0 == " " }.map(String.init) - return SignalProducer(platforms) - .map { platform in SignalProducer(result: SDK.from(string: platform)) } - .flatten(.merge) + return Set( + supportedPlatforms.split(separator: " ").map(String.init) + ) + } else if let platformName = self["PLATFORM_NAME"].value { + return [platformName] as Set + } else { + return [] as Set } + } - let firstBuildSDK = self["PLATFORM_NAME"].flatMap(SDK.from(string:)) - return SignalProducer(result: firstBuildSDK) + public var archs: Result, CarthageError> { + return self["ARCHS"].map { Set($0.components(separatedBy: " ")) } } /// Attempts to determine the ProductType specified in these build settings. @@ -166,6 +207,12 @@ public struct BuildSettings { return productType.fanout(machOType).map(FrameworkType.init) } + internal var frameworkSearchPaths: Result<[URL], CarthageError> { + return self["FRAMEWORK_SEARCH_PATHS"].map { paths in + paths.split(separator: " ").map { URL(fileURLWithPath: String($0), isDirectory: true) } + } + } + /// Attempts to determine the URL to the built products directory. public var builtProductsDirectoryURL: Result { return self["BUILT_PRODUCTS_DIR"].map { productsDir in @@ -176,9 +223,26 @@ public struct BuildSettings { private var productsDirectoryURLDependingOnAction: Result { if action == .archive { return self["OBJROOT"] - .fanout(archiveIntermediatesBuildProductsPath) - .map { objroot, path -> URL in + .map { objroot -> URL in let root = URL(fileURLWithPath: objroot, isDirectory: true) + guard 1600 <= UInt(self.settings["XCODE_VERSION_ACTUAL", default: "0"]) ?? 0 else { return root } + + return Result(at: root, attempt: { root in + var url = root + for _ in 0...url.pathComponents.count { + if url.pathComponents.suffix(2) == ["Build", "Intermediates.noindex"] { + return url + } else { + url.deleteLastPathComponent() + } + } + + throw NSError() + }) + .value ?? root + } + .fanout(archiveIntermediatesBuildProductsPath) + .map { root, path -> URL in return root.appendingPathComponent(path) } } else { @@ -241,6 +305,11 @@ public struct BuildSettings { return self["WRAPPER_NAME"] } + /// Attempts to determine the name of the built product. + public var productName: Result { + return self["PRODUCT_NAME"] + } + /// Attempts to determine the URL to the built product's wrapper, corresponding /// to its xcodebuild action. public var wrapperURL: Result { @@ -292,6 +361,26 @@ public struct BuildSettings { return self["TARGET_BUILD_DIR"] } + /// The "OPERATING_SYSTEM" component of the target triple. Used in XCFrameworks to denote the supported platform. + public var platformTripleOS: Result { + return self["LLVM_TARGET_TRIPLE_OS_VERSION"].map { osVersion in + // osVersion is a string like "ios8.0". Remove any trailing version number. + // This should match the OS component of an "unversionedTriple" printed by `swift -print-target-info`. + osVersion.replacingOccurrences(of: "([0-9]\\.?)*$", with: "", options: .regularExpression) + }.flatMapError { _ in + // LLVM_TARGET_TRIPLE_OS_VERSION may be unavailable if `USE_LLVM_TARGET_TRIPLES = NO`. + // SWIFT_PLATFORM_TARGET_PREFIX anecdotally appears to contain the unversioned OS component, even in + // non-swift projects. + self["SWIFT_PLATFORM_TARGET_PREFIX"] + } + } + + // The "ENVIRONMENT" component of the target triple, which is "simulator" when building for a simulator target + // and missing otherwise. + public var platformTripleVariant: Result { + return self["LLVM_TARGET_TRIPLE_SUFFIX"].map { $0.stripping(prefix: "-") } + } + /// Add subdirectory path if it's not possible to paste product to destination path public func productDestinationPath(in destinationURL: URL) -> URL { let directoryURL: URL @@ -310,3 +399,80 @@ extension BuildSettings: CustomStringConvertible { return "Build settings for target \"\(target)\": \(settings)" } } + +private enum Environment { + static let withoutActiveXcodeXCConfigFile: [String: String] = + ProcessInfo.processInfo.environment + .merging( + ["XCODE_XCCONFIG_FILE": "/dev/null", "LC_ALL": "c"], + uniquingKeysWith: { _, replacer in replacer } + ) +} + +extension SDK { + /// Starting around Xcode 7.3.1, and current as of Xcode 11.5, Xcodes contain a + /// xcodeproj that we can derive `XCDBLD.SDK`s via Xcode-defaulted `AVAILABLE_PLATFORMS`. + /// + /// - Note: Pass no scheme, later Xcodes handle it correctly, but Xcode 7.3.1 doesn’t… + /// - Note: As last ditch effort, try inside `/Applications/Xcode.app`, which might not exist. + /// - Note: Mostly, `xcodebuild -showsdks -json`-based `XCDBLD.SDK`s will be grabbed instead of + /// this signal reaching completion. + /// - Note: Will, where possible, draw from `SDK.knownIn2023YearSDKs` for 2023-era captialization. + static let setFromFallbackXcodeprojBuildSettings: SignalProducer?, NoError> = + Task("/usr/bin/xcrun", arguments: ["--find", "xcodebuild"], environment: Environment.withoutActiveXcodeXCConfigFile) + .launch() + .materializeResults() // to map below and ignore errors + .map { + $0.value?.value.flatMap { String(data: $0, encoding: .utf8) } + ?? "/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild\n" + } + .map { String.reversed($0)().first == "\n" ? String($0.dropLast(1)) : $0 } + .map(URL.init(fileURLWithPath:)) + .map { (base: URL) in + let relative = "../../usr/share/xcs/xcsd/node_modules/nodobjc/node_modules/ffi/deps/libffi/libffi.xcodeproj/" + return relative.withCString { + URL(fileURLWithFileSystemRepresentation: $0, isDirectory: true, relativeTo: base.isFileURL ? base.standardizedFileURL : URL(string: "file:///var/empty/ø/ø/ø/")!) + } + } + .map { (potentialFile: URL) in BuildArguments( + project: ProjectLocator.projectFile(potentialFile), + scheme: Scheme?.none, + configuration: "Release" + ) } + .map { ($0 as BuildArguments, BuildArguments.Action?.none, Environment.withoutActiveXcodeXCConfigFile) } + .flatMap(.race, BuildSettings.load) // note: above var empty path will soft error and get nilled below + .materializeResults() + .reduce(into: Set?.none) { + guard let unsplit = $1.value?.settings["AVAILABLE_PLATFORMS"] else { return } + guard $0 == nil else { return } + $0 = Set( + unsplit.split(separator: " ").lazy.map { + SDK(rawValue: String($0)) ?? SDK(name: String($0), simulatorHeuristic: "") + } + ) + } +} + +extension SDK { + /// - See: `SDK.setFromJSONShowSDKs` + /// - Note: Fallbacks are `SDK.setFromFallbackXcodeprojBuildSettings` and + /// hardcoded `SDK.knownIn2023YearSDKs`. + static let setsFromJSONShowSDKsWithFallbacks: SignalProducer, NoError> = + SDK.setFromJSONShowSDKs + .concat(SDK.setFromFallbackXcodeprojBuildSettings) + .skip(while: { $0 == nil }) + .take(first: 1) + .skipNil() + .reduce(into: SDK.knownIn2023YearSDKs) { $0 = $1 } + .replayLazily(upTo: 1) +} + +extension NSRegularExpression { + /// Check ``location`` of the returned object against ``Foundation.NSNotFound``. That's a good way to utilize this. + static func rangeOfFirstMatch(pattern: String, within string: String?) -> NSRange { + let ourString = string ?? "" + let range = NSRange(ourString.startIndex..., in: ourString) + let regex = try! NSRegularExpression(pattern: pattern) + return regex.rangeOfFirstMatch(in: ourString, range: range) + } +} diff --git a/Source/CarthageKit/CarthageKitVersion.swift b/Source/CarthageKit/CarthageKitVersion.swift index d5836e0520..aa741e0bd5 100644 --- a/Source/CarthageKit/CarthageKitVersion.swift +++ b/Source/CarthageKit/CarthageKitVersion.swift @@ -4,5 +4,5 @@ import Foundation public struct CarthageKitVersion { public let value: SemanticVersion - public static let current = CarthageKitVersion(value: SemanticVersion(0, 33, 0)) + public static let current = CarthageKitVersion(value: SemanticVersion(0, 40, 0)) } diff --git a/Source/CarthageKit/Constants.swift b/Source/CarthageKit/Constants.swift index b242768275..9342082596 100644 --- a/Source/CarthageKit/Constants.swift +++ b/Source/CarthageKit/Constants.swift @@ -1,5 +1,6 @@ import Foundation import Result +import Tentacle /// A struct including all constants. public struct Constants { @@ -96,9 +97,11 @@ public struct Constants { /// The relative path to a project's Cartfile.resolved. public static let resolvedCartfilePath = "Cartfile.resolved" + // TODO: Deprecate this. /// The text that needs to exist in a GitHub Release asset's name, for it to be /// tried as a binary framework. - public static let binaryAssetPattern = ".framework" + public static let frameworkBinaryAssetPattern = ".framework" + public static let xcframeworkBinaryAssetPattern = ".xcframework" /// MIME types allowed for GitHub Release assets, for them to be considered as /// binary frameworks. diff --git a/Source/CarthageKit/Dependency.swift b/Source/CarthageKit/Dependency.swift index 564e12234a..8294527ec4 100644 --- a/Source/CarthageKit/Dependency.swift +++ b/Source/CarthageKit/Dependency.swift @@ -50,6 +50,10 @@ public enum Dependency: Hashable { public var relativePath: String { return (Constants.checkoutsFolderPath as NSString).appendingPathComponent(name) } + + public var xcframeworkPath: String { + return (Constants.binariesFolderPath as NSString).appendingPathComponent("\(name).xcframework") + } } extension Dependency { diff --git a/Source/CarthageKit/Errors.swift b/Source/CarthageKit/Errors.swift index 15a89c36df..bb6e92640d 100644 --- a/Source/CarthageKit/Errors.swift +++ b/Source/CarthageKit/Errors.swift @@ -13,6 +13,12 @@ public enum CarthageError: Error { let dictionary: [URL: [URL]] } + public struct XCFrameworkRequired: Equatable { + let productName: String + let commonArchitectures: Set + let underlyingError: TaskError + } + /// One or more arguments was invalid. case invalidArgument(description: String) @@ -67,7 +73,7 @@ public enum CarthageError: Error { /// The project is not sharing any framework schemes, so Carthage cannot /// discover them. - case noSharedFrameworkSchemes(Dependency, Set) + case noSharedFrameworkSchemes(Dependency, Set) /// The project is not sharing any schemes, so Carthage cannot discover /// them. @@ -104,6 +110,13 @@ public enum CarthageError: Error { /// An archive (.zip, .gz, .bz2 ...) contains binaries that would /// be copied to the same destination path case duplicatesInArchive(duplicates: DuplicatesInArchive) + + /// (cause) + /// + /// Building universal frameworks with common architectures is not possible. + /// The device and simulator slices for "(productName)" both build for: (commonArchitectures) + /// Rebuild with --use-xcframeworks to create an xcframework bundle instead. + case xcframeworkRequired(XCFrameworkRequired) } extension CarthageError { @@ -197,6 +210,9 @@ extension CarthageError: Equatable { case let (.duplicatesInArchive(left), .duplicatesInArchive(right)): return left == right + case let (.xcframeworkRequired(left), .xcframeworkRequired(right)): + return left == right + default: return false } @@ -277,7 +293,7 @@ extension CarthageError: CustomStringConvertible { case let .noSharedFrameworkSchemes(dependency, platforms): var description = "Dependency \"\(dependency.name)\" has no shared framework schemes" if !platforms.isEmpty { - let platformsString = platforms.map { $0.rawValue }.joined(separator: ", ") + let platformsString = Set(platforms.map { $0.platformSimulatorlessFromHeuristic }).joined(separator: ", ") description += " for any of the platforms: \(platformsString)" } @@ -381,6 +397,15 @@ extension CarthageError: CustomStringConvertible { .map { "* \t\($0.value.map{ url in return url.absoluteString }.joined(separator: "\n\t")) \n\t\tto:\n\t\($0.key)" } .joined(separator: "\n") return "Invalid archive - Found multiple frameworks with the same unarchiving destination:\n\(prettyDupeList)" + + case let .xcframeworkRequired(info): + let archs = info.commonArchitectures.joined(separator: ", ") + return [ + "\(info.underlyingError)", + "Building universal frameworks with common architectures is not possible. " + + "The device and simulator slices for \"\(info.productName)\" both build for: \(archs)", + "Rebuild with --use-xcframeworks to create an xcframework bundle instead." + ].joined(separator: "\n") } } } diff --git a/Source/CarthageKit/FrameworkExtensions.swift b/Source/CarthageKit/FrameworkExtensions.swift index e7a37877bf..93c4198677 100644 --- a/Source/CarthageKit/FrameworkExtensions.swift +++ b/Source/CarthageKit/FrameworkExtensions.swift @@ -408,27 +408,6 @@ extension Reactive where Base: FileManager { } .map { URL(fileURLWithPath: $0, isDirectory: true) } } - - public func copyItem(_ source: URL, into: URL) -> SignalProducer { - let destination = into.appendingPathComponent(source.lastPathComponent) - do { - try self.base.copyItem(at: source, to: destination, avoiding·rdar·32984063: true) - return SignalProducer(value: destination) - } catch { - return SignalProducer(error: .internalError(description: "copyItem failed: \(error)\n\(source)\n\(into)")) - } - } - - public func replaceItem(at originalItemURL: URL, withItemAt newItemURL: URL) -> SignalProducer<(), CarthageError> { - do { - guard (try self.base.replaceItemAt(originalItemURL, withItemAt: newItemURL, backupItemName: nil, options: .usingNewMetadataOnly)) != nil else { - return SignalProducer(error: .internalError(description: "replaceItem succeeded, but returned nil")) - } - return SignalProducer(.empty) - } catch { - return SignalProducer(error: .internalError(description: "replaceItem failed: \(error)")) - } - } } private let defaultSessionError = NSError(domain: Constants.bundleIdentifier, code: 1, userInfo: nil) diff --git a/Source/CarthageKit/Git.swift b/Source/CarthageKit/Git.swift index 4343b37987..4b93f24f7d 100644 --- a/Source/CarthageKit/Git.swift +++ b/Source/CarthageKit/Git.swift @@ -167,7 +167,6 @@ public func contentsOfFileInRepository(_ repositoryFileURL: URL, _ path: String, public func checkoutRepositoryToDirectory( _ repositoryFileURL: URL, _ workingDirectoryURL: URL, - force: Bool, revision: String = "HEAD" ) -> SignalProducer<(), CarthageError> { return SignalProducer { () -> Result<[String: String], CarthageError> in @@ -185,23 +184,15 @@ public func checkoutRepositoryToDirectory( ) } } - .flatMap(.concat) { (environment: [String: String]) -> SignalProducer in - var arguments = [ "checkout", "--quiet" ] - if force { - arguments.append("--force") - } - arguments.append(revision) - - return launchGitTask(arguments, repositoryFileURL: repositoryFileURL, environment: environment) + .flatMap(.concat) { environment in + return launchGitTask([ "checkout", "--quiet", "--force", revision ], repositoryFileURL: repositoryFileURL, environment: environment) } .then(SignalProducer<(), CarthageError>.empty) } /// Clones the given submodule into the working directory of its parent /// repository, but without any Git metadata. -/// -/// `cacheURLMap` maps from a remote URL to an optional local cache URL -public func cloneSubmoduleInWorkingDirectory(_ submodule: Submodule, _ workingDirectoryURL: URL, cacheURLMap: ((GitURL) -> URL?)?) -> SignalProducer<(), CarthageError> { +public func cloneSubmoduleInWorkingDirectory(_ submodule: Submodule, _ workingDirectoryURL: URL) -> SignalProducer<(), CarthageError> { let submoduleDirectoryURL = workingDirectoryURL.appendingPathComponent(submodule.path, isDirectory: true) func repositoryCheck(_ description: String, attempt closure: () throws -> T) -> Result { @@ -216,10 +207,7 @@ public func cloneSubmoduleInWorkingDirectory(_ submodule: Submodule, _ workingDi } let purgeGitDirectories = FileManager.default.reactive - .enumerator(at: submoduleDirectoryURL, - includingPropertiesForKeys: [ .isDirectoryKey, .nameKey ], - options: [ .skipsSubdirectoryDescendants ], - catchErrors: true) + .enumerator(at: submoduleDirectoryURL, includingPropertiesForKeys: [ .isDirectoryKey, .nameKey ], catchErrors: true) .attemptMap { enumerator, url -> Result<(), CarthageError> in return repositoryCheck("enumerate name of descendant at \(url.path)", attempt: { try url.resourceValues(forKeys: [ .nameKey ]).name @@ -241,23 +229,13 @@ public func cloneSubmoduleInWorkingDirectory(_ submodule: Submodule, _ workingDi } return SignalProducer<(), CarthageError> { () -> Result<(), CarthageError> in - return repositoryCheck("remove submodule checkout") { - try FileManager.default.removeItem(at: submoduleDirectoryURL) - } + repositoryCheck("remove submodule checkout") { + try FileManager.default.removeItem(at: submoduleDirectoryURL) } - .then(cloneOrFetch( - remoteURL: submodule.url, - cacheURL: cacheURLMap?(submodule.url), - destinationURL: submoduleDirectoryURL, - isDestinationBare: false, - commitish: submodule.sha)) - .then(checkoutRepositoryToDirectory(submoduleDirectoryURL, submoduleDirectoryURL, force: false, revision: submodule.sha)) - .then(submodulesInRepository(submoduleDirectoryURL) - .flatMap(.concat) { submodule -> SignalProducer<(), CarthageError> in - return cloneSubmoduleInWorkingDirectory(submodule, submoduleDirectoryURL, cacheURLMap: cacheURLMap) - } - ) - .then(purgeGitDirectories) + } + .then(cloneRepository(submodule.url, workingDirectoryURL.appendingPathComponent(submodule.path), isBare: false)) + .then(checkoutSubmodule(submodule, submoduleDirectoryURL)) + .then(purgeGitDirectories) } /// Recursively checks out the given submodule's revision, in its working @@ -563,92 +541,3 @@ public func addSubmoduleToRepository(_ repositoryFileURL: URL, _ submodule: Subm } } } - -/// Used by the cloneOrFetch function to indicate whether the operation will be a clone or fetching -public enum CloneOrFetch { - case cloning, fetching -} - -/// Clones the given project to the given local URL, -/// or fetches inside it if it has already been cloned. -/// Optionally takes a commitish to check for prior to fetching. -/// -/// Returns a signal which will send the operation type once started, -/// then complete when the operation completes. -public func cloneOrFetch( - remoteURL: GitURL, - to repositoryURL: URL, - isBare: Bool, - commitish: String? = nil -) -> SignalProducer { - return isGitRepository(repositoryURL) - .flatMap(.merge) { isRepository -> SignalProducer in - if isRepository { - let fetchProducer: () -> SignalProducer = { - guard FetchCache.needsFetch(forURL: remoteURL) else { - return SignalProducer(value: nil) - } - - return SignalProducer(value: CloneOrFetch.fetching) - .concat( - fetchRepository(repositoryURL, remoteURL: remoteURL, refspec: "+refs/heads/*:refs/heads/*") - .then(SignalProducer.empty) - ) - } - - // If we've already cloned the repo, check for the revision, possibly skipping an unnecessary fetch - if let commitish = commitish { - return SignalProducer.zip( - branchExistsInRepository(repositoryURL, pattern: commitish), - commitExistsInRepository(repositoryURL, revision: commitish) - ) - .flatMap(.concat) { branchExists, commitExists -> SignalProducer in - // If the given commitish is a branch, we should fetch. - if branchExists || !commitExists { - return fetchProducer() - } else { - return SignalProducer(value: nil) - } - } - } else { - return fetchProducer() - } - } else { - // Either the directory didn't exist or it did but wasn't a git repository - // (Could happen if the process is killed during a previous directory creation) - // So we remove it, then clone - _ = try? FileManager.default.removeItem(at: repositoryURL) - return SignalProducer(value: CloneOrFetch.cloning) - .concat( - cloneRepository(remoteURL, repositoryURL, isBare: isBare) - .then(SignalProducer.empty) - ) - } - } -} - -/// Clones the given project to the given destination URL, -/// or fetches inside it if it has already been cloned. -/// If a `cacheURL` is passed, first clone or fetch in the cache repository. -/// Optionally takes a commitish to check for prior to fetching. -/// -/// Returns a signal which will send a void value when the operation completes. -public func cloneOrFetch( - remoteURL: GitURL, - cacheURL: URL?, - isCacheBare: Bool = true, - destinationURL: URL, - isDestinationBare: Bool, - commitish: String? = nil -) -> SignalProducer<(), CarthageError> { - if let cacheURL = cacheURL { - return cloneOrFetch(remoteURL: remoteURL, to: cacheURL, isBare: isCacheBare, commitish: commitish) - .then(cloneOrFetch(remoteURL: GitURL(cacheURL.path), to: destinationURL, isBare: isDestinationBare, commitish: commitish)) - .map { _ in () } - .take(last: 1) - } else { - return cloneOrFetch(remoteURL: remoteURL, to: destinationURL, isBare: isDestinationBare, commitish: commitish) - .map { _ in () } - .take(last: 1) - } -} diff --git a/Source/CarthageKit/GitHub.swift b/Source/CarthageKit/GitHub.swift index 8e5a298afa..61416ba1e3 100644 --- a/Source/CarthageKit/GitHub.swift +++ b/Source/CarthageKit/GitHub.swift @@ -161,12 +161,3 @@ extension Client { } } } - -extension URLSession { - public static var proxiedSession: URLSession { - let configuration = URLSessionConfiguration.default - configuration.connectionProxyDictionary = Proxy.default.connectionProxyDictionary - - return URLSession(configuration: configuration) - } -} diff --git a/Source/CarthageKit/GitURL.swift b/Source/CarthageKit/GitURL.swift index 7b3bce5997..1d03471540 100644 --- a/Source/CarthageKit/GitURL.swift +++ b/Source/CarthageKit/GitURL.swift @@ -47,21 +47,26 @@ public struct GitURL { /// The name of the repository, if it can be inferred from the URL. public var name: String? { - let absoluteURLString: String - if urlString.hasPrefix(".") { - absoluteURLString = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - .appendingPathComponent(urlString) - .standardizedFileURL - .absoluteString - } else { - absoluteURLString = urlString - } - let components = absoluteURLString.split(omittingEmptySubsequences: true) { $0 == "/" } - - return components + let lastComponent = urlString + .replacingOccurrences(of: "\u{0000}", with: "\u{2400}") // can’t have those + .split(omittingEmptySubsequences: true) { $0 == "/" } .last .map(String.init) .map(strippingGitSuffix) + + /// Potentially used to prevent backwards or noop directory traversal via «FULL STOP» characters… + /// …by deploying the «FULLWIDTH FULL STOP» character. + var replacementForEntirelyCharactersOfFullStop: [Character] = [] + for char in (lastComponent ?? "") { + guard char == "." else { replacementForEntirelyCharactersOfFullStop = []; break } + replacementForEntirelyCharactersOfFullStop.append("\u{FF0E}") + } + + guard replacementForEntirelyCharactersOfFullStop.isEmpty else { + return String(replacementForEntirelyCharactersOfFullStop) + } + + return lastComponent } public init(_ urlString: String) { diff --git a/Source/CarthageKit/MachHeader.swift b/Source/CarthageKit/MachHeader.swift index f8840775ae..7fb953d564 100644 --- a/Source/CarthageKit/MachHeader.swift +++ b/Source/CarthageKit/MachHeader.swift @@ -33,7 +33,7 @@ struct MachHeader { return !is64BitHeader } - var endianess: Endianness { + var endianness: Endianness { return magic == MH_CIGAM_64 || magic == MH_CIGAM ? .big : .little } } @@ -61,8 +61,11 @@ extension MachHeader { /// magic cputype cpusubtype caps filetype ncmds sizeofcmds flags /// 0xfeedfacf 16777223 3 0x00 1 8 1720 0x00002000 /// + /// - Note: `objdump` invocation requires flags unimplemented in Xcode 7.X ⋯ as it stands, + /// only a moreso-alternative codepath invokes this; be cautious to not invert that. /// - See Also: [LLVM MachODump.cpp](https://llvm.org/viewvc/llvm-project/llvm/trunk/tools/llvm-objdump/MachODump.cpp?view=markup&pathrev=225383###see%C2%B7line%C2%B72745) static func headers(forMachOFileAtUrl url: URL) -> SignalProducer { + // TODO: Potentially, `mdfind` other non-version-7.X Xcodes that might contain valid `objdump`. // This is the command `otool -h` actually invokes let task = Task("/usr/bin/xcrun", arguments: [ diff --git a/Source/CarthageKit/Project.swift b/Source/CarthageKit/Project.swift index c34437395c..0c3c0b60ad 100644 --- a/Source/CarthageKit/Project.swift +++ b/Source/CarthageKit/Project.swift @@ -1,5 +1,6 @@ // swiftlint:disable file_length +import CommonCrypto import Foundation import Result import ReactiveSwift @@ -47,9 +48,6 @@ public enum ProjectEvent { /// Building an uncached project. case buildingUncached(Dependency) - - /// Removing unused packages - case removingUnneededItem(URL) } extension ProjectEvent: Equatable { @@ -103,7 +101,7 @@ public final class Project { // swiftlint:disable:this type_body_length /// Whether to use submodules for dependencies, or just check out their /// working directories. public var useSubmodules = false - + /// Whether to use authentication credentials from ~/.netrc file /// to download binary only frameworks. public var useNetrc = false @@ -240,9 +238,9 @@ public final class Project { // swiftlint:disable:this type_body_length return SignalProducer(value: binaryProject) } else { self._projectEventsObserver.send(value: .downloadingBinaryFrameworkDefinition(.binary(binary), binary.url)) - + let request = self.buildURLRequest(for: binary.url, useNetrc: self.useNetrc) - return URLSession.shared.reactive.data(with: request) + return URLSession.proxiedSession.reactive.data(with: request) .mapError { CarthageError.readFailed(binary.url, $0 as NSError) } .attemptMap { data, _ in return BinaryProject.from(jsonData: data).mapError { error in @@ -256,8 +254,8 @@ public final class Project { // swiftlint:disable:this type_body_length } .startOnQueue(self.cachedBinaryProjectsQueue) } - - + + /// Builds URL request /// /// - Parameters: @@ -267,7 +265,7 @@ public final class Project { // swiftlint:disable:this type_body_length private func buildURLRequest(for url: URL, useNetrc: Bool) -> URLRequest { var request = URLRequest(url: url) guard useNetrc else { return request } - + // When downloading a binary, `carthage` will take into account the user's // `~/.netrc` file to determine authentication credentials switch Netrc.load() { @@ -602,21 +600,16 @@ public final class Project { // swiftlint:disable:this type_body_length .then(shouldCheckout ? checkoutResolvedDependencies(dependenciesToUpdate, buildOptions: buildOptions) : .empty) } - /// Constructs the file:// URL at which a given .framework + /// Constructs the file:// URL at which a given .framework or .xcframework /// will be found. Depends on the location of the current project. private func frameworkURLInCarthageBuildFolder( - forPlatform platform: Platform, + forSDK sdk: SDK, frameworkNameAndExtension: String ) -> Result { - guard let lastComponent = URL(string: frameworkNameAndExtension)?.pathExtension, - lastComponent == "framework" else { - return .failure(.internalError(description: "\(frameworkNameAndExtension) is not a valid framework identifier")) - } - - guard let destinationURLInWorkingDir = platform + guard let destinationURLInWorkingDir = sdk .relativeURL? .appendingPathComponent(frameworkNameAndExtension, isDirectory: true) else { - return .failure(.internalError(description: "failed to construct framework destination url from \(platform) and \(frameworkNameAndExtension)")) + return .failure(.internalError(description: "failed to construct framework destination url from \(sdk.platformSimulatorlessFromHeuristic) and \(frameworkNameAndExtension)")) } return .success(self @@ -677,9 +670,7 @@ public final class Project { // swiftlint:disable:this type_body_length .flatMap(.merge) { frameworksUrls -> SignalProducer in return SignalProducer(frameworksUrls) .flatMap(.merge) { url -> SignalProducer in - return platformForFramework(url) - .attemptMap { self.frameworkURLInCarthageBuildFolder(forPlatform: $0, - frameworkNameAndExtension: url.lastPathComponent) } + return self.getBinaryFrameworkURL(url: url) } .collect() .flatMap(.merge) { destinationUrls -> SignalProducer in @@ -696,8 +687,7 @@ public final class Project { // swiftlint:disable:this type_body_length // Check if the framework are compatible with the current Swift version .flatMap(.merge) { pair -> SignalProducer in return checkFrameworkCompatibility(pair.frameworkSourceURL, usingToolchain: toolchain) - .mapError { error in CarthageError.internalError(description: error.description) } - .reduce(into: pair) { (_, _) = ($0.1, $1) } + .then(SignalProducer(value: pair)) } // If the framework is compatible copy it over to the destination folder in Carthage/Build .flatMap(.merge) { pair -> SignalProducer in @@ -707,6 +697,10 @@ public final class Project { // swiftlint:disable:this type_body_length } // Copy .dSYM & .bcsymbolmap too .flatMap(.merge) { frameworkDestinationURL -> SignalProducer in + guard frameworkDestinationURL.pathExtension != "xcframework" else { + // xcframeworks have embedded debug information which is not copied out. + return SignalProducer(value: frameworkDestinationURL) + } return self.copyDSYMToBuildFolderForFramework(frameworkDestinationURL, fromDirectoryURL: directoryURL) .then(self.copyBCSymbolMapsToBuildFolderForFramework(frameworkDestinationURL, fromDirectoryURL: directoryURL)) .then(SignalProducer(value: frameworkDestinationURL)) @@ -725,6 +719,19 @@ public final class Project { // swiftlint:disable:this type_body_length } } + /// Ensures binary framework has a valid extension and returns url in build folder + private func getBinaryFrameworkURL(url: URL) -> SignalProducer { + switch url.pathExtension { + case "xcframework": + return SignalProducer(value: url) + .map { self.directoryURL.appendingPathComponent(Constants.binariesFolderPath).appendingPathComponent($0.lastPathComponent) } + default: + return platformForFramework(url) + .attemptMap { self.frameworkURLInCarthageBuildFolder(forSDK: $0, + frameworkNameAndExtension: url.lastPathComponent) } + } + } + /// Removes the file located at the given URL /// /// Sends empty value on successful removal @@ -737,11 +744,17 @@ public final class Project { // swiftlint:disable:this type_body_length /// Installs binaries and debug symbols for the given project, if available. /// /// Sends a boolean indicating whether binaries were installed. - private func installBinaries(for dependency: Dependency, pinnedVersion: PinnedVersion, toolchain: String?) -> SignalProducer { + private func installBinaries(for dependency: Dependency, pinnedVersion: PinnedVersion, preferXCFrameworks: Bool, toolchain: String?) -> SignalProducer { switch dependency { case let .gitHub(server, repository): let client = Client(server: server) - return self.downloadMatchingBinaries(for: dependency, pinnedVersion: pinnedVersion, fromRepository: repository, client: client) + return self.downloadMatchingBinaries( + for: dependency, + pinnedVersion: pinnedVersion, + fromRepository: repository, + preferXCFrameworks: preferXCFrameworks, + client: client + ) .flatMapError { error -> SignalProducer in if !client.isAuthenticated { return SignalProducer(error: error) @@ -750,6 +763,7 @@ public final class Project { // swiftlint:disable:this type_body_length for: dependency, pinnedVersion: pinnedVersion, fromRepository: repository, + preferXCFrameworks: preferXCFrameworks, client: Client(server: server, isAuthenticated: false) ) } @@ -779,6 +793,7 @@ public final class Project { // swiftlint:disable:this type_body_length for dependency: Dependency, pinnedVersion: PinnedVersion, fromRepository repository: Repository, + preferXCFrameworks: Bool, client: Client ) -> SignalProducer { return client.execute(repository.release(forTag: pinnedVersion.commitish)) @@ -805,13 +820,12 @@ public final class Project { // swiftlint:disable:this type_body_length self._projectEventsObserver.send(value: .downloadingBinaries(dependency, release.nameWithFallback)) }) .flatMap(.concat) { release -> SignalProducer in - return SignalProducer(release.assets) - .filter { asset in - if asset.name.range(of: Constants.Project.binaryAssetPattern) == nil { - return false - } - return Constants.Project.binaryAssetContentTypes.contains(asset.contentType) - } + let potentialFrameworkAssets = release.assets.filter { asset in + let matchesContentType = Constants.Project.binaryAssetContentTypes.contains(asset.contentType) + let matchesName = asset.name.contains(Constants.Project.frameworkBinaryAssetPattern) || asset.name.contains(Constants.Project.xcframeworkBinaryAssetPattern) + return matchesContentType && matchesName + } + return SignalProducer(binaryAssetFilter(prioritizing: potentialFrameworkAssets, preferXCFrameworks: preferXCFrameworks)) .flatMap(.concat) { asset -> SignalProducer in let fileURL = fileURLToCachedBinary(dependency, release, asset) @@ -866,56 +880,50 @@ public final class Project { // swiftlint:disable:this type_body_length /// Checks out the given dependency into its intended working directory, /// cloning it first if need be. - private func checkoutDependency( + private func checkoutOrCloneDependency( _ dependency: Dependency, version: PinnedVersion, submodulesByPath: [String: Submodule] ) -> SignalProducer<(), CarthageError> { let revision = version.commitish - let repositoryURL = repositoryFileURL(for: dependency) - - let producer = { () -> SignalProducer<(), CarthageError> in - let workingDirectoryURL = self.directoryURL.appendingPathComponent(dependency.relativePath, isDirectory: true) - - /// The submodule for an already existing submodule at dependency project’s path - /// or the submodule to be added at this path given the `--use-submodules` flag. - let submodule: Submodule? - - if var foundSubmodule = submodulesByPath[dependency.relativePath] { - foundSubmodule.url = dependency.gitURL(preferHTTPS: self.preferHTTPS)! - foundSubmodule.sha = revision - submodule = foundSubmodule - } else if self.useSubmodules { - submodule = Submodule(name: dependency.relativePath, path: dependency.relativePath, url: dependency.gitURL(preferHTTPS: self.preferHTTPS)!, sha: revision) - } else { - submodule = nil - } + return cloneOrFetchDependency(dependency, commitish: revision) + .flatMap(.merge) { repositoryURL -> SignalProducer<(), CarthageError> in + let workingDirectoryURL = self.directoryURL.appendingPathComponent(dependency.relativePath, isDirectory: true) + + /// The submodule for an already existing submodule at dependency project’s path + /// or the submodule to be added at this path given the `--use-submodules` flag. + let submodule: Submodule? + + if var foundSubmodule = submodulesByPath[dependency.relativePath] { + foundSubmodule.url = dependency.gitURL(preferHTTPS: self.preferHTTPS)! + foundSubmodule.sha = revision + submodule = foundSubmodule + } else if self.useSubmodules { + submodule = Submodule(name: dependency.relativePath, path: dependency.relativePath, url: dependency.gitURL(preferHTTPS: self.preferHTTPS)!, sha: revision) + } else { + submodule = nil + } - let symlinkCheckoutPaths = self.symlinkCheckoutPaths(for: dependency, version: version, withRepository: repositoryURL, atRootDirectory: self.directoryURL) + let symlinkCheckoutPaths = self.symlinkCheckoutPaths(for: dependency, version: version, withRepository: repositoryURL, atRootDirectory: self.directoryURL) - if let submodule = submodule { - // In the presence of `submodule` for `dependency` — before symlinking, (not after) — add submodule and its submodules: - // `dependency`, subdependencies that are submodules, and non-Carthage-housed submodules. - return addSubmoduleToRepository(self.directoryURL, submodule, GitURL(repositoryURL.path)) - .startOnQueue(self.gitOperationQueue) - .then(symlinkCheckoutPaths) - } else { - return checkoutRepositoryToDirectory(repositoryURL, workingDirectoryURL, force: true, revision: revision) - // For checkouts of “ideally bare” repositories of `dependency`, we add its submodules by cloning ourselves, after symlinking. - .then(symlinkCheckoutPaths) - .then( - submodulesInRepository(repositoryURL, revision: revision) - .flatMap(.concat) { (submodule) in - return cloneSubmoduleInWorkingDirectory(submodule, workingDirectoryURL, cacheURLMap: { (gitURL: GitURL) in - let dependency = Dependency.git(gitURL) - return repositoryFileURL(for: dependency) - }) - } - ) + if let submodule = submodule { + // In the presence of `submodule` for `dependency` — before symlinking, (not after) — add submodule and its submodules: + // `dependency`, subdependencies that are submodules, and non-Carthage-housed submodules. + return addSubmoduleToRepository(self.directoryURL, submodule, GitURL(repositoryURL.path)) + .startOnQueue(self.gitOperationQueue) + .then(symlinkCheckoutPaths) + } else { + return checkoutRepositoryToDirectory(repositoryURL, workingDirectoryURL, revision: revision) + // For checkouts of “ideally bare” repositories of `dependency`, we add its submodules by cloning ourselves, after symlinking. + .then(symlinkCheckoutPaths) + .then( + submodulesInRepository(repositoryURL, revision: revision) + .flatMap(.merge) { + cloneSubmoduleInWorkingDirectory($0, workingDirectoryURL) + } + ) + } } - } - - return producer() .on(started: { self._projectEventsObserver.send(value: .checkingOut(dependency, revision)) }) @@ -962,121 +970,6 @@ public final class Project { // swiftlint:disable:this type_body_length } } - public func removeUnneededItems() -> SignalProducer<(), CarthageError> { - let binariesDirectoryURL = self.directoryURL - .appendingPathComponent(Constants.binariesFolderPath, isDirectory: true) - .resolvingSymlinksInPath() - - return loadResolvedCartfile() - .flatMap(.merge) { resolved -> SignalProducer in - return SignalProducer(resolved.dependencies.keys) - } - .flatMap(.merge) { dependency -> SignalProducer<(checkoutURL: URL, versionFileURL: URL, binaryURLs: [URL]), CarthageError> in - let checkoutURL = self.directoryURL - .appendingPathComponent(dependency.relativePath, isDirectory: true) - .resolvingSymlinksInPath() - - let versionFileURL = VersionFile - .url(for: dependency, rootDirectoryURL: self.directoryURL) - .resolvingSymlinksInPath() - - let frameworkURLs = buildableSchemesInDirectory(checkoutURL, withConfiguration: "Release") - .flatMap(.concurrent(limit: 4)) { scheme, project -> SignalProducer in - let buildArguments = BuildArguments(project: project, scheme: scheme, configuration: "Release") - return BuildSettings.load(with: buildArguments) - } - .flatMap(.concat) { settings -> SignalProducer<(BuildSettings, String), CarthageError> in - return SignalProducer(settings.wrapperName).map { (settings, $0) } - } - .flatMap(.concat) { settings, wrapperName -> SignalProducer<(URL, isStatic: Bool), CarthageError> in - settings.buildSDKs.map { sdk -> (URL, isStatic: Bool) in - let url = settings.productDestinationPath(in: binariesDirectoryURL.appendingPathComponent(sdk.platform.rawValue, isDirectory: true)) - .appendingPathComponent(wrapperName) - let isStatic = settings.frameworkType.value.flatMap { $0 } == .static - return (url, isStatic) - } - } - - return frameworkURLs.flatMap(.concurrent(limit: 4)) { frameworkURL, isStatic -> SignalProducer in - let framework = SignalProducer(value: frameworkURL) - - if isStatic { - return framework - } - - let bcSymbolMaps = BCSymbolMapsForFramework(frameworkURL) - .flatMapError { _ in SignalProducer.empty } - let dSYMs = dSYMForFramework(frameworkURL, inDirectoryURL: frameworkURL.deletingLastPathComponent()) - .flatMapError { _ in SignalProducer.empty } - return .merge(framework, bcSymbolMaps, dSYMs) - } - .collect() - .map { (checkoutURL, versionFileURL, $0) } - } - .collect() - .map { urls -> (checkoutURLs: Set, versionFileURLs: Set, binaryURLs: Set) in - var checkoutURLSet: Set = [] - var versionFileURLSet: Set = [] - var binaryURLSet: Set = [] - - for (checkoutURL, versionFileURL, binaryURLs) in urls { - checkoutURLSet.insert(checkoutURL) - versionFileURLSet.insert(versionFileURL) - binaryURLSet.formUnion(binaryURLs) - } - - return (checkoutURLSet, versionFileURLSet, binaryURLSet) - } - .flatMap(.merge) { checkoutURLs, versionFileURLs, binaryURLs -> SignalProducer in - let fileManager = FileManager.default - - var urls: [URL] = [] - urls += (try? fileManager - .contentsOfDirectory( - at: self.directoryURL.appendingPathComponent(Constants.checkoutsFolderPath, isDirectory: true), - includingPropertiesForKeys: nil - ) - .map { $0.resolvingSymlinksInPath() } - .filter { !checkoutURLs.contains($0) }) ?? [] - - urls += (try? fileManager - .contentsOfDirectory( - at: self.directoryURL.appendingPathComponent(Constants.binariesFolderPath, isDirectory: true), - includingPropertiesForKeys: nil - ) - .map { $0.resolvingSymlinksInPath() } - .filter { $0.pathExtension == VersionFile.pathExtension && - !versionFileURLs.contains($0) }) ?? [] - - urls += Platform.supportedPlatforms - .flatMap { platform -> [URL] in - (try? fileManager - .contentsOfDirectory( - at: self.directoryURL.appendingPathComponent(platform.relativePath, isDirectory: true), - includingPropertiesForKeys: nil - ) - .filter { !binaryURLs.contains($0) && $0.lastPathComponent != FrameworkType.staticFolderName }) ?? [] - } - - urls += Platform.supportedPlatforms - .flatMap { platform -> [URL] in - (try? fileManager - .contentsOfDirectory( - at: self.directoryURL - .appendingPathComponent(platform.relativePath, isDirectory: true) - .appendingPathComponent(FrameworkType.staticFolderName, isDirectory: true), - includingPropertiesForKeys: nil - ) - .filter { !binaryURLs.contains($0) }) ?? [] - } - - return SignalProducer(Set(urls)) - } - .on { self._projectEventsObserver.send(value: ProjectEvent.removingUnneededItem($0)) } - .flatMap(.merge, self.removeItem(at:)) - .then(SignalProducer<(), CarthageError>.empty) - } - /// Checks out the dependencies listed in the project's Cartfile.resolved, /// optionally they are limited by the given list of dependency names. public func checkoutResolvedDependencies(_ dependenciesToCheckout: [String]? = nil, buildOptions: BuildOptions?) -> SignalProducer<(), CarthageError> { @@ -1107,24 +1000,11 @@ public final class Project { // swiftlint:disable:this type_body_length .flatMap(.concurrent(limit: 4)) { dependency, version -> SignalProducer<(), CarthageError> in switch dependency { case .git, .gitHub: - return self.cloneOrFetchDependency(dependency, commitish: version.commitish) - .then(SignalProducer<(), CarthageError>.empty) + return self.checkoutOrCloneDependency(dependency, version: version, submodulesByPath: submodulesByPath) case .binary: return .empty } } - // The checkoutDependency operation may clone or fetch submodules that are the same as some dependencies, - // so it should run after cloneOrFetchDependency to prevent conflict. - .then(SignalProducer<(Dependency, PinnedVersion), CarthageError>(dependencies) - .flatMap(.concat) { (dependency, version) -> SignalProducer<(), CarthageError> in - switch dependency { - case .git, .gitHub: - return self.checkoutDependency(dependency, version: version, submodulesByPath: submodulesByPath) - case .binary: - return .empty - } - } - ) } .then(SignalProducer<(), CarthageError>.empty) } @@ -1133,36 +1013,44 @@ public final class Project { // swiftlint:disable:this type_body_length binary: BinaryURL, pinnedVersion: PinnedVersion, projectName: String, - toolchain: String? + toolchain: String?, + preferXCFrameworks: Bool ) -> SignalProducer<(), CarthageError> { return SignalProducer(result: SemanticVersion.from(pinnedVersion)) .mapError { CarthageError(scannableError: $0) } .combineLatest(with: self.downloadBinaryFrameworkDefinition(binary: binary)) - .attemptMap { semanticVersion, binaryProject -> Result<(SemanticVersion, URL), CarthageError> in - guard let frameworkURL = binaryProject.versions[pinnedVersion] else { - return .failure(CarthageError.requiredVersionNotFound(Dependency.binary(binary), VersionSpecifier.exactly(semanticVersion))) + .flatMap(.concat) { semanticVersion, binaryProject -> SignalProducer<(SemanticVersion, URL), CarthageError> in + guard let frameworkURLs = binaryProject.versions[pinnedVersion] else { + return SignalProducer(error: CarthageError.requiredVersionNotFound(Dependency.binary(binary), VersionSpecifier.exactly(semanticVersion))) } - - return .success((semanticVersion, frameworkURL)) + + let urlsAndVersions = binaryAssetFilter(prioritizing: frameworkURLs, preferXCFrameworks: preferXCFrameworks) + .map { (semanticVersion, $0) } + + return SignalProducer(urlsAndVersions) } .flatMap(.concat) { semanticVersion, frameworkURL in return self.downloadBinary(dependency: Dependency.binary(binary), version: semanticVersion, url: frameworkURL) } - .flatMap(.concat) { self.unarchiveAndCopyBinaryFrameworks(zipFile: $0, projectName: projectName, pinnedVersion: pinnedVersion, toolchain: toolchain) } + .flatMap(.concat) { zipFile in + self.unarchiveAndCopyBinaryFrameworks(zipFile: zipFile, projectName: projectName, pinnedVersion: pinnedVersion, toolchain: toolchain) + .on(failed: { _ in + try? FileManager.default.removeItem(at: zipFile) + }) + } .flatMap(.concat) { self.removeItem(at: $0) } } /// Downloads the binary only framework file. Sends the URL to each downloaded zip, after it has been moved to a /// less temporary location. private func downloadBinary(dependency: Dependency, version: SemanticVersion, url: URL) -> SignalProducer { - let fileName = url.lastPathComponent - let fileURL = fileURLToCachedBinaryDependency(dependency, version, fileName) + let fileURL = downloadURLToCachedBinaryDependency(dependency, version, url) if FileManager.default.fileExists(atPath: fileURL.path) { return SignalProducer(value: fileURL) } else { let request = self.buildURLRequest(for: url, useNetrc: self.useNetrc) - return URLSession.shared.reactive.download(with: request) + return URLSession.proxiedSession.reactive.download(with: request) .on(started: { self._projectEventsObserver.send(value: .downloadingBinaries(dependency, version.description)) }) @@ -1305,12 +1193,12 @@ public final class Project { // swiftlint:disable:this type_body_length guard options.useBinaries else { return .empty } - return self.installBinaries(for: dependency, pinnedVersion: version, toolchain: options.toolchain) + return self.installBinaries(for: dependency, pinnedVersion: version, preferXCFrameworks: options.useXCFrameworks, toolchain: options.toolchain) .filterMap { installed -> (Dependency, PinnedVersion)? in return installed ? (dependency, version) : nil } case let .binary(binary): - return self.installBinariesForBinaryProject(binary: binary, pinnedVersion: version, projectName: dependency.name, toolchain: options.toolchain) + return self.installBinariesForBinaryProject(binary: binary, pinnedVersion: version, projectName: dependency.name, toolchain: options.toolchain, preferXCFrameworks: options.useXCFrameworks) .then(.init(value: (dependency, version))) } } @@ -1451,9 +1339,21 @@ private func fileURLToCachedBinary(_ dependency: Dependency, _ release: Release, } /// Constructs a file URL to where the binary only framework download should be cached -private func fileURLToCachedBinaryDependency(_ dependency: Dependency, _ semanticVersion: SemanticVersion, _ fileName: String) -> URL { - // ~/Library/Caches/org.carthage.CarthageKit/binaries/MyBinaryProjectFramework/2.3.1/MyBinaryProject.framework.zip - return Constants.Dependency.assetsURL.appendingPathComponent("\(dependency.name)/\(semanticVersion)/\(fileName)") +private func downloadURLToCachedBinaryDependency(_ dependency: Dependency, _ semanticVersion: SemanticVersion, _ url: URL) -> URL { + let urlBytes = url.absoluteString.utf8CString + var digest = Data(count: Int(CC_SHA256_DIGEST_LENGTH)) + _ = digest.withUnsafeMutableBytes { buffer in + urlBytes.withUnsafeBytes { data in + CC_SHA256(data.baseAddress!, CC_LONG(urlBytes.count), buffer) + } + } + let hexDigest = digest.map { String(format: "%02hhx", $0) }.joined() + let fileName = url.deletingPathExtension().lastPathComponent + let fileExtension = url.pathExtension + + // ~/Library/Caches/org.carthage.CarthageKit/binaries/MyBinaryProjectFramework/2.3.1/MyBinaryProject.framework-578d2a1e3a62983f70dfd8d0b04531b77615cc381edd603813657372d40a8fa1.zip + return Constants.Dependency.assetsURL + .appendingPathComponent("\(dependency.name)/\(semanticVersion)/\(fileName)-\(hexDigest).\(fileExtension)") } /// Caches the downloaded binary at the given URL, moving it to the other URL @@ -1513,7 +1413,7 @@ private func filesInDirectory(_ directoryURL: URL, _ typeIdentifier: String? = n } /// Sends the platform specified in the given Info.plist. -func platformForFramework(_ frameworkURL: URL) -> SignalProducer { +func platformForFramework(_ frameworkURL: URL) -> SignalProducer { return SignalProducer(value: frameworkURL) // Neither DTPlatformName nor CFBundleSupportedPlatforms can not be used // because Xcode 6 and below do not include either in macOS frameworks. @@ -1573,17 +1473,18 @@ func platformForFramework(_ frameworkURL: URL) -> SignalProducer macosx .map { sdkName in sdkName.trimmingCharacters(in: CharacterSet.letters.inverted) } - .attemptMap { platform in SDK.from(string: platform).map { $0.platform } } + .map { SDK(name: $0, simulatorHeuristic: "") } } /// Sends the URL to each framework bundle found in the given directory. internal func frameworksInDirectory(_ directoryURL: URL) -> SignalProducer { return filesInDirectory(directoryURL, kUTTypeFramework as String) + .concat(filesInDirectory(directoryURL, "com.apple.xcframework")) .filter { !$0.pathComponents.contains("__MACOSX") } .filter { url in // Skip nested frameworks let frameworksInURL = url.pathComponents.filter { pathComponent in - return (pathComponent as NSString).pathExtension == "framework" + return ["framework", "xcframework"].contains((pathComponent as NSString).pathExtension) } return frameworksInURL.count == 1 }.filter { url in @@ -1593,7 +1494,7 @@ internal func frameworksInDirectory(_ directoryURL: URL) -> SignalProducer SignalProducer SignalProducer { return UUIDsForFramework(frameworkURL) @@ -1699,25 +1600,107 @@ public func cloneOrFetch( destinationURL: URL = Constants.Dependency.repositoriesURL, commitish: String? = nil ) -> SignalProducer<(ProjectEvent?, URL), CarthageError> { + let fileManager = FileManager.default + let repositoryURL = repositoryFileURL(for: dependency, baseURL: destinationURL) + return SignalProducer { Result(at: destinationURL, attempt: { - try FileManager.default.createDirectory(at: $0, withIntermediateDirectories: true) + try fileManager.createDirectory(at: $0, withIntermediateDirectories: true) + return dependency.gitURL(preferHTTPS: preferHTTPS)! }) } - .flatMap(.merge) { () -> SignalProducer<(ProjectEvent?, URL), CarthageError> in - let remoteURL = dependency.gitURL(preferHTTPS: preferHTTPS)! - let repositoryURL = repositoryFileURL(for: dependency, baseURL: destinationURL) - - return cloneOrFetch(remoteURL: remoteURL, to: repositoryURL, isBare: true, commitish: commitish) - .map { (cloneOrFetch) -> (ProjectEvent?, URL) in - switch cloneOrFetch { - case .none: - return (nil, repositoryURL) - case .some(.cloning): - return (.cloning(dependency), repositoryURL) - case .some(.fetching): - return (.fetching(dependency), repositoryURL) + .flatMap(.merge) { (remoteURL: GitURL) -> SignalProducer<(ProjectEvent?, URL), CarthageError> in + return isGitRepository(repositoryURL) + .flatMap(.merge) { isRepository -> SignalProducer<(ProjectEvent?, URL), CarthageError> in + if isRepository { + let fetchProducer: () -> SignalProducer<(ProjectEvent?, URL), CarthageError> = { + guard FetchCache.needsFetch(forURL: remoteURL) else { + return SignalProducer(value: (nil, repositoryURL)) + } + + return SignalProducer(value: (.fetching(dependency), repositoryURL)) + .concat( + fetchRepository(repositoryURL, remoteURL: remoteURL, refspec: "+refs/heads/*:refs/heads/*") + .then(SignalProducer<(ProjectEvent?, URL), CarthageError>.empty) + ) + } + + // If we've already cloned the repo, check for the revision, possibly skipping an unnecessary fetch + if let commitish = commitish { + return SignalProducer.zip( + branchExistsInRepository(repositoryURL, pattern: commitish), + commitExistsInRepository(repositoryURL, revision: commitish) + ) + .flatMap(.concat) { branchExists, commitExists -> SignalProducer<(ProjectEvent?, URL), CarthageError> in + // If the given commitish is a branch, we should fetch. + if branchExists || !commitExists { + return fetchProducer() + } else { + return SignalProducer(value: (nil, repositoryURL)) + } + } + } else { + return fetchProducer() + } + } else { + // Either the directory didn't exist or it did but wasn't a git repository + // (Could happen if the process is killed during a previous directory creation) + // So we remove it, then clone + _ = try? fileManager.removeItem(at: repositoryURL) + return SignalProducer(value: (.cloning(dependency), repositoryURL)) + .concat( + cloneRepository(remoteURL, repositoryURL) + .then(SignalProducer<(ProjectEvent?, URL), CarthageError>.empty) + ) } } } } + +private func binaryAssetPrioritization(forName assetName: String) -> (keyName: String, priority: UInt8) { + let priorities: KeyValuePairs = [".xcframework": 10 as UInt8, ".XCFramework": 10, ".XCframework": 10, ".framework": 40] + + for (pathExtension, priority) in priorities { + var (potentialPatternRange, keyName) = (assetName.range(of: pathExtension), assetName) + guard let patternRange = potentialPatternRange else { continue } + keyName.removeSubrange(patternRange) + return (keyName, priority) + } + + // If we can't tell whether this is a framework or an xcframework, return it with a low priority. + return (assetName, 70) +} + +/** +Given a list of known assets for a release, parses asset names to identify XCFramework assets, and returns which assets should be downloaded. + +For example: +``` +>>> binaryAssetFilter( + prioritizing: [Foo.xcframework.zip, Foo.framework.zip, Bar.framework.zip], + preferXCFrameworks: true + ) +[Foo.xcframework.zip, Bar.framework.zip] +``` +*/ +private func binaryAssetFilter(prioritizing assets: [A], preferXCFrameworks: Bool) -> [A] { + let bestPriorityAssetsByKey = assets.reduce(into: [:] as [String: [A: UInt8]]) { assetNames, asset in + if asset.name.lowercased().contains(".xcframework") && !preferXCFrameworks { + // Skip assets that look like xcframework when --use-xcframeworks is not passed. + return + } + let (key, priority) = binaryAssetPrioritization(forName: asset.name) + let assetPriorities = assetNames[key, default: [:]].merging([asset: priority], uniquingKeysWith: min) + let bestPriority = assetPriorities.values.min()! + assetNames[key] = assetPriorities.filter { $1 == bestPriority } + } + return bestPriorityAssetsByKey.values.flatMap { $0.keys } +} + +private protocol AssetNameConvertible: Hashable { + var name: String { get } +} +extension URL: AssetNameConvertible { + var name: String { return lastPathComponent } +} +extension Release.Asset: AssetNameConvertible {} diff --git a/Source/CarthageKit/Proxy.swift b/Source/CarthageKit/Proxy.swift index 0ca9dd1f2d..e92840f775 100644 --- a/Source/CarthageKit/Proxy.swift +++ b/Source/CarthageKit/Proxy.swift @@ -6,8 +6,9 @@ struct Proxy { init(environment: [String: String]) { let http = Proxy.makeHTTPDictionary(environment) let https = Proxy.makeHTTPSDictionary(environment) + let noProxy = Proxy.makeNoProxyDictionary(environment) - let combined = http.merging(https) { _, property in property } + let combined = http.merging(https) { _, property in property }.merging(noProxy) { _, property in property } // the proxy dictionary on URLSessionConfiguration must be nil so that it can default to the system proxy. connectionProxyDictionary = combined.isEmpty ? nil : combined @@ -46,8 +47,38 @@ struct Proxy { return dictionary } + + private static func makeNoProxyDictionary(_ environment: [String: String]) -> [AnyHashable: Any] { + #if os(OSX) + + let vars = ["no_proxy", "NO_PROXY"] + guard + let noProxyList: [String] = (vars.compactMap { environment[$0] }.first)?.split(separator: ",").compactMap({ String($0) }), + !noProxyList.isEmpty + else { return [:] } + + let dictionary: [AnyHashable: Any] = [ + kCFNetworkProxiesExceptionsList: noProxyList + ] + + return dictionary + + #else + return [:] + #endif + } + } extension Proxy { static let `default`: Proxy = Proxy(environment: ProcessInfo.processInfo.environment) } + +extension URLSession { + public static var proxiedSession: URLSession { + let configuration = URLSessionConfiguration.default + configuration.connectionProxyDictionary = Proxy.default.connectionProxyDictionary + + return URLSession(configuration: configuration) + } +} diff --git a/Source/CarthageKit/Simulator.swift b/Source/CarthageKit/Simulator.swift index 3bca928693..8f57e04619 100644 --- a/Source/CarthageKit/Simulator.swift +++ b/Source/CarthageKit/Simulator.swift @@ -43,9 +43,9 @@ internal func selectAvailableSimulator(of sdk: SDK, from data: Data) -> Simulato let devices = jsonObject["devices"] else { return nil } - let platformName = sdk.platform.rawValue + func reducePlatformNames(_ result: inout [String: [Simulator]], _ entry: (key: String, value: [Simulator])) { - guard let platformVersion = parsePlatformVersion(for: platformName, from: entry.key) else { return } + guard let platformVersion = parsePlatformVersion(for: sdk.simulatorJsonKeyUnderDevicesDictQuery, from: entry.key) else { return } guard entry.value.contains(where: { $0.isAvailable }) else { return } result[platformVersion] = entry.value } @@ -67,14 +67,57 @@ internal func selectAvailableSimulator(of sdk: SDK, from data: Data) -> Simulato } /// Parses a matching platform and version from a given identifier. +/// +/// - Warning: Comparing dissimilar — say, hyphenated against "iOS 9.0"-style — is _not_ +/// accomodated by chains with this function, (but generally not needed anyway.) internal func parsePlatformVersion(for platformName: String, from identifier: String) -> String? { - guard let platformRange = identifier.range(of: platformName) else { return nil } + let asciiDigitCharacterSet = CharacterSet.urlUserAllowed.intersection(CharacterSet.decimalDigits) + let badEndingCharacterSet = asciiDigitCharacterSet.union(CharacterSet(charactersIn: ".-")).inverted + + let øøø = identifier.suffix(from: identifier.lastIndex(of: " ") ?? identifier.endIndex).dropFirst() + if + øøø.unicodeScalars.firstIndex(where: badEndingCharacterSet.contains) == nil, + identifier.commonPrefix( + with: platformName.replacingOccurrences(of: " ", with: "-"), + options: .caseInsensitive + ).isEmpty == false /* btw, `øøø == ""` returns here too · ✓ */ + { + return [øøø.range(of: ".*", options: .regularExpression)!].reduce(into: identifier) { + $0.replaceSubrange($1, with: øøø) + } + } + + var suffix = identifier.suffix(from: identifier.lastIndex(of: ".") ?? identifier.endIndex).dropFirst() + + // let assumedSpaceReplacementCharacter = "-" // but, as of 2019 no simulator displayNames with spaces in the platform section exist + + let commonality = suffix.commonPrefix( + with: platformName.replacingOccurrences(of: " ", with: "-").appending("-"), + options: .caseInsensitive + ) - let nonDigitCharacters = CharacterSet.decimalDigits.inverted - let version = identifier - .suffix(from: platformRange.upperBound) - .split(whereSeparator: { $0.unicodeScalars.contains(where: { nonDigitCharacters.contains($0) }) }) + guard commonality.isEmpty == false else { return nil } + + guard [platformName, identifier].allSatisfy({ $0.contains("ß") == false }) else { return nil } + // 〜 that above character matches (when case insensitive) the two character string "SS" + // 〜 and therefore would nonunify `commonality`’s `endIndex` and where we begin to search for the version + + switch suffix.dropFirst(commonality.count) { + case "": + return nil + case let possibleVersion where possibleVersion.unicodeScalars.firstIndex(where: badEndingCharacterSet.contains) == nil: + suffix = possibleVersion + default: + return nil + } + + let version = suffix + .components(separatedBy: asciiDigitCharacterSet.inverted) + .filter { $0.isEmpty == false } .joined(separator: ".") - return "\(platformName) \(version)" + // possible for version to contain three+ «.» and four+ numeric components, + // but later SemanticVersioning parsing will negate those · ✓ + + return "\(String(commonality.dropLast(1))) \(version)" } diff --git a/Source/CarthageKit/VersionFile.swift b/Source/CarthageKit/VersionFile.swift index 806f2472d6..2fe5230170 100644 --- a/Source/CarthageKit/VersionFile.swift +++ b/Source/CarthageKit/VersionFile.swift @@ -8,6 +8,8 @@ import XCDBLD public struct CachedFramework: Codable { enum CodingKeys: String, CodingKey { case name = "name" + case container = "container" + case libraryIdentifier = "identifier" case hash = "hash" case linking = "linking" case swiftToolchainVersion = "swiftToolchainVersion" @@ -15,6 +17,8 @@ public struct CachedFramework: Codable { /// Name of the framework public let name: String + public let container: String? + public let libraryIdentifier: String? /// Hash of the framework public let hash: String /// The linking type of the framework. One of `dynamic` or `static`. Defaults to `dynamic` @@ -27,12 +31,21 @@ public struct CachedFramework: Codable { } /// The framework's expected location within a platform directory. - var relativePath: String { + func location(in buildDirectory: URL, sdk: SDK) -> URL { + if let container = container, let libraryIdentifier = libraryIdentifier { + return buildDirectory + .appendingPathComponent(container) + .appendingPathComponent(libraryIdentifier) + .appendingPathComponent("\(name).framework") + } + let platformDirectory = buildDirectory.appendingPathComponent(sdk.platformSimulatorlessFromHeuristic) switch linking { case .some(.static): - return "\(FrameworkType.staticFolderName)/\(name).framework" + return platformDirectory + .appendingPathComponent(FrameworkType.staticFolderName) + .appendingPathComponent("\(name).framework") default: - return "\(name).framework" + return platformDirectory.appendingPathComponent("\(name).framework") } } } @@ -61,19 +74,22 @@ public struct VersionFile: Codable { /// The extension representing a serialized VersionFile. static let pathExtension = "version" - subscript(_ platform: Platform) -> [CachedFramework]? { - switch platform { - case .macOS: + subscript(_ platform: SDK) -> [CachedFramework]? { + switch platform.platformSimulatorlessFromHeuristic { + case "Mac": return macOS - case .iOS: + case "iOS": return iOS - case .watchOS: + case "watchOS": return watchOS - case .tvOS: + case "tvOS": return tvOS + + default: + return nil } } @@ -124,13 +140,10 @@ public struct VersionFile: Codable { /// - binariesDirectoryURL: the binaries directory public func frameworkURL( for cachedFramework: CachedFramework, - platform: Platform, + platform: SDK, binariesDirectoryURL: URL ) -> URL { - return binariesDirectoryURL - .appendingPathComponent(platform.rawValue, isDirectory: true) - .resolvingSymlinksInPath() - .appendingPathComponent(cachedFramework.relativePath, isDirectory: true) + return cachedFramework.location(in: binariesDirectoryURL, sdk: platform) } /// Calculates the path of the binary inside the framework corresponding with a version file @@ -140,7 +153,7 @@ public struct VersionFile: Codable { /// - binariesDirectoryURL: the binaries directory public func frameworkBinaryURL( for cachedFramework: CachedFramework, - platform: Platform, + platform: SDK, binariesDirectoryURL: URL ) -> URL { return frameworkURL( @@ -155,7 +168,7 @@ public struct VersionFile: Codable { /// order that they were provided in. public func hashes( for cachedFrameworks: [CachedFramework], - platform: Platform, + platform: SDK, binariesDirectoryURL: URL ) -> SignalProducer { return SignalProducer(cachedFrameworks) @@ -184,7 +197,7 @@ public struct VersionFile: Codable { /// as they will be compatible with it by definition. public func swiftVersionMatches( for cachedFrameworks: [CachedFramework], - platform: Platform, + platform: SDK, binariesDirectoryURL: URL, localSwiftVersion: String ) -> SignalProducer { @@ -201,7 +214,7 @@ public struct VersionFile: Codable { } else { return frameworkSwiftVersion(frameworkURL) .map { swiftVersion -> Bool in - return swiftVersion == localSwiftVersion + return swiftVersion == localSwiftVersion || isModuleStableAPI(localSwiftVersion, swiftVersion, frameworkURL) } .flatMapError { _ in SignalProducer(value: false) } } @@ -210,7 +223,7 @@ public struct VersionFile: Codable { /// Check if the version file matches its values with the ones provided public func satisfies( - platform: Platform, + platform: SDK, commitish: String, binariesDirectoryURL: URL, localSwiftVersion: String @@ -246,7 +259,7 @@ public struct VersionFile: Codable { /// Check if the version file matches its values with the ones provided public func satisfies( - platform: Platform, + platform: SDK, commitish: String, hashes: [String?], swiftVersionMatches: [Bool] @@ -301,7 +314,7 @@ public struct VersionFile: Codable { /// /// Returns a signal that succeeds once the file has been created. public func createVersionFileForCurrentProject( - platforms: Set, + platforms: Set?, buildProducts: [URL], rootDirectoryURL: URL ) -> SignalProducer<(), CarthageError> { @@ -396,7 +409,7 @@ public func createVersionFileForCurrentProject( public func createVersionFile( for dependency: Dependency, version: PinnedVersion, - platforms: Set, + platforms: Set?, buildProducts: [URL], rootDirectoryURL: URL ) -> SignalProducer<(), CarthageError> { @@ -422,12 +435,22 @@ private func createVersionFile( let versionFileURL = rootBinariesURL .appendingPathComponent(".\(dependencyName).\(VersionFile.pathExtension)") + let knownIn2019YearSDK: (String) -> String = { prefix in + SDK.knownIn2019YearSDKs + .first(where: { sdk in sdk.rawValue.hasPrefix(prefix) } )! + .platformSimulatorlessFromHeuristic + } + + let sortedFrameworks: ([CachedFramework]?) -> [CachedFramework]? = { + $0?.sorted { $0.name < $1.name } + } + let versionFile = VersionFile( commitish: commitish, - macOS: platformCaches[Platform.macOS.rawValue], - iOS: platformCaches[Platform.iOS.rawValue], - watchOS: platformCaches[Platform.watchOS.rawValue], - tvOS: platformCaches[Platform.tvOS.rawValue]) + macOS: sortedFrameworks(platformCaches[knownIn2019YearSDK("mac")]), + iOS: sortedFrameworks(platformCaches[knownIn2019YearSDK("iphoneos")]), + watchOS: sortedFrameworks(platformCaches[knownIn2019YearSDK("watchos")]), + tvOS: sortedFrameworks(platformCaches[knownIn2019YearSDK("appletvos")])) return versionFile.write(to: versionFileURL) } @@ -442,66 +465,86 @@ private func createVersionFile( public func createVersionFileForCommitish( _ commitish: String, dependencyName: String, - platforms: Set = Set(Platform.supportedPlatforms), + platforms: Set? = nil, buildProducts: [URL], rootDirectoryURL: URL ) -> SignalProducer<(), CarthageError> { var platformCaches: [String: [CachedFramework]] = [:] - let platformsToCache = platforms.isEmpty ? Set(Platform.supportedPlatforms) : platforms + let platformsToCache = (platforms ?? SDK.knownIn2019YearSDKs).intersection(SDK.knownIn2019YearSDKs) + for platform in platformsToCache { - platformCaches[platform.rawValue] = [] + platformCaches[platform.platformSimulatorlessFromHeuristic] = [] } struct FrameworkDetail { - let platformName: String let frameworkName: String + let frameworkLocator: FrameworkLocator let frameworkSwiftVersion: String? - let frameworkType: FrameworkType + } + enum FrameworkLocator { + case xcframework(name: String, libraryIdentifier: String) + case platformDirectory(name: String, linking: FrameworkType) } if !buildProducts.isEmpty { return SignalProducer(buildProducts) - .flatMap(.merge) { url -> SignalProducer<(String, FrameworkDetail), CarthageError> in + .skipRepeats() + .flatMap(.merge, { url -> SignalProducer<(URL, URL), CarthageError> in + return frameworkBundlesInURL(url) + .map { ($0.bundleURL, url) } + .flatMapError { _ in .empty } + }) + .flatMap(.merge) { url, containerURL -> SignalProducer<(String, FrameworkDetail), CarthageError> in let frameworkName: String - let platformName: String - let frameworkType: FrameworkType + let frameworkLocator: FrameworkLocator switch ( url.deletingLastPathComponent().deletingLastPathComponent().lastPathComponent, url.deletingLastPathComponent().lastPathComponent, url.deletingPathExtension().lastPathComponent ) { + case (containerURL.lastPathComponent, let libraryIdentifier, let name): + frameworkName = name + frameworkLocator = .xcframework(name: containerURL.lastPathComponent, libraryIdentifier: libraryIdentifier) case (let platform, FrameworkType.staticFolderName, let name): frameworkName = name - platformName = platform - frameworkType = .static + frameworkLocator = .platformDirectory(name: platform, linking: .static) case (_, let platform, let name): frameworkName = name - platformName = platform - frameworkType = .dynamic + frameworkLocator = .platformDirectory(name: platform, linking: .dynamic) } return frameworkSwiftVersionIfIsSwiftFramework(url) .mapError { swiftVersionError -> CarthageError in .unknownFrameworkSwiftVersion(swiftVersionError.description) } .flatMap(.merge) { frameworkSwiftVersion -> SignalProducer<(String, FrameworkDetail), CarthageError> in - let frameworkDetail: FrameworkDetail = .init(platformName: platformName, - frameworkName: frameworkName, - frameworkSwiftVersion: frameworkSwiftVersion, - frameworkType: frameworkType) - let details = SignalProducer(value: frameworkDetail) - let binaryURL = url.appendingPathComponent(frameworkName, isDirectory: false) - return SignalProducer.zip(hashForFileAtURL(binaryURL), details) + let frameworkDetail = FrameworkDetail( + frameworkName: frameworkName, + frameworkLocator: frameworkLocator, + frameworkSwiftVersion: frameworkSwiftVersion + ) + let details = SignalProducer(value: frameworkDetail) + let binaryURL = url.appendingPathComponent(frameworkName, isDirectory: false) + return SignalProducer.zip(hashForFileAtURL(binaryURL), details) } } .reduce(into: platformCaches) { (platformCaches: inout [String: [CachedFramework]], values: (String, FrameworkDetail)) in let hash = values.0 - let platformName = values.1.platformName let frameworkName = values.1.frameworkName let frameworkSwiftVersion = values.1.frameworkSwiftVersion - let frameworkType = values.1.frameworkType - let cachedFramework = CachedFramework(name: frameworkName, hash: hash, linking: frameworkType, swiftToolchainVersion: frameworkSwiftVersion) - if var frameworks = platformCaches[platformName] { + let cachedFramework: CachedFramework + let platformName: String? + + switch values.1.frameworkLocator { + case .platformDirectory(name: let name, linking: let linking): + platformName = name + cachedFramework = CachedFramework(name: frameworkName, container: nil, libraryIdentifier: nil, hash: hash, linking: linking, swiftToolchainVersion: frameworkSwiftVersion) + case .xcframework(name: let container, libraryIdentifier: let identifier): + let targetOS = identifier.components(separatedBy: "-")[0] + platformName = SDK.associatedSetOfKnownIn2023YearSDKs(targetOS).first?.platformSimulatorlessFromHeuristic + cachedFramework = CachedFramework(name: frameworkName, container: container, libraryIdentifier: identifier, hash: hash, linking: nil, swiftToolchainVersion: frameworkSwiftVersion) + } + if let platformName = platformName, var frameworks = platformCaches[platformName] { frameworks.append(cachedFramework) platformCaches[platformName] = frameworks } @@ -537,7 +580,7 @@ public func createVersionFileForCommitish( public func versionFileMatches( _ dependency: Dependency, version: PinnedVersion, - platforms: Set, + platforms: Set?, rootDirectoryURL: URL, toolchain: String? ) -> SignalProducer { @@ -548,7 +591,7 @@ public func versionFileMatches( let commitish = version.commitish - let platformsToCheck = platforms.isEmpty ? Set(Platform.supportedPlatforms) : platforms + let platformsToCheck = (platforms ?? SDK.knownIn2019YearSDKs).intersection(SDK.knownIn2019YearSDKs) let rootBinariesURL = rootDirectoryURL .appendingPathComponent(Constants.binariesFolderPath, isDirectory: true) @@ -557,7 +600,7 @@ public func versionFileMatches( return swiftVersion(usingToolchain: toolchain) .mapError { error in CarthageError.internalError(description: error.description) } .flatMap(.concat) { localSwiftVersion in - return SignalProducer(platformsToCheck) + return SignalProducer(platformsToCheck) .flatMap(.merge) { platform in return versionFile.satisfies( platform: platform, diff --git a/Source/CarthageKit/XCDBLDExtensions.swift b/Source/CarthageKit/XCDBLDExtensions.swift index c29770c2ff..2783aa9a6a 100644 --- a/Source/CarthageKit/XCDBLDExtensions.swift +++ b/Source/CarthageKit/XCDBLDExtensions.swift @@ -11,18 +11,18 @@ extension MachOType { } } -extension Platform { +extension SDK { /// The relative path at which binaries corresponding to this platform will /// be stored. public var relativePath: String { - let subfolderName = rawValue + let subfolderName = self.platformSimulatorlessFromHeuristic return (Constants.binariesFolderPath as NSString).appendingPathComponent(subfolderName) } /// The relative URL at which binaries corresponding to this platform will /// be stored. public var relativeURL: URL? { - let subfolderName = rawValue + let subfolderName = self.platformSimulatorlessFromHeuristic return URL(string: Constants.binariesFolderPath)?.appendingPathComponent(subfolderName, isDirectory: true) } } @@ -41,10 +41,15 @@ extension ProjectLocator { .flatMap(.merge) { directoriesToSkip -> SignalProducer in return FileManager.default.reactive .enumerator(at: directoryURL.resolvingSymlinksInPath(), includingPropertiesForKeys: [ .typeIdentifierKey ], options: enumerationOptions, catchErrors: true) - .map { _, url in url } - .filter { url in - return !directoriesToSkip.contains { $0.hasSubdirectory(url) } + .filter { enumerator, url in + if directoriesToSkip.contains(where: { $0.hasSubdirectory(url) }) { + enumerator.skipDescendants() + return false + } else { + return true + } } + .map { _, url in url } } .filterMap { url -> ProjectLocator? in if let uti = url.typeIdentifier.value { @@ -101,11 +106,6 @@ extension ProjectLocator { } extension SDK { - /// Attempts to parse an SDK name from a string returned from `xcodebuild`. - public static func from(string: String) -> Result { - return Result(self.init(rawValue: string.lowercased()), failWith: .parseError(description: "unexpected SDK key \"\(string)\"")) - } - /// Split the given SDKs into simulator ones and device ones. internal static func splitSDKs(_ sdks: S) -> (simulators: [SDK], devices: [SDK]) where S.Iterator.Element == SDK { return ( diff --git a/Source/CarthageKit/Xcode.swift b/Source/CarthageKit/Xcode.swift index 2467db1da4..4fd0be4238 100644 --- a/Source/CarthageKit/Xcode.swift +++ b/Source/CarthageKit/Xcode.swift @@ -62,7 +62,11 @@ internal func frameworkSwiftVersionIfIsSwiftFramework(_ frameworkURL: URL) -> Si internal func frameworkSwiftVersion(_ frameworkURL: URL) -> SignalProducer { // Fall back to dSYM version parsing if header is not present guard let swiftHeaderURL = frameworkURL.swiftHeaderURL() else { - return dSYMSwiftVersion(frameworkURL.appendingPathExtension("dSYM")) + let dSYMInXCFramework = frameworkURL.deletingLastPathComponent().appendingPathComponent("dSYMs") + .appendingPathComponent("\(frameworkURL.lastPathComponent).dSYM") + let dSYMInBuildFolder = frameworkURL.appendingPathExtension("dSYM") + return dSYMSwiftVersion(dSYMInXCFramework) + .flatMapError { _ in dSYMSwiftVersion(dSYMInBuildFolder) } } guard @@ -78,7 +82,10 @@ internal func frameworkSwiftVersion(_ frameworkURL: URL) -> SignalProducer SignalProducer { // Pick one architecture - guard let arch = architecturesInPackage(dSYMURL).first()?.value else { + guard let arch = architecturesInPackage( + dSYMURL, + xcrunQuery: ["lipo", "-info"] + ).flatten().first()?.value else { return SignalProducer(error: .unknownFrameworkSwiftVersion(message: "No architectures found in dSYM.")) } @@ -130,12 +137,12 @@ internal func isSwiftFramework(_ frameworkURL: URL) -> Bool { return frameworkURL.swiftmoduleURL() != nil } -/// Emits the framework URL if it matches the local Swift version and errors if not. -internal func checkSwiftFrameworkCompatibility(_ frameworkURL: URL, usingToolchain toolchain: String?) -> SignalProducer { +/// Completes if the framework URL matches the local Swift version and errors if it does not. +internal func checkSwiftFrameworkCompatibility(_ frameworkURL: URL, usingToolchain toolchain: String?) -> SignalProducer { return SignalProducer.combineLatest(swiftVersion(usingToolchain: toolchain), frameworkSwiftVersion(frameworkURL)) .attemptMap { localSwiftVersion, frameworkSwiftVersion in return localSwiftVersion == frameworkSwiftVersion || isModuleStableAPI(localSwiftVersion, frameworkSwiftVersion, frameworkURL) - ? .success(frameworkURL) + ? .success(()) : .failure(.incompatibleFrameworkSwiftVersions(local: localSwiftVersion, framework: frameworkSwiftVersion)) } } @@ -164,24 +171,30 @@ private func determineMajorMinorVersion(_ swiftVersion: String) -> Double? { return Double(swiftVersion[range]) } -/// Emits the framework URL if it is compatible with the build environment and errors if not. -internal func checkFrameworkCompatibility(_ frameworkURL: URL, usingToolchain toolchain: String?) -> SignalProducer { - if isSwiftFramework(frameworkURL) { - return checkSwiftFrameworkCompatibility(frameworkURL, usingToolchain: toolchain) - } else { - return SignalProducer(value: frameworkURL) - } +/// Completes if the framework URL if it is compatible with the build environment and errors if not. +internal func checkFrameworkCompatibility(_ frameworkURL: URL, usingToolchain toolchain: String?) -> SignalProducer { + return frameworkBundlesInURL(frameworkURL) + .mapError { CarthageError.readFailed(frameworkURL, $0 as NSError) } + .flatMap(.concat) { bundle -> SignalProducer in + if isSwiftFramework(bundle.bundleURL) { + return checkSwiftFrameworkCompatibility(bundle.bundleURL, usingToolchain: toolchain) + .mapError { .internalError(description: $0.description) } + } else { + return .empty + } + } + } /// Creates a task description for executing `xcodebuild` with the given /// arguments. -public func xcodebuildTask(_ tasks: [String], _ buildArguments: BuildArguments) -> Task { - return Task("/usr/bin/xcrun", arguments: buildArguments.arguments + tasks) +public func xcodebuildTask(_ tasks: [String], _ buildArguments: BuildArguments, environment: [String: String]? = nil) -> Task { + return Task("/usr/bin/xcrun", arguments: buildArguments.arguments + tasks, environment: environment) } /// Creates a task description for executing `xcodebuild` with the given /// arguments. -public func xcodebuildTask(_ task: String, _ buildArguments: BuildArguments) -> Task { +public func xcodebuildTask(_ task: String, _ buildArguments: BuildArguments, environment: [String: String]? = nil) -> Task { return xcodebuildTask([task], buildArguments) } @@ -190,12 +203,12 @@ public func xcodebuildTask(_ task: String, _ buildArguments: BuildArguments) -> public func buildableSchemesInDirectory( // swiftlint:disable:this function_body_length _ directoryURL: URL, withConfiguration configuration: String, - forPlatforms platforms: Set = [] + forPlatforms platformAllowList: Set? = nil ) -> SignalProducer<(Scheme, ProjectLocator), CarthageError> { precondition(directoryURL.isFileURL) let locator = ProjectLocator .locate(in: directoryURL) - .flatMap(.concat) { project -> SignalProducer<(ProjectLocator, [Scheme]), CarthageError> in + .flatMap(.concurrent(limit: 4)) { project -> SignalProducer<(ProjectLocator, [Scheme]), CarthageError> in return project .schemes() .collect() @@ -222,7 +235,7 @@ public func buildableSchemesInDirectory( // swiftlint:disable:this function_body /// from a workspace, then it might include additional targets that would trigger our /// check. let buildArguments = BuildArguments(project: project, scheme: scheme, configuration: configuration) - return shouldBuildScheme(buildArguments, platforms) + return shouldBuildScheme(buildArguments, platformAllowList) .filter { $0 } .map { _ in (scheme, project) } } @@ -236,7 +249,7 @@ public func buildableSchemesInDirectory( // swiftlint:disable:this function_body switch project { case .workspace where schemes.contains(scheme): let buildArguments = BuildArguments(project: project, scheme: scheme, configuration: configuration) - return shouldBuildScheme(buildArguments, platforms) + return shouldBuildScheme(buildArguments, platformAllowList) .filter { $0 } .map { _ in project } @@ -255,7 +268,7 @@ public func buildableSchemesInDirectory( // swiftlint:disable:this function_body if !schemes.isEmpty { return .init(schemes) } else { - return .init(error: .noSharedFrameworkSchemes(.git(GitURL(directoryURL.path)), platforms)) + return .init(error: .noSharedFrameworkSchemes(.git(GitURL(directoryURL.path)), platformAllowList ?? [])) } } } @@ -317,6 +330,9 @@ internal enum PackageType: String { /// A .framework package. case framework = "FMWK" + /// A .xcframework package + case xcframework = "XFWK" + /// A .bundle package. Some frameworks might have this package type code /// (e.g. https://github.com/ResearchKit/ResearchKit/blob/1.3.0/ResearchKit/Info.plist#L15-L16). case bundle = "BNDL" @@ -453,29 +469,24 @@ private func shouldBuildFrameworkType(_ frameworkType: FrameworkType?) -> Bool { } /// Determines whether the given scheme should be built automatically. -private func shouldBuildScheme(_ buildArguments: BuildArguments, _ forPlatforms: Set) -> SignalProducer { +private func shouldBuildScheme( + _ buildArguments: BuildArguments, + _ platformAllowList: Set? = nil +) -> SignalProducer { precondition(buildArguments.scheme != nil) - return BuildSettings.load(with: buildArguments) - .flatMap(.concat) { settings -> SignalProducer in - let frameworkType = SignalProducer(result: settings.frameworkType) + let setSignal = SDK.setsFromJSONShowSDKsWithFallbacks.promoteError(CarthageError.self) - if forPlatforms.isEmpty { - return frameworkType - .flatMapError { _ in .empty } - } else { - return settings.buildSDKs - .filter { forPlatforms.contains($0.platform) } - .flatMap(.merge) { _ in frameworkType } - .flatMapError { _ in .empty } - } + return BuildSettings.load(with: buildArguments) + .flatMap(.merge) { (settings) -> SignalProducer, CarthageError> in + let supportedFrameworks = settings.buildSDKRawNames.map { sdk in SDK(name: sdk, simulatorHeuristic: "") } + guard settings.frameworkType.recover(nil) != nil else { return .empty } + return setSignal.map { $0.intersection(supportedFrameworks) } + } + .reduce(into: false) { + let filter = (platformAllowList ?? $1).contains + $0 = $0 || $1.firstIndex(where: filter) != nil } - .filter(shouldBuildFrameworkType) - // If we find any framework target, we should indeed build this scheme. - .map { _ in true } - // Otherwise, nope. - .concat(value: false) - .take(first: 1) } /// Aggregates all of the build settings sent on the given signal, associating @@ -532,6 +543,9 @@ private func mergeBuildProducts( simulatorBuildSettings: BuildSettings, into destinationFolderURL: URL ) -> SignalProducer { + let commonArchitectures = deviceBuildSettings.archs.fanout(simulatorBuildSettings.archs).map { deviceArchs, simulatorArchs in + deviceArchs.intersection(simulatorArchs) + } return copyBuildProductIntoDirectory(destinationFolderURL, deviceBuildSettings) .flatMap(.merge) { productURL -> SignalProducer in let executableURLs = (deviceBuildSettings.executableURL.fanout(simulatorBuildSettings.executableURL)).map { [ $0, $1 ] } @@ -579,10 +593,75 @@ private func mergeBuildProducts( .then(copyBCSymbolMapsForBuildProductIntoDirectory(destinationFolderURL, simulatorBuildSettings)) .then(SignalProducer(value: productURL)) } + .mapError { error -> CarthageError in + if case .taskError(let taskError) = error, + let commonArchitectures = commonArchitectures.value, + let productName = deviceBuildSettings.productName.value { + return .xcframeworkRequired(.init(productName: productName, commonArchitectures: commonArchitectures, underlyingError: taskError)) + } else { + return error + } + } +} + +/// Extracts the built product and debug information from a build described by `settings` and adds it to an xcframework +/// in `directoryURL`. Sends the xcframework's URL when complete. +private func mergeIntoXCFramework(in directoryURL: URL, settings: BuildSettings) -> SignalProducer { + let xcframework = SignalProducer(result: settings.productName).map { productName in + directoryURL.appendingPathComponent(productName).appendingPathExtension("xcframework") + } + let framework = SignalProducer(result: settings.wrapperURL.map({ $0.resolvingSymlinksInPath() })) + + let buildDSYMs = SignalProducer(result: settings.wrapperURL) + .filter { _ in settings.machOType.value != .staticlib } + .flatMap(.concat, createDebugInformation) + .ignoreTaskData() + .map({ $0 }) + let buildSymbolMaps = SignalProducer(result: settings.wrapperURL) + .filter { _ in settings.bitcodeEnabled.value == true } + .flatMap(.concat, BCSymbolMapsForFramework) + .filter({ (try? $0.checkResourceIsReachable()) ?? false }) + .map({ $0 }) + let buildDebugSymbols = buildDSYMs.concat(buildSymbolMaps).collect() + let platformName = SignalProducer(result: settings.platformTripleOS) + let fileManager = FileManager.default + let createTemporaryDirectory = fileManager.reactive.createTemporaryDirectoryWithTemplate("carthage-xcframework-XXXXXX") + + return SignalProducer.combineLatest( + framework, + buildDebugSymbols, + platformName, + xcframework, + createTemporaryDirectory + ).flatMap(.concat) { frameworkURL, debugSymbols, platformName, xcframeworkURL, temporaryDirectory -> SignalProducer in + let outputURL = temporaryDirectory.appendingPathComponent(xcframeworkURL.lastPathComponent) + + return mergeIntoXCFramework( + xcframeworkURL, + framework: frameworkURL, + debugSymbols: debugSymbols, + platformName: platformName, + variant: settings.platformTripleVariant.value, + outputURL: outputURL + ) + .mapError(CarthageError.taskError) + .attempt { replacementURL in + return Result(at: xcframeworkURL) { url in + if fileManager.fileExists(atPath: url.path) { + try fileManager.removeItem(at: url) + } + try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) + try fileManager.copyItem(at: replacementURL, to: url) + }.flatMap { _ in + Result(at: temporaryDirectory) { try fileManager.removeItem(at: $0) } + } + } + .then(SignalProducer(value: xcframeworkURL)) + } } /// A callback function used to determine whether or not an SDK should be built -public typealias SDKFilterCallback = (_ sdks: [SDK], _ scheme: Scheme, _ configuration: String, _ project: ProjectLocator) -> Result<[SDK], CarthageError> +public typealias SDKFilterCallback = (_ sdk: Set, _ scheme: Scheme, _ configuration: String, _ project: ProjectLocator) -> Result, CarthageError> /// Builds one scheme of the given project, for all supported SDKs. /// @@ -597,7 +676,6 @@ public func buildScheme( // swiftlint:disable:this function_body_length cyclomat sdkFilter: @escaping SDKFilterCallback = { sdks, _, _, _ in .success(sdks) } ) -> SignalProducer, CarthageError> { precondition(workingDirectoryURL.isFileURL) - let buildArgs = BuildArguments( project: project, scheme: scheme, @@ -605,55 +683,40 @@ public func buildScheme( // swiftlint:disable:this function_body_length cyclomat derivedDataPath: options.derivedDataPath, toolchain: options.toolchain ) + let buildURL = rootDirectoryURL.appendingPathComponent(Constants.binariesFolderPath) return BuildSettings.SDKsForScheme(scheme, inProject: project) .flatMap(.concat) { sdk -> SignalProducer in - var argsForLoading = buildArgs - argsForLoading.sdk = sdk - - return BuildSettings - .load(with: argsForLoading) - .filter { settings in - // Filter out SDKs that require bitcode when bitcode is disabled in - // project settings. This is necessary for testing frameworks, which - // must add a User-Defined setting of ENABLE_BITCODE=NO. - return settings.bitcodeEnabled.value == true || ![.tvOS, .watchOS].contains(sdk) - } - .map { _ in sdk } - } - .reduce(into: [:]) { (sdksByPlatform: inout [Platform: Set], sdk: SDK) in - let platform = sdk.platform - - if var sdks = sdksByPlatform[platform] { - sdks.insert(sdk) - sdksByPlatform.updateValue(sdks, forKey: platform) - } else { - sdksByPlatform[platform] = [sdk] - } - } - .flatMap(.concat) { sdksByPlatform -> SignalProducer<(Platform, [SDK]), CarthageError> in - if sdksByPlatform.isEmpty { - fatalError("No SDKs found for scheme \(scheme)") - } - - let values = sdksByPlatform.map { ($0, Array($1)) } - return SignalProducer(values) - } - .flatMap(.concat) { platform, sdks -> SignalProducer<(Platform, [SDK]), CarthageError> in - let filterResult = sdkFilter(sdks, scheme, options.configuration, project) - return SignalProducer(result: filterResult.map { (platform, $0) }) + return BuildSettings.denySDKUnderXcodesSub14IfWatchOSOrTVOSAndProjectsDoesNotSpecifySatisfactoryBitcodeSetting(sdk, under: buildArgs) + .filterMap { $1 ? $0 as Optional : nil } } - .filter { _, sdks in - return !sdks.isEmpty + .reduce(into: [] as Set) { $0.formUnion([$1]) } + .flatMap(.concat) { sdks -> SignalProducer<(String, [SDK]), CarthageError> in + if sdks.isEmpty { fatalError("No SDKs found for scheme \(scheme)") } + // fatalError in unlikely case that propogated logic error from Carthage authors + // has become unrecoverable + + return sdkFilter(sdks, scheme, options.configuration, project).analysis( + ifSuccess: { filteredSDKs in + SignalProducer<(String, [SDK]), CarthageError>( + Dictionary(grouping: filteredSDKs, by: { $0.relativePath }).lazy.map { ($0, $1) } + ) + }, ifFailure: { error in + SignalProducer<(String, [SDK]), CarthageError>(error: error) + }) } - .flatMap(.concat) { platform, sdks -> SignalProducer, CarthageError> in - let folderURL = rootDirectoryURL.appendingPathComponent(platform.relativePath, isDirectory: true).resolvingSymlinksInPath() + .flatMap(.concat) { relativePath, sdks -> SignalProducer, CarthageError> in + let folderURL = rootDirectoryURL.appendingPathComponent(relativePath, isDirectory: true).resolvingSymlinksInPath() switch sdks.count { case 1: return build(sdk: sdks[0], with: buildArgs, in: workingDirectoryURL) .flatMapTaskEvents(.merge) { settings in - return copyBuildProductIntoDirectory(settings.productDestinationPath(in: folderURL), settings) + if options.useXCFrameworks { + return mergeIntoXCFramework(in: buildURL, settings: settings) + } else { + return copyBuildProductIntoDirectory(settings.productDestinationPath(in: folderURL), settings) + } } case 2: @@ -678,7 +741,11 @@ public func buildScheme( // swiftlint:disable:this function_body_length cyclomat return SignalProducer(value: .standardError(data)) case let .success(deviceSettingsByTarget): - return settingsByTarget(build(sdk: simulatorSDK, with: buildArgs, in: workingDirectoryURL)) + return settingsByTarget( + build(sdk: simulatorSDK, + with: buildArgs, + in: workingDirectoryURL) + ) .flatMapTaskEvents(.concat) { (simulatorSettingsByTarget: [String: BuildSettings]) -> SignalProducer<(BuildSettings, BuildSettings), CarthageError> in assert( deviceSettingsByTarget.count == simulatorSettingsByTarget.count, @@ -704,11 +771,16 @@ public func buildScheme( // swiftlint:disable:this function_body_length cyclomat } } .flatMapTaskEvents(.concat) { deviceSettings, simulatorSettings in - return mergeBuildProducts( - deviceBuildSettings: deviceSettings, - simulatorBuildSettings: simulatorSettings, - into: deviceSettings.productDestinationPath(in: folderURL) - ) + if options.useXCFrameworks { + return mergeIntoXCFramework(in: buildURL, settings: deviceSettings) + .concat(mergeIntoXCFramework(in: buildURL, settings: simulatorSettings)) + } else { + return mergeBuildProducts( + deviceBuildSettings: deviceSettings, + simulatorBuildSettings: simulatorSettings, + into: deviceSettings.productDestinationPath(in: folderURL) + ) + } } default: @@ -716,6 +788,10 @@ public func buildScheme( // swiftlint:disable:this function_body_length cyclomat } } .flatMapTaskEvents(.concat) { builtProductURL -> SignalProducer in + guard !options.useXCFrameworks else { + // XCFrameworks have debug information embedded in them after being merged. + return SignalProducer(value: builtProductURL) + } return UUIDsForFramework(builtProductURL) // Only attempt to create debug info if there is at least // one dSYM architecture UUID in the framework. This can @@ -747,14 +823,82 @@ private func resolveSameTargetName(for settings: BuildSettings) -> SignalProduce } } +/// Using target architecture information in `settings`, copy platform-specific framework bundles from all +/// xcframeworks in `buildDirectory` to a temporary directory. +/// +/// The extracted frameworks are used to support building projects which are not configured to use XCFramework products +/// from their Carthage/Build directory. +/// +/// Sends the temporary directory or `nil` if there are no xcframeworks to extract. The directory is _not_ deleted +/// upon disposal, so that asynchronous build actions can use extracted frameworks after the producer has completed. +func extractXCFrameworks(in buildDirectory: URL, for settings: BuildSettings) -> SignalProducer { + let isRelativeToBuildDirectory = { (url: URL) -> Bool in + // `url` might not exist (e.g. it's "Carthage/Build/iOS" and an "iOS" directory was never created). In order + // for resolvingSymlinksInPath to resolve as much of the path as it can, reduce it to the nearest extant ancestor. + var extantURL = url + while (try? extantURL.checkResourceIsReachable()) == nil { + extantURL.deleteLastPathComponent() + } + return extantURL.resolvingSymlinksInPath().path.starts(with: buildDirectory.resolvingSymlinksInPath().path) + } + guard let platformTripleOS = settings.platformTripleOS.value, + let frameworkSearchPaths = settings.frameworkSearchPaths.value, + frameworkSearchPaths.contains(where: isRelativeToBuildDirectory) else { + // Skip extracting xcframeworks if this project doesn't declare its OS triple, or if it doesn't link + // against any frameworks in Carthage/Build. + return SignalProducer(value: nil) + } + + let findFrameworks = SignalProducer<[URL]?, CarthageError> { + try? FileManager.default.contentsOfDirectory(at: buildDirectory.resolvingSymlinksInPath(), includingPropertiesForKeys: nil) + } + .skipNil() + .flatten() + .filter { $0.pathExtension == "xcframework" } + .flatMap(.merge) { url -> SignalProducer in + frameworkBundlesInURL(url, compatibleWith: platformTripleOS, variant: settings.platformTripleVariant.value) + .mapError { .readFailed(url, $0 as NSError) } + .map { $0.bundleURL } + } + + let makeTemporaryDirectory = SignalProducer { () -> Result in + var templatePath = (NSTemporaryDirectory() as NSString).appendingPathComponent("carthage-xcframework-XXXX").utf8CString + let result = templatePath.withUnsafeMutableBufferPointer({ mkdtemp($0.baseAddress) }) + let temporaryURL = URL( + fileURLWithPath: templatePath.withUnsafeBufferPointer { String(validatingUTF8: $0.baseAddress!)! } + ) + guard result != nil else { + return .failure(.writeFailed(temporaryURL, NSError(domain: NSPOSIXErrorDomain, code: Int(errno)))) + } + return .success(temporaryURL) + } + + // Copy frameworks into the temporary directory. Send its URL once if _any_ frameworks were copied, or `nil` if + // no matching frameworks were found. + return makeTemporaryDirectory.flatMap(.concat) { temporaryURL -> SignalProducer in + findFrameworks.attempt { url in + let destination = temporaryURL.appendingPathComponent(url.lastPathComponent) + return Result(at: destination) { try FileManager.default.copyItem(at: url, to: $0) } + }.map { _ in temporaryURL } + } + .concat(value: nil) + .collect() + .map { $0.first! } +} + /// Runs the build for a given sdk and build arguments, optionally performing a clean first // swiftlint:disable:next function_body_length -private func build(sdk: SDK, with buildArgs: BuildArguments, in workingDirectoryURL: URL) -> SignalProducer, CarthageError> { +private func build( + sdk: SDK, + with buildArgs: BuildArguments, + in workingDirectoryURL: URL +) -> SignalProducer, CarthageError> { + var argsForLoading = buildArgs argsForLoading.sdk = sdk + argsForLoading.onlyActiveArchitecture = false var argsForBuilding = argsForLoading - argsForBuilding.onlyActiveArchitecture = false // If SDK is the iOS simulator, then also find and set a valid destination. // This fixes problems when the project deployment version is lower than @@ -776,10 +920,10 @@ private func build(sdk: SDK, with buildArgs: BuildArguments, in workingDirectory if let selectedSimulator = selectAvailableSimulator(of: sdk, from: data) { return .init(value: selectedSimulator) } else { - return .init(error: CarthageError.noAvailableSimulators(platformName: sdk.platform.rawValue)) + return .init(error: CarthageError.noAvailableSimulators(platformName: sdk.platformSimulatorlessFromHeuristic)) } } - .map { "platform=\(sdk.platform.rawValue) Simulator,id=\($0.udid.uuidString)" } + .map { "platform=\(sdk.platformSimulatorlessFromHeuristic) Simulator,id=\($0.udid.uuidString)" } } return SignalProducer(value: nil) } @@ -813,10 +957,24 @@ private func build(sdk: SDK, with buildArgs: BuildArguments, in workingDirectory } .flatMap(.concat) { settings in resolveSameTargetName(for: settings) } .collect() - .flatMap(.concat) { settings -> SignalProducer, CarthageError> in + .flatMap(.concat) { settings -> SignalProducer<([BuildSettings], URL?), CarthageError> in + // Use the build settings of an arbitrary target to extract platform-specific frameworks from any xcframeworks. + // Theoretically, different targets in the scheme could map to different LLVM targets, but it's hard to + // imagine how that would work since they are all building to the same destination. + guard let firstTargetSettings = settings.first else { return .empty } + let buildDirectoryURL = workingDirectoryURL.appendingPathComponent(Constants.binariesFolderPath) + return extractXCFrameworks(in: buildDirectoryURL, for: firstTargetSettings).map { (settings, $0) } + } + .flatMap(.concat) { settings, extractedXCFrameworksDir -> SignalProducer, CarthageError> in let actions: [String] = { var result: [String] = [xcodebuildAction.rawValue] + if settings.contains(where: { UInt64($0["XCODE_VERSION_ACTUAL"].recover("")) ?? 0 >= 1230 }) { + // Fixes Xcode 12.3 refusing to link against fat binaries + // "Building for iOS Simulator, but the linked and embedded framework 'REDACTED.framework' was built for iOS + iOS Simulator." + result += [ "VALIDATE_WORKSPACE=NO" ] + } + if xcodebuildAction == .archive { result += [ // Prevent generating unnecessary empty `.xcarchive` @@ -844,6 +1002,15 @@ private func build(sdk: SDK, with buildArgs: BuildArguments, in workingDirectory ] } + if let extractedXCFrameworksDir = extractedXCFrameworksDir { + // If the project's working directory contains xcframeworks in Carthage/Build, target-specific + // frameworks will have been extracted to a temporary directory. Provide these frameworks as a fallback + // in case the project is not configured to build using xcframeworks. + result += [ + "FRAMEWORK_SEARCH_PATHS=$(inherited) \(extractedXCFrameworksDir.path)" + ] + } + return result }() @@ -853,6 +1020,19 @@ private func build(sdk: SDK, with buildArgs: BuildArguments, in workingDirectory return buildScheme.launch() .flatMapTaskEvents(.concat) { _ in SignalProducer(settings) } .mapError(CarthageError.taskError) + .concat(SignalProducer { observer, _ in + // Delete extractedXCFrameworksDir after a successful build. + guard let extractedXCFrameworksDir = extractedXCFrameworksDir else { + observer.sendCompleted() + return + } + do { + try FileManager.default.removeItem(at: extractedXCFrameworksDir) + observer.sendCompleted() + } catch let error as NSError { + observer.send(error: .writeFailed(extractedXCFrameworksDir, error)) + } + }) } } } @@ -937,13 +1117,7 @@ public func buildInDirectory( // swiftlint:disable:this function_body_length let initialValue = (project, scheme) let wrappedSDKFilter: SDKFilterCallback = { sdks, scheme, configuration, project in - let filteredSDKs: [SDK] - if options.platforms.isEmpty { - filteredSDKs = sdks - } else { - filteredSDKs = sdks.filter { options.platforms.contains($0.platform) } - } - return sdkFilter(filteredSDKs, scheme, configuration, project) + return sdkFilter((options.platforms /* allow list */ ?? sdks).intersection(sdks), scheme, configuration, project) } return buildScheme( @@ -1003,6 +1177,42 @@ public func buildInDirectory( // swiftlint:disable:this function_body_length } } +public func copyAndStripFramework( + _ source: URL, + target: URL, + validArchitectures: [String], + strippingDebugSymbols: Bool = true, + queryingCodesignIdentityWith codesignIdentityQuery: SignalProducer = .init(value: nil), + copyingSymbolMapsInto symbolMapDestinationSignal: Result? = nil +) -> SignalProducer<(), CarthageError> { + let strippedArchitectureData = architecturesInPackage(source) + .flatMap(.race) { (archs: [String]) in + nonDestructivelyStripArchitectures(source, Set(archs).subtracting(validArchitectures)) + } + + return SignalProducer.combineLatest(copyProduct(source, target), codesignIdentityQuery, strippedArchitectureData) + .flatMap(.merge) { url, codesigningIdentity, strippedArchitectureData -> SignalProducer<(), CarthageError> in + return SignalProducer(value: strippedArchitectureData.1.relativePath) + .attemptMap { + return Result(at: target.appendingPathComponent($0)) { + try strippedArchitectureData.0.write(to: $0) + } + } + .concat(strippingDebugSymbols ? stripDebugSymbols(target) : .empty) + .concat(stripHeadersDirectory(target)) + .concat(stripPrivateHeadersDirectory(target)) + .concat(stripModulesDirectory(target)) + .concat(codesigningIdentity.map { codesign(target, $0) } ?? .empty) + .concat( + (symbolMapDestinationSignal?.producer ?? SignalProducer.empty) + .flatMap(.merge) { + BCSymbolMapsForFramework(source).copyFileURLsIntoDirectory($0) + } + .then(SignalProducer<(), CarthageError>.empty) + ) + } +} + /// Strips a framework from unexpected architectures and potentially debug symbols, /// optionally codesigning the result. public func stripFramework( @@ -1037,72 +1247,12 @@ public func stripDSYM(_ dSYMURL: URL, keepingArchitectures: [String]) -> SignalP } /// Strips a universal file from unexpected architectures. -private func stripBinary(_ binaryURL: URL, keepingArchitectures: [String]) -> SignalProducer<(), CarthageError> { - // With a very complex build, where multiple application targets share Carthage output, - // there is a concurrency issue if two build phases running in parallel try to work on the - // same framework. - // - // In a nutshell: - // pid 1094 copyProduct(MyFramework.framework) - // pid 1094 stripArchitecture(armv7) - // pid 1094 stripArchitecture(arm64) - // pid 1684 copyProduct(MyFramework.framework) - // pid 1684 stripArchitecture(armv7) - // pid 1916 copyProduct(MyFramework.framework) - // pid 1916 stripArchitecture(armv7) - // pid 1684 stripArchitecture(arm64) - // pid 1916 stripArchitecture(arm64) <-- already stripped, so an error occurs - // - // A shell task (/usr/bin/xcrun lipo -remove armv7 […] failed with exit code 1: - // fatal error: […]MyFramework.framework does not contain that architecture - // - // So we copy it to /tmp, modify it there, and copy it back to the original - // location. Problem averted! - // - // Footnote: Turns out this works perfectly for _framework_s, but the dSYMs - // introduce a wrinkle: They are all copied to ($BUILT_PRODUCTS_DIR), regardless - // of where the frameworks go, so that whole lipo scenario still exists. After - // many overly-complex attemts to handle cross-process & cross-thread locking, - // I came up with a simplified solution. - // - // - Continue to do all the work in tmp - // - Check to see if the product in tmp is exactly the same as the destination - // - If so, delete the temp file - // - If not, overwrite the dest (this handles updates to an existing dSYM) - // - - let fileManager = FileManager.default.reactive - - let createTempDir: SignalProducer = fileManager.createTemporaryDirectoryWithTemplate("carthage-lipo-XXXXXX") - - let copyItem: (URL, URL) -> SignalProducer = { source, dest in - fileManager.copyItem(source, into: dest) - } - - let strip: (URL, [String]) -> SignalProducer = { workspace, keeping in - return architecturesInPackage(workspace) - .filter { !keepingArchitectures.contains($0) } - .flatMap(.concat) { stripArchitecture(workspace, $0) } - .then(SignalProducer(value: workspace)) - } - - let replace: (URL, URL) -> SignalProducer<(), CarthageError> = { original, modified in - return fileManager.replaceItem(at: original, withItemAt: modified) - } - - return createTempDir - .flatMap(.merge) { tempDir in - copyItem(binaryURL, tempDir) - } - .flatMap(.merge) { tempFile in - strip(tempFile, keepingArchitectures) - } - .filter { tempFile in - return !FileManager.default.contentsEqual(atPath: tempFile.path, andPath: binaryURL.path) - } - .flatMap(.merge) { tempFile in - replace(binaryURL, tempFile) - } +private func stripBinary(_ packageURL: URL, keepingArchitectures: [String]) -> SignalProducer<(), CarthageError> { + return architecturesInPackage(packageURL) + .flatMap(.race) { (packageArchs: [String]) in + stripArchitectures(packageURL, Set(packageArchs).subtracting(keepingArchitectures)) + .then(SignalProducer<(), CarthageError>(value: ())) + } } /// Copies a product into the given folder. The folder will be created if it @@ -1210,74 +1360,145 @@ extension Signal where Value: TaskEventType { } } -/// Strips the given architecture from a framework. -private func stripArchitecture(_ frameworkURL: URL, _ architecture: String) -> SignalProducer<(), CarthageError> { - return SignalProducer { () -> Result in binaryURL(frameworkURL) } - .flatMap(.merge) { binaryURL -> SignalProducer, CarthageError> in - let lipoTask = Task("/usr/bin/xcrun", arguments: ["lipo", "-remove", architecture, "-output", binaryURL.path, binaryURL.path]) - return lipoTask.launch() +public func nonDestructivelyStripArchitectures(_ frameworkURL: URL, _ architectures: Set) -> SignalProducer<(Data, URL), CarthageError> { + return SignalProducer(value: frameworkURL) + .attemptMap(binaryURL) + .attemptMap { + let frameworkPathComponents = sequence(state: frameworkURL.absoluteURL.pathComponents.makeIterator(), next: { + $0.next() ?? "" + }) + + let suffix = zip(frameworkPathComponents, $0.pathComponents).drop(while: { $0 == $1 }) + + if suffix.contains(where: { $0.0 != "" }) { + return .failure(CarthageError.internalError(description: "In attempt to read NSBundle «\(frameworkURL.absoluteString)»'s binary url, could not relativize «\($0.debugDescription)» against «\(frameworkURL.absoluteString)».")) + } + return Result( + URLComponents(string: suffix.map { $0.1 }.joined(separator: "/"))? + .url(relativeTo: frameworkURL.absoluteURL.appendingPathComponent("/")), + failWith: CarthageError.internalError(description: "In attempt to read NSBundle «\(frameworkURL.absoluteString)»'s binary url, could not relativize «\($0.debugDescription)» against «\(frameworkURL.absoluteString)».") + ) + } + .zip(with: FileManager.default.reactive.createTemporaryDirectoryWithTemplate("carthage-lipo-XXXXXX")) + .flatMap(.race) { (relativeBinaryURL: URL, tempDir: URL) -> SignalProducer<(Data, URL), CarthageError> in + let outputURL = URL(string: relativeBinaryURL.relativePath, relativeTo: tempDir)! + + let command: [String] = { + if architectures.isEmpty { return [ "-create" ] } // creating just the contents of the original + return architectures.flatMap { [ "-remove", $0 ] } // creating a binary removing the specified + }() + + let arguments = [ + [ relativeBinaryURL.absoluteURL.path ], + command, + [ "-output", outputURL.path ], + ].reduce(into: ["lipo"]) { $0.append(contentsOf: $1) } + + let task = Task("/usr/bin/xcrun", arguments: arguments) + .launch() + .attempt { _ in + try? FileManager.default.createDirectory(at: outputURL.deletingLastPathComponent(), withIntermediateDirectories: true) + return .success(()) + } .mapError(CarthageError.taskError) + + let result: SignalProducer<(Data, URL), CarthageError> = SignalProducer(value: outputURL) + .attemptMap { + Result(at: $0, carthageError: CarthageError.readFailed) { url in + defer { try? FileManager.default.removeItem(at: url) } + return try Data(contentsOf: url) + } + .fanout(.success(relativeBinaryURL)) + } + + return task.then(result) } - .then(SignalProducer<(), CarthageError>.empty) } -/// Returns a signal of all architectures present in a given package. -public func architecturesInPackage(_ packageURL: URL) -> SignalProducer { +/// Strips the given architectures from a framework. +private func stripArchitectures(_ packageURL: URL, _ architectures: Set) -> SignalProducer<(), CarthageError> { return SignalProducer { () -> Result in binaryURL(packageURL) } - .flatMap(.merge) { binaryURL -> SignalProducer in - let lipoTask = Task("/usr/bin/xcrun", arguments: [ "lipo", "-info", binaryURL.path]) - - return lipoTask.launch() - .ignoreTaskData() + .flatMap(.merge) { binaryURL -> SignalProducer<(), CarthageError> in + // If there are no architectures to be stripped, then we don't + // need the lipo command at all. In fact, the assigment to arguments + // below would actually produce invalid input to the lipo command, with + // no -remove arguments + guard architectures.count > 0 else { + return SignalProducer<(), CarthageError>.empty + } + + let arguments = [ + [ binaryURL.absoluteURL.path ], + architectures.flatMap { [ "-remove", $0 ] }, + [ "-output", binaryURL.absoluteURL.path ], + ].reduce(into: ["lipo"]) { $0.append(contentsOf: $1) } + + let lipoTask = Task("/usr/bin/xcrun", arguments: arguments) + return lipoTask + .launch() .mapError(CarthageError.taskError) - .map { String(data: $0, encoding: .utf8) ?? "" } - .flatMap(.merge) { output -> SignalProducer in - var characterSet = CharacterSet.alphanumerics - characterSet.insert(charactersIn: " _-") - - let scanner = Scanner(string: output) - - if scanner.scanString("Architectures in the fat file:", into: nil) { - // The output of "lipo -info PathToBinary" for fat files - // looks roughly like so: - // - // Architectures in the fat file: PathToBinary are: armv7 arm64 - // - var architectures: NSString? - - scanner.scanString(binaryURL.path, into: nil) - scanner.scanString("are:", into: nil) - scanner.scanCharacters(from: characterSet, into: &architectures) - - let components = architectures? - .components(separatedBy: " ") - .filter { !$0.isEmpty } - - if let components = components { - return SignalProducer(components) - } - } + .then(SignalProducer<(), CarthageError>.empty) + } +} + +// Returns a signal of all architectures present in a given package. +public func architecturesInPackage(_ packageURL: URL, xcrunQuery: [String] = ["lipo", "-info"]) -> SignalProducer<[String], CarthageError> { + let binaryURLResult = binaryURL(packageURL) + guard let binaryURL = binaryURLResult.value else { return SignalProducer(error: binaryURLResult.error!) } - if scanner.scanString("Non-fat file:", into: nil) { - // The output of "lipo -info PathToBinary" for thin - // files looks roughly like so: - // - // Non-fat file: PathToBinary is architecture: x86_64 - // - var architecture: NSString? + return Task("/usr/bin/xcrun", arguments: xcrunQuery + [binaryURL.path]) + .launch() + .ignoreTaskData() + .mapError(CarthageError.taskError) + .map { String(data: $0, encoding: .utf8) ?? "" } + .attemptMap { output -> Result<[String], CarthageError> in + var characterSet = CharacterSet.alphanumerics + characterSet.insert(charactersIn: " _-") + + let scanner = Scanner(string: output) - scanner.scanString(binaryURL.path, into: nil) - scanner.scanString("is architecture:", into: nil) - scanner.scanCharacters(from: characterSet, into: &architecture) + if scanner.scanString("Architectures in the fat file:", into: nil) { + // The output of "lipo -info PathToBinary" for fat files + // looks roughly like so: + // + // Architectures in the fat file: PathToBinary are: armv7 arm64 + // + var architectures: NSString? - if let architecture = architecture { - return SignalProducer(value: architecture as String) - } - } + scanner.scanString(binaryURL.path, into: nil) + scanner.scanString("are:", into: nil) + scanner.scanCharacters(from: characterSet, into: &architectures) + + let components = architectures? + .components(separatedBy: " ") + .filter { !$0.isEmpty } - return SignalProducer(error: .invalidArchitectures(description: "Could not read architectures from \(packageURL.path)")) + if let components = components { + return .success(components) } + } + + if scanner.scanString("Non-fat file:", into: nil) { + // The output of "lipo -info PathToBinary" for thin + // files looks roughly like so: + // + // Non-fat file: PathToBinary is architecture: x86_64 + // + var architecture: NSString? + + scanner.scanString(binaryURL.path, into: nil) + scanner.scanString("is architecture:", into: nil) + scanner.scanCharacters(from: characterSet, into: &architecture) + + if let architecture = architecture { + return .success([architecture.replacingOccurrences(of: "\0", with: "")]) + } + } + + // think I changed the output of the below error (which used to use packageURL.path) + return .failure(.invalidArchitectures(description: "Could not read architectures from \(packageURL.path)")) } + .reduce(into: [] as [String]) { $0.append(contentsOf: $1 as [String]) } } /// Strips debug symbols from the given framework @@ -1424,7 +1645,7 @@ public func binaryURL(_ packageURL: URL) -> Result { domain: NSCocoaErrorDomain, code: CocoaError.fileReadCorruptFile.rawValue, userInfo: [ - NSLocalizedDescriptionKey: "Cannot retrive binary file from bundle at \(packageURL)", + NSLocalizedDescriptionKey: "Cannot retrieve binary file from bundle at \(packageURL)", NSLocalizedRecoverySuggestionErrorKey: "Does the bundle contain an Info.plist?" ] ))) diff --git a/Source/Scripts/carthage-bash-completion b/Source/Scripts/carthage-bash-completion index 009cacd436..e308015f57 100755 --- a/Source/Scripts/carthage-bash-completion +++ b/Source/Scripts/carthage-bash-completion @@ -2,7 +2,7 @@ _carthage() { local commands command cur prev - commands="archive bootstrap build checkout cleanup copy-frameworks fetch help outdated update validate version" + commands="archive bootstrap build checkout copy-frameworks fetch help outdated update validate version" command="${COMP_WORDS[1]}" cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -42,10 +42,6 @@ _carthage() { --color --project-directory' -- ${cur})) return 0 ;; - cleanup) - COMPREPLY=($(compgen -W '--color --project-directory' -- ${cur})) - return 0 - ;; fetch) COMPREPLY=($(compgen -W '--color' -- ${cur})) return 0 diff --git a/Source/Scripts/carthage-fish-completion b/Source/Scripts/carthage-fish-completion index 9a410729cc..7306ab64c3 100755 --- a/Source/Scripts/carthage-fish-completion +++ b/Source/Scripts/carthage-fish-completion @@ -52,9 +52,6 @@ complete -c carthage -n '__fish_prog_using_subcommand checkout' -l no-use-binari complete -c carthage -n '__fish_prog_using_subcommand checkout' -l color -a "auto always never" -x complete -c carthage -n '__fish_prog_using_subcommand checkout' -l project-directory -x -a '(__fish_complete_directories (commandline -ct))' -complete -c carthage -n '__fish_prog_using_subcommand cleanup' -l color -a "auto always never" -x -complete -c carthage -n '__fish_prog_using_subcommand cleanup' -l project-directory -x -a '(__fish_complete_directories (commandline -ct))' - complete -c carthage -n '__fish_prog_using_subcommand fetch' -l color -a "auto always never" -x complete -c carthage -n '__fish_prog_using_subcommand help' -a "archive bootstrap build checkout copy-frameworks fetch help outdated update validate version" -f diff --git a/Source/Scripts/carthage-zsh-completion b/Source/Scripts/carthage-zsh-completion index a7f4bccc8e..9ae26290c9 100755 --- a/Source/Scripts/carthage-zsh-completion +++ b/Source/Scripts/carthage-zsh-completion @@ -9,7 +9,6 @@ __subcommands() { 'bootstrap' 'build' 'checkout' - 'cleanup' 'copy-frameworks' 'fetch' 'help' @@ -89,13 +88,6 @@ _carthage() { '(-)*:: :->null_state' \ && ret=0 ;; - (cleanup) - _arguments \ - '--color: :(auto always never)' \ - '--project-directory: :_directories' \ - '(-)*:: :->null_state' \ - && ret=0 - ;; (fetch) _arguments \ '--color: :(auto always never)' \ diff --git a/Source/XCDBLD/BuildArguments.swift b/Source/XCDBLD/BuildArguments.swift index a475abe564..c00da7f288 100644 --- a/Source/XCDBLD/BuildArguments.swift +++ b/Source/XCDBLD/BuildArguments.swift @@ -106,7 +106,7 @@ public struct BuildArguments { // Since we wouldn't be trying to build this target unless it were // for macOS already, just let xcodebuild figure out the SDK on its // own. - if sdk != .macOSX { + if sdk.rawValue != "macosx" { args += [ "-sdk", sdk.rawValue ] } } @@ -135,6 +135,8 @@ public struct BuildArguments { // Frameworks get signed in the copy-frameworks action args += [ "CODE_SIGNING_REQUIRED=NO", "CODE_SIGN_IDENTITY=" ] + args += [ "SUPPORTS_MACCATALYST=NO" ] + args += [ "CARTHAGE=YES" ] return args diff --git a/Source/XCDBLD/FrameworkBundle.swift b/Source/XCDBLD/FrameworkBundle.swift new file mode 100644 index 0000000000..60331a4942 --- /dev/null +++ b/Source/XCDBLD/FrameworkBundle.swift @@ -0,0 +1,130 @@ +import Foundation +import ReactiveSwift +import ReactiveTask +import Result + +/// Loads a bundle directory from a given URL and sends Bundle objects for each framework in it. +/// +/// If `url` is an XCFramework, sends a Bundle for each embedded framework bundle. +/// If `url` is a framework bundle, sends a Bundle instance for the directory. +/// - parameter url: A framework or xcframework URL to load from. +/// - parameter platformName: If given, only sends bundles from an XCFramework with a matching `SupportedPlatform`. +/// - parameter variant: If given along with `platformName`, only sends bundles from an XCFramework with a matching `SupportedPlatformVariant`. +public func frameworkBundlesInURL(_ url: URL, compatibleWith platformName: String? = nil, variant: String? = nil) -> SignalProducer { + guard let bundle = Bundle(url: url) else { + return .empty + } + + switch bundle.object(forInfoDictionaryKey: "CFBundlePackageType") as? String { + case "XFWK": + let decoder = PropertyListDecoder() + let infoData = bundle.infoDictionary.flatMap({ try? PropertyListSerialization.data(fromPropertyList: $0, format: .binary, options: 0) }) ?? Data() + let xcframework = Result(catching: { try decoder.decode(XCFramework.self, from: infoData) }) + return SignalProducer(result: xcframework) + .map({ $0.availableLibraries }).flatten() + .filter { library in + guard let platformName = platformName else { return true } + return library.supportedPlatform == platformName && library.supportedPlatformVariant == variant + } + .map({ Bundle(url: url.appendingPathComponent($0.identifier).appendingPathComponent($0.path)) }) + .skipNil() + default: // Typically "FMWK" but not required + return SignalProducer(value: bundle) + } +} + +/// Create or update an xcframework from a framework bundle and its debug information. Any existing framework with the +/// same platform information will be replaced. +/// +/// XCFrameworks cannot be updated in-place, so this works by taking existing frameworks and debug info from the +/// xcframework, adding in the given framework and debug info, and writing it all into an a new xcframework bundle. +/// +/// Existing libraries in the xcframework with the same `platformName` and `variant` will be removed, so this function +/// can be used to update a single library in the xcframework with a new build. +/// - parameter xcframeworkURL: An xcframework which read from and merged into. +/// - parameter framework: The new framework to merge. +/// - parameter debugSymbols: dSYMs and bcsymbolmaps for `framework`. +/// - parameter platformName: The OS portion of the platform triple. Libraries in the xcframework with a matching platform name and variant will be replaced. +/// - parameter variant: The environment portion of the platform triple (i.e. "simulator" or nil). Libraries in the xcframework with a matching platform name and variant will be replaced. +/// - parameter outputURL: Location to write the merged xcframework to. +public func mergeIntoXCFramework( + _ xcframeworkURL: URL, + framework: URL, + debugSymbols: [URL], + platformName: String, + variant: String?, + outputURL: URL +) -> SignalProducer { + let baseArguments = ["xcodebuild", "-create-xcframework", "-allow-internal-distribution", "-output", outputURL.path] + let newLibraryArguments = ["-framework", framework.path] + debugSymbols.flatMap { ["-debug-symbols", $0.path] } + + let buildExistingLibraryArguments: SignalProducer<[String], NoError> = SignalProducer { () throws -> XCFramework in + // Load an existing xcframework at xcframeworkURL + let decoder = PropertyListDecoder() + let infoData = try Data(contentsOf: xcframeworkURL.appendingPathComponent("Info.plist")) + return try decoder.decode(XCFramework.self, from: infoData) + } + .flatMap(.concat) { xcframework -> SignalProducer in + // Only persist frameworks which _won't_ be overwritten by the new library + return SignalProducer(xcframework.availableLibraries.filter { library in + library.supportedPlatform != platformName || library.supportedPlatformVariant != variant + }) + } + .flatMap(.concat) { library -> SignalProducer in + // Discover and include dSYMs and bcsymbolmaps for each library + let libraryURL = xcframeworkURL.appendingPathComponent(library.identifier) + var arguments = ["-framework", libraryURL.appendingPathComponent(library.path).path] + + if let debugSymbolsPath = library.debugSymbolsPath, + let dsyms = try? FileManager.default.contentsOfDirectory( + at: libraryURL.appendingPathComponent(debugSymbolsPath), + includingPropertiesForKeys: nil + ) { + arguments += dsyms.flatMap { ["-debug-symbols", $0.path] } + } + + if let bitcodeSymbolMapsPath = library.bitcodeSymbolMapsPath, + let bcsymbolmaps = try? FileManager.default.contentsOfDirectory( + at: libraryURL.appendingPathComponent(bitcodeSymbolMapsPath), + includingPropertiesForKeys: nil + ) { + arguments += bcsymbolmaps.flatMap { ["-debug-symbols", $0.path] } + } + return SignalProducer(arguments) + } + .collect() + .flatMapError { _ in SignalProducer(value: []) } + + return buildExistingLibraryArguments.promoteError().flatMap(.concat) { existingLibraryArguments in + let arguments = baseArguments + newLibraryArguments + existingLibraryArguments + return Task("/usr/bin/xcrun", arguments: arguments).launch().ignoreTaskData().map { _ in outputURL } + } +} + +struct XCFramework: Decodable { + let availableLibraries: [Library] + let version: String + + struct Library: Decodable { + let identifier: String + let path: String + let supportedPlatform: String + let supportedPlatformVariant: String? + let debugSymbolsPath: String? + let bitcodeSymbolMapsPath: String? + + enum CodingKeys: String, CodingKey { + case identifier = "LibraryIdentifier" + case path = "LibraryPath" + case supportedPlatform = "SupportedPlatform" + case supportedPlatformVariant = "SupportedPlatformVariant" + case debugSymbolsPath = "DebugSymbolsPath" + case bitcodeSymbolMapsPath = "BitcodeSymbolMapsPath" + } + } + + enum CodingKeys: String, CodingKey { + case availableLibraries = "AvailableLibraries" + case version = "XCFrameworkFormatVersion" + } +} diff --git a/Source/XCDBLD/Platform.swift b/Source/XCDBLD/Platform.swift index 1a227600bf..b265d34274 100644 --- a/Source/XCDBLD/Platform.swift +++ b/Source/XCDBLD/Platform.swift @@ -1,36 +1,12 @@ import Foundation -/// Represents a platform to build for. -public enum Platform: String { - /// macOS. - case macOS = "Mac" +/* +Former file of `XCDBLD.Platform` — previously, platform functioned as somewhat of a +simulator-removed instance of an SDK. - /// iOS for device and simulator. - case iOS = "iOS" +This is dissimilar (in spirit and practice) to how Xcode Build Settings and +`xcodebuild -showsdks -json` employed the term `Platform`. - /// Apple Watch device and simulator. - case watchOS = "watchOS" - - /// Apple TV device and simulator. - case tvOS = "tvOS" - - /// All supported build platforms. - public static let supportedPlatforms: [Platform] = [ .macOS, .iOS, .watchOS, .tvOS ] - - /// The SDKs that need to be built for this platform. - public var SDKs: [SDK] { - switch self { - case .macOS: - return [ .macOSX ] - - case .iOS: - return [ .iPhoneSimulator, .iPhoneOS ] - - case .watchOS: - return [ .watchOS, .watchSimulator ] - - case .tvOS: - return [ .tvOS, .tvSimulator ] - } - } -} +Functionality previously provided by this type is mostly now accomodated by +`SDK.platformSimulatorlessFromHeuristic`. +*/ diff --git a/Source/XCDBLD/SDK.swift b/Source/XCDBLD/SDK.swift index ab42eaece2..047d337c5e 100644 --- a/Source/XCDBLD/SDK.swift +++ b/Source/XCDBLD/SDK.swift @@ -1,112 +1,240 @@ import Foundation import Result +import ReactiveTask +import ReactiveSwift + +/// - Note: Previously, `SDK` as an enum had a hardcoded set of values, +/// and was associated with a hardcoded set of `Platform`s — +/// where `Platform`s were pretty much SDK value minus simulator. +/// +/// Now, `SDK`s no longer have the constraint of being hardcoded-in +/// and in areas where `Platform` was interpolated, we use +/// `platformSimulatorlessFromHeuristic` which (in practice) usually +/// draws upon data from `xcodebuild -showsdks -json`. +public struct SDK: Hashable { + private let name: String + private let simulatorHeuristic: String + // it’s a fairly solid heuristic + + public init(name: String, simulatorHeuristic: String) { + (self.name, self.simulatorHeuristic) = (name, simulatorHeuristic) + } -/// Represents an SDK buildable by Xcode. -public enum SDK: String { - /// macOS. - case macOSX = "macosx" - - /// iOS, for device. - case iPhoneOS = "iphoneos" + public var rawValue: String { return name.lowercased() } - /// iOS, for the simulator. - case iPhoneSimulator = "iphonesimulator" + public var isSimulator: Bool { + return ["simulator"].contains(where: { + simulatorHeuristic.prefix(12).caseInsensitiveCompare($0 + " - ") == .orderedSame + || ( + name.suffix(9).caseInsensitiveCompare($0) == .orderedSame + && name.suffix(18).caseInsensitiveCompare($0 + $0) != .orderedSame + ) + }) + } - /// watchOS, for the Apple Watch device. - case watchOS = "watchos" + public var isDevice: Bool { + return !isSimulator + } - /// watchSimulator, for the Apple Watch simulator. - case watchSimulator = "watchsimulator" + public func hash(into: inout Hasher) { + return into.combine(self.rawValue) + } - /// tvOS, for the Apple TV device. - case tvOS = "appletvos" + public static func == (lhs: SDK, rhs: SDK) -> Bool { + return lhs.rawValue == rhs.rawValue + } - /// tvSimulator, for the Apple TV simulator. - case tvSimulator = "appletvsimulator" + public var simulatorJsonKeyUnderDevicesDictQuery: String { + guard self.simulatorHeuristic != "Simulator - visionOS" else { + return "xros" + } - public static let allSDKs: Set = [.macOSX, .iPhoneOS, .iPhoneSimulator, .watchOS, .watchSimulator, .tvOS, .tvSimulator] + return self.platformSimulatorlessFromHeuristic + } - /// Returns whether this is a device SDK. - public var isDevice: Bool { - switch self { - case .macOSX, .iPhoneOS, .watchOS, .tvOS: - return true + /// Take `simulatorHeuristic` and (best as possible) derive what used to be `XCDBLD.Platform` from it. + /// With data from `xcodebuild -showsdks -json`, should do solid job. + public var platformSimulatorlessFromHeuristic: String { + guard self.rawValue != "macosx" else { return "Mac" } + + guard simulatorHeuristic.isEmpty == false else { + let result = SDK.knownIn2019YearDictionary[self.rawValue, default: ("", [""], "")].1 + guard result.first != .some("") else { + return ["simulator"].reduce(into: self.name) { + let suffix = $0.suffix($1.utf8.count) + guard String(suffix).caseInsensitiveCompare($1) == .orderedSame else { return } + $0.removeSubrange(suffix.startIndex...) + } + } + + // essentially, the above asserts that `result.first?.firstIndex(of: " ") != nil` + // because of our hardcoded `knownIn2019YearDictionary` + return String( + result.first!.split(separator: " ", omittingEmptySubsequences: true).first! + ) + } - case .iPhoneSimulator, .watchSimulator, .tvSimulator: - return false + return ["simulator - "].reduce(into: simulatorHeuristic) { + let prefix = $0.commonPrefix(with: $1, options: .caseInsensitive) + guard prefix.isEmpty == false else { return } + $0.removeSubrange(.. = + Set( + knownIn2019YearDictionary + .mapValues { $0.2 } + .map(SDK.init) + ) + + public static let knownIn2023YearSDKs: Set = + Set( + knownIn2023YearDictionary + .mapValues { $0.2 } + .map(SDK.init) + ) + + /// - Warning: + public init?(rawValue: String) { + if rawValue.caseInsensitiveCompare("tvos") == .orderedSame { + self.name = "AppleTVOS" + self.simulatorHeuristic = "" + return } + + if rawValue.caseInsensitiveCompare("visionos") == .orderedSame { + self.name = "XROS" + self.simulatorHeuristic = "" + return + } + + guard let index = SDK.knownIn2023YearDictionary.index(forKey: rawValue.lowercased()) else { return nil } + + (self.name, _, self.simulatorHeuristic) = SDK.knownIn2023YearDictionary[index].value } - /// The platform that this SDK targets. - public var platform: Platform { - switch self { - case .iPhoneOS, .iPhoneSimulator: - return .iOS + private static func associatedSetOfKnownSDKs(_ argumentSubstring: String, dictionary: [String: (String, [String], String)]) -> Set { + let potentialSDK = argumentSubstring.lowercased() - case .watchOS, .watchSimulator: - return .watchOS + let potentialIndex = dictionary.index(forKey: potentialSDK) + ?? dictionary.firstIndex( + where: { _, value in + value.1.contains { $0.caseInsensitiveCompare(potentialSDK) == .orderedSame } + } + ) - case .tvOS, .tvSimulator: - return .tvOS + guard let index = potentialIndex else { return Set() } + + return [ + Optional(dictionary[index].value), + dictionary[dictionary[index].key.dropLast(2).appending("simulator")], + dictionary[dictionary[index].key.dropLast(9).appending("os")] + ] + .reduce(into: [] as Set) { + guard let value = $1 else { return } + $0.formUnion([SDK(name: value.0, simulatorHeuristic: value.2)]) + } - case .macOSX: - return .macOS - } } - private static var aliases: [String: SDK] { - return ["tvos": .tvOS] + public static func associatedSetOfKnownIn2023YearSDKs(_ argumentSubstring: String) -> Set { + return associatedSetOfKnownSDKs(argumentSubstring, dictionary: SDK.knownIn2023YearDictionary) } - public init?(rawValue: String) { - let lowerCasedRawValue = rawValue.lowercased() - let maybeSDK = SDK - .allSDKs - .map { ($0, $0.rawValue) } - .first { _, stringValue in stringValue.lowercased() == lowerCasedRawValue }? - .0 - - guard let sdk = maybeSDK ?? SDK.aliases[lowerCasedRawValue] else { - return nil - } - self = sdk + public static func associatedSetOfKnownIn2019YearSDKs(_ argumentSubstring: String) -> Set { + return associatedSetOfKnownSDKs(argumentSubstring, dictionary: SDK.knownIn2019YearDictionary) } } +// swiftlint:disable force_cast + +extension SDK { + /// - Note: Will, if available, use the version of `xcodebuild` from `DEVELOPER_DIR`. + /// - Note: Will omit SDKs — like DriverKit — where `canonicalName` and `platform` + /// do not share a common prefix. + public static let setFromJSONShowSDKs: SignalProducer?, NoError> = + Task("/usr/bin/xcrun", arguments: ["xcodebuild", "-showsdks", "-json"]) + .launch() + .materializeResults() // to map below and ignore errors + .filterMap { try? JSONSerialization.jsonObject(with: $0.value?.value ?? Data(bytes: []), options: JSONSerialization.ReadingOptions()) as? NSArray ?? NSArray() } + .map { + $0.compactMap { (nsobject: Any) -> SDK? in + let platform = NSString.lowercased( + (nsobject as! NSObject).value(forKey: "platform") as? NSString ?? "" + )(with: Locale?.none) + + guard platform.isEmpty == false else { return nil } + + guard NSString.lowercased( + (nsobject as! NSObject).value(forKey: "canonicalName") as? NSString ?? "\0" + )(with: Locale?.none).hasPrefix(platform) else { return nil } + + let simulatorHeuristic = CollectionOfOne( + (nsobject as! NSObject).value(forKey: "displayName") as? NSString + ).reduce(into: "") { + $0 = $1?.appending("") ?? $0 + let potentialVersion = $0.reversed().drop(while: "1234567890.".contains) + guard potentialVersion.firstIndex(of: ".") == potentialVersion.lastIndex(of: ".") else { + return + } + + $0 = String(potentialVersion.base.suffix(from: potentialVersion.startIndex).dropFirst().reversed()) + } + + let parseTitleCasePlatform: (String) -> String? = { + let index = $0.lastIndex(of: "/") ?? $0.startIndex + switch $0[index...].dropFirst().prefix(platform.count) { + case let string where string.caseInsensitiveCompare(platform) == .orderedSame: + return String(string) + default: + return nil + } + } + + let titleCasedPlatform = repeatElement( + (nsobject as! NSObject).value(forKey: "platformPath") as? NSString ?? "", count: 1 + ).reduce(into: String?.none) { $0 = parseTitleCasePlatform($1.appending("")) } + + return SDK(name: titleCasedPlatform ?? platform, simulatorHeuristic: simulatorHeuristic) + } + } + .reduce(into: Set?.none) { + guard $0 == nil else { return } + $0 = Set($1) + } +} + extension SDK: CustomStringConvertible { public var description: String { - switch self { - case .iPhoneOS: - return "iOS Device" - - case .iPhoneSimulator: - return "iOS Simulator" - - case .macOSX: - return "macOS" - - case .watchOS: - return "watchOS" - - case .watchSimulator: - return "watchOS Simulator" - - case .tvOS: - return "tvOS" - - case .tvSimulator: - return "tvOS Simulator" - } + return SDK.knownIn2019YearDictionary[self.rawValue]?.1.first! + ?? self.platformSimulatorlessFromHeuristic.appending(self.isSimulator ? " Simulator" : "") } } diff --git a/Source/XCDBLD/XcodeVersion.swift b/Source/XCDBLD/XcodeVersion.swift index 6853c2c1bc..05829d9c34 100644 --- a/Source/XCDBLD/XcodeVersion.swift +++ b/Source/XCDBLD/XcodeVersion.swift @@ -13,6 +13,10 @@ public struct XcodeVersion { self.buildVersion = buildVersion } + public var majorVersionNumber: Int? { + version.components(separatedBy: ".").first.flatMap(Int.init) + } + internal init?(xcodebuildOutput: String) { let range = NSRange(xcodebuildOutput.startIndex..., in: xcodebuildOutput) guard let match = XcodeVersion.regex.firstMatch(in: xcodebuildOutput, range: range) else { diff --git a/Source/carthage/Archive.swift b/Source/carthage/Archive.swift index ed1b84210c..52e0822715 100644 --- a/Source/carthage/Archive.swift +++ b/Source/carthage/Archive.swift @@ -71,7 +71,9 @@ public struct ArchiveCommand: CommandProtocol { } return frameworks.flatMap(.merge) { frameworks -> SignalProducer<(), CarthageError> in - return SignalProducer(Platform.supportedPlatforms) + // TODO: Better warning/planning for compressing up archives with non-known-in-year-2019 platforms. + // NOTE: as of current, non-known-in-year-2019 platforms are not compressed and copied by this command. + return SignalProducer(SDK.knownIn2023YearSDKs) .flatMap(.merge) { platform -> SignalProducer in return SignalProducer(frameworks).map { framework in return (platform.relativePath as NSString).appendingPathComponent(framework) diff --git a/Source/carthage/Build.swift b/Source/carthage/Build.swift index 924d4ad790..4ddb876fdb 100644 --- a/Source/carthage/Build.swift +++ b/Source/carthage/Build.swift @@ -18,11 +18,13 @@ extension BuildOptions: OptionsProtocol { return curry(BuildOptions.init) <*> mode <| Option(key: "configuration", defaultValue: "Release", usage: "the Xcode configuration to build" + addendum) - <*> (mode <| Option(key: "platform", defaultValue: .all, usage: platformUsage)).map { $0.platforms } + <*> (mode <| Option(key: "platform", defaultValue: .all, usage: platformUsage)) + .map { if case let .setDisjointWithFlaggedAll(set) = $0 { return set } else { return nil } } <*> mode <| Option(key: "toolchain", defaultValue: nil, usage: "the toolchain to build with") <*> mode <| Option(key: "derived-data", defaultValue: nil, usage: "path to the custom derived data folder") <*> mode <| Option(key: "cache-builds", defaultValue: false, usage: "use cached builds when possible") <*> mode <| Option(key: "use-binaries", defaultValue: true, usage: "don't use downloaded binaries when possible") + <*> mode <| Option(key: "use-xcframeworks", defaultValue: false, usage: "create xcframework bundles instead of one framework per platform (requires Xcode 12+)") } } @@ -220,115 +222,33 @@ public struct BuildCommand: CommandProtocol { } } -/// Represents the user's chosen platform to build for. public enum BuildPlatform: Equatable { - /// Build for all available platforms. + case setDisjointWithFlaggedAll(Set) case all - - /// Build only for iOS. - case iOS - - /// Build only for macOS. - case macOS - - /// Build only for watchOS. - case watchOS - - /// Build only for tvOS. - case tvOS - - /// Build for multiple platforms within the list. - case multiple([BuildPlatform]) - - /// The set of `Platform` corresponding to this setting. - public var platforms: Set { - switch self { - case .all: - return [] - - case .iOS: - return [ .iOS ] - - case .macOS: - return [ .macOS ] - - case .watchOS: - return [ .watchOS ] - - case .tvOS: - return [ .tvOS ] - - case let .multiple(buildPlatforms): - return buildPlatforms.reduce(into: []) { set, buildPlatform in - set.formUnion(buildPlatform.platforms) - } - } - } } -extension BuildPlatform: CustomStringConvertible { - public var description: String { - switch self { - case .all: - return "all" - - case .iOS: - return "iOS" +extension BuildPlatform: ArgumentProtocol { + public static let name = "platform" - case .macOS: - return "macOS" + private static func parseSet(_ string: String) throws -> BuildPlatform { + switch Set(string.split()) { + case []: + throw CocoaError(.keyValueValidation) + case let set: + guard set != ["all"] else { return .all } - case .watchOS: - return "watchOS" + guard set.isDisjoint(with: ["all"]) else { throw CocoaError(.keyValueValidation) /* because not solely `all` */ } - case .tvOS: - return "tvOS" + let values = try set.lazy.map(SDK.associatedSetOfKnownIn2023YearSDKs).reduce(into: [] as Set) { + guard $1.isEmpty == false else { throw CocoaError(.keyValueValidation) } + $0.formUnion($1) + } - case let .multiple(buildPlatforms): - return buildPlatforms.map { $0.description }.joined(separator: ", ") + return .setDisjointWithFlaggedAll(values) } } -} - -extension BuildPlatform: ArgumentProtocol { - public static let name = "platform" - - private static let acceptedStrings: [String: BuildPlatform] = [ - "macOS": .macOS, "Mac": .macOS, "OSX": .macOS, "macosx": .macOS, - "iOS": .iOS, "iphoneos": .iOS, "iphonesimulator": .iOS, - "watchOS": .watchOS, "watchsimulator": .watchOS, - "tvOS": .tvOS, "tvsimulator": .tvOS, "appletvos": .tvOS, "appletvsimulator": .tvOS, - "all": .all, - ] public static func from(string: String) -> BuildPlatform? { - let tokens = string.split() - - let findBuildPlatform: (String) -> BuildPlatform? = { string in - return self.acceptedStrings - .first { key, _ in string.caseInsensitiveCompare(key) == .orderedSame } - .map { _, platform in platform } - } - - switch tokens.count { - case 0: - return nil - - case 1: - return findBuildPlatform(tokens[0]) - - default: - var buildPlatforms = [BuildPlatform]() - for token in tokens { - if let found = findBuildPlatform(token), found != .all { - buildPlatforms.append(found) - } else { - // Reject if an invalid value is included in the comma- - // separated string. - return nil - } - } - return .multiple(buildPlatforms) - } + return try? parseSet(string) } } diff --git a/Source/carthage/Cleanup.swift b/Source/carthage/Cleanup.swift index 3aa3c00b40..8054539d45 100644 --- a/Source/carthage/Cleanup.swift +++ b/Source/carthage/Cleanup.swift @@ -4,35 +4,11 @@ import Foundation import Result import Curry -/// Type that encapsulates the configuration and evaluation of the `cleanup` subcommand. -public struct CleanupCommand: CommandProtocol { - public let verb = "cleanup" - public let function = "Remove unneeded files from Carthage directory" +/* +Former file of `carthage cleanup` command — which existed on-master, but unshipped-in-tags — and no longer makes sense when set of SDKs are non-fixed across Xcode versions. - public struct Options: OptionsProtocol { - public let directoryPath: String - public let colorOptions: ColorOptions +See also — and major thanks to @sidepelican and @chuganzy for developing it… +〜 sorry that the new system of dynamically parsed SDKs makes it nonviable; maybe it's viable in some future way… - public static func evaluate(_ mode: CommandMode) -> Result> { - return curry(self.init) - <*> mode <| Option( - key: "project-directory", - defaultValue: FileManager.default.currentDirectoryPath, - usage: "the directory containing the Carthage project" - ) - <*> ColorOptions.evaluate(mode) - } - - public func loadProject() -> Project { - let directoryURL = URL(fileURLWithPath: self.directoryPath, isDirectory: true) - let project = Project(directoryURL: directoryURL) - var eventSink = ProjectEventSink(colorOptions: colorOptions) - project.projectEvents.observeValues { eventSink.put($0) } - return project - } - } - - public func run(_ options: Options) -> Result<(), CarthageError> { - return options.loadProject().removeUnneededItems().waitOnCommand() - } -} +See commit . +*/ diff --git a/Source/carthage/CopyFrameworks.swift b/Source/carthage/CopyFrameworks.swift index 059654d4eb..2db87456d7 100644 --- a/Source/carthage/CopyFrameworks.swift +++ b/Source/carthage/CopyFrameworks.swift @@ -32,10 +32,18 @@ public struct CopyFrameworksCommand: CommandProtocol { carthage.println("warning: Ignoring \(frameworkName) because it does not support the current architecture\n") return .empty } else { - let copyFrameworks = copyFramework(source, target: target, validArchitectures: validArchitectures) + let copyFrameworks = copyAndStripFramework( + source, + target: target, + validArchitectures: validArchitectures, + strippingDebugSymbols: shouldStripDebugSymbols(), + queryingCodesignIdentityWith: codeSigningIdentity(), + copyingSymbolMapsInto: builtProductsFolder() + ) + let copydSYMs = copyDebugSymbolsForFramework(source, validArchitectures: validArchitectures) - return SignalProducer.combineLatest(copyFrameworks, copydSYMs) - .then(SignalProducer<(), CarthageError>.empty) + + return SignalProducer.merge(copyFrameworks, copydSYMs) } } } @@ -46,28 +54,8 @@ public struct CopyFrameworksCommand: CommandProtocol { } } -private func copyFramework(_ source: URL, target: URL, validArchitectures: [String]) -> SignalProducer<(), CarthageError> { - return SignalProducer.combineLatest(copyProduct(source, target), codeSigningIdentity()) - .flatMap(.merge) { url, codesigningIdentity -> SignalProducer<(), CarthageError> in - let strip = stripFramework( - url, - keepingArchitectures: validArchitectures, - strippingDebugSymbols: shouldStripDebugSymbols(), - codesigningIdentity: codesigningIdentity - ) - if buildActionIsArchiveOrInstall() { - return strip - .then(copyBCSymbolMapsForFramework(url, fromDirectory: source.deletingLastPathComponent())) - .then(SignalProducer<(), CarthageError>.empty) - } else { - return strip - } - } -} - private func shouldIgnoreFramework(_ framework: URL, validArchitectures: [String]) -> SignalProducer { return architecturesInPackage(framework) - .collect() .map { architectures in // Return all the architectures, present in the framework, that are valid. validArchitectures.filter(architectures.contains) @@ -165,9 +153,23 @@ private func frameworksFolder() -> Result { } private func validArchitectures() -> Result<[String], CarthageError> { - return getEnvironmentVariable("VALID_ARCHS").map { architectures -> [String] in - return architectures.components(separatedBy: " ") - } + let validArchs = getEnvironmentVariable("VALID_ARCHS").map { architectures -> [String] in + return architectures.components(separatedBy: " ") + } + + if validArchs.error != nil { + return validArchs + } + + let archs = getEnvironmentVariable("ARCHS").map { architectures -> [String] in + return architectures.components(separatedBy: " ") + } + + if archs.error != nil { + return archs + } + + return .success(validArchs.value!.filter(archs.value!.contains)) } private func buildActionIsArchiveOrInstall() -> Bool { diff --git a/Source/carthage/Extensions.swift b/Source/carthage/Extensions.swift index 3df0821427..abb81783e2 100644 --- a/Source/carthage/Extensions.swift +++ b/Source/carthage/Extensions.swift @@ -95,15 +95,15 @@ internal struct ProjectEventSink { case let .downloadingBinaries(dependency, release): carthage.println(formatting.bullets + "Downloading " + formatting.projectName(dependency.name) - + ".framework binary at " + formatting.quote(release)) + + " binary at " + formatting.quote(release)) case let .skippedDownloadingBinaries(dependency, message): carthage.println(formatting.bullets + "Skipped downloading " + formatting.projectName(dependency.name) - + ".framework binary due to the error:\n\t" + formatting.quote(message)) + + " binary due to the error:\n\t" + formatting.quote(message)) case let .skippedInstallingBinaries(dependency, error): let output = """ - \(formatting.bullets) Skipped installing \(formatting.projectName(dependency.name)).framework binary due to the error: + \(formatting.bullets) Skipped installing \(formatting.projectName(dependency.name)) binary due to the error: \(formatting.quote(String(describing: error))) Falling back to building from the source @@ -123,9 +123,6 @@ internal struct ProjectEventSink { case let .buildingUncached(dependency): carthage.println(formatting.bullets + "No cache found for " + formatting.projectName(dependency.name) + ", building with all downstream dependencies") - - case let .removingUnneededItem(url): - carthage.println(formatting.bullets + "Removing unneeded file at " + url.path) } } } diff --git a/Source/carthage/main.swift b/Source/carthage/main.swift index 7d3eb3239c..709fefbc32 100644 --- a/Source/carthage/main.swift +++ b/Source/carthage/main.swift @@ -25,7 +25,6 @@ registry.register(ArchiveCommand()) registry.register(BootstrapCommand()) registry.register(BuildCommand()) registry.register(CheckoutCommand()) -registry.register(CleanupCommand()) registry.register(CopyFrameworksCommand()) registry.register(FetchCommand()) registry.register(OutdatedCommand()) diff --git a/Tests/CarthageKitTests/BinaryProjectSpec.swift b/Tests/CarthageKitTests/BinaryProjectSpec.swift index 3b6a17cdb0..d8cd7be0c3 100644 --- a/Tests/CarthageKitTests/BinaryProjectSpec.swift +++ b/Tests/CarthageKitTests/BinaryProjectSpec.swift @@ -10,16 +10,25 @@ class BinaryProjectSpec: QuickSpec { it("should parse") { let jsonData = ( "{" + - "\"1.0\": \"https://my.domain.com/release/1.0.0/framework.zip\"," + - "\"1.0.1\": \"https://my.domain.com/release/1.0.1/framework.zip\"" + + "\"1.0\": \"https://example.com/release/1.0.0/framework.zip\"," + + "\"1.0.1\": \"https://example.com/release/1.0.1/framework.zip?alt=https://example.com/release/1.0.1/xcframework.zip&alt=https://example.com/some/other/alternate.zip\"," + + "\"1.0.2\": \"https://example.com/release/1.0.2/framework.zip?alt=https%3A%2F%2Fexample.com%2Frelease%2F1.0.2%2Fxcframework.zip\"" + "}" ).data(using: .utf8)! let actualBinaryProject = BinaryProject.from(jsonData: jsonData).value let expectedBinaryProject = BinaryProject(versions: [ - PinnedVersion("1.0"): URL(string: "https://my.domain.com/release/1.0.0/framework.zip")!, - PinnedVersion("1.0.1"): URL(string: "https://my.domain.com/release/1.0.1/framework.zip")!, + PinnedVersion("1.0"): [URL(string: "https://example.com/release/1.0.0/framework.zip")!], + PinnedVersion("1.0.1"): [ + URL(string: "https://example.com/release/1.0.1/framework.zip")!, + URL(string: "https://example.com/release/1.0.1/xcframework.zip")!, + URL(string: "https://example.com/some/other/alternate.zip")!, + ], + PinnedVersion("1.0.2"): [ + URL(string: "https://example.com/release/1.0.2/framework.zip")!, + URL(string: "https://example.com/release/1.0.2/xcframework.zip")! + ], ]) expect(actualBinaryProject) == expectedBinaryProject @@ -52,7 +61,7 @@ class BinaryProjectSpec: QuickSpec { } it("should fail with an invalid semantic version") { - let jsonData = "{ \"1.a\": \"https://my.domain.com/release/1.0.0/framework.zip\" }".data(using: .utf8)! + let jsonData = "{ \"1.a\": \"https://example.com/release/1.0.0/framework.zip\" }".data(using: .utf8)! let actualError = BinaryProject.from(jsonData: jsonData).error @@ -60,18 +69,18 @@ class BinaryProjectSpec: QuickSpec { } it("should fail with a non-parseable URL") { - let jsonData = "{ \"1.0\": \"💩\" }".data(using: .utf8)! + let jsonData = "{ \"1.0\": \"https://[].erroneous_square_brackets.example.com/\" }".data(using: .utf8)! let actualError = BinaryProject.from(jsonData: jsonData).error - expect(actualError) == .invalidURL("💩") + expect(actualError) == .invalidURL("https://[].erroneous_square_brackets.example.com/") } it("should fail with a non HTTPS url") { - let jsonData = "{ \"1.0\": \"http://my.domain.com/framework.zip\" }".data(using: .utf8)! + let jsonData = "{ \"1.0\": \"http://example.com/framework.zip\" }".data(using: .utf8)! let actualError = BinaryProject.from(jsonData: jsonData).error - expect(actualError) == .nonHTTPSURL(URL(string: "http://my.domain.com/framework.zip")!) + expect(actualError) == .nonHTTPSURL(URL(string: "http://example.com/framework.zip")!) } it("should parse with a file url") { @@ -79,7 +88,7 @@ class BinaryProjectSpec: QuickSpec { let actualBinaryProject = BinaryProject.from(jsonData: jsonData).value let expectedBinaryProject = BinaryProject(versions: [ - PinnedVersion("1.0"): URL(string: "file:///my/domain/com/framework.zip")!, + PinnedVersion("1.0"): [URL(string: "file:///my/domain/com/framework.zip")!], ]) expect(actualBinaryProject) == expectedBinaryProject diff --git a/Tests/CarthageKitTests/CartfileCommentsSpec.swift b/Tests/CarthageKitTests/CartfileCommentsSpec.swift index 027778bdc2..1e7da60d6f 100644 --- a/Tests/CarthageKitTests/CartfileCommentsSpec.swift +++ b/Tests/CarthageKitTests/CartfileCommentsSpec.swift @@ -18,7 +18,8 @@ class CarfileCommentsSpec: QuickSpec { "I say \"hello\" you say \"goodbye\"!" ] .forEach { - expect($0.strippingTrailingCartfileComment) == $0 + let cartfileStringWithNoTrueComments = $0 + expect(cartfileStringWithNoTrueComments.strippingTrailingCartfileComment).to(equal(cartfileStringWithNoTrueComments)) } } @@ -29,7 +30,8 @@ class CarfileCommentsSpec: QuickSpec { "\"#\"" ] .forEach { - expect($0.strippingTrailingCartfileComment) == $0 + let cartfileStringWithNoTrueComments = $0 + expect(cartfileStringWithNoTrueComments.strippingTrailingCartfileComment).to(equal(cartfileStringWithNoTrueComments)) } } diff --git a/Tests/CarthageKitTests/DB.swift b/Tests/CarthageKitTests/DB.swift index 7eedfa2b9d..9d0869f79a 100644 --- a/Tests/CarthageKitTests/DB.swift +++ b/Tests/CarthageKitTests/DB.swift @@ -3,6 +3,7 @@ import ReactiveSwift import Foundation import Result import Tentacle +import struct XCDBLD.SDK // swiftlint:disable no_extension_access_modifier let git1 = Dependency.git(GitURL("https://example.com/repo1")) @@ -102,3 +103,8 @@ extension DB: ExpressibleByDictionaryLiteral { } } } + +extension SDK { + static let macOS = SDK.knownIn2019YearSDKs.first(where: { $0.rawValue == "macosx" })! + static let iOS = SDK.knownIn2019YearSDKs.first(where: { $0.rawValue == "iphoneos" })! +} diff --git a/Tests/CarthageKitTests/DependencySpec.swift b/Tests/CarthageKitTests/DependencySpec.swift index 0afc0cffc6..00b22d3c22 100644 --- a/Tests/CarthageKitTests/DependencySpec.swift +++ b/Tests/CarthageKitTests/DependencySpec.swift @@ -100,29 +100,66 @@ class DependencySpec: QuickSpec { fileManager.changeCurrentDirectoryPath(startingDirectory) } - it("should be the directory name if the given URL string is '.'") { + it("should sanitize even despite the given URL string being (pathologically) solely the nul character") { + // this project would not be able to be checked out + + let dependency = Dependency.git(GitURL("\u{0000}")) + + expect(dependency.name) == "␀" + } + + it("should sanitize even despite the given URL string being (pathologically) solely the nul character and path separators") { + // this project would not be able to be checked out + + let dependency = Dependency.git(GitURL("/\u{0000}/")) + + expect(dependency.name) == "␀" + } + + it("should sanitize even despite the given URL string containing (pathologically) the nul character") { + // this project would not be able to be checked out + + let dependency = Dependency.git(GitURL("./../../../../../\u{0000}myproject")) + + expect(dependency.name) == "␀myproject" + } + + + it("should sanitize if the given URL string is (pathologically) «.»") { let dependency = Dependency.git(GitURL(".")) - expect(dependency.name) == temporaryDirectoryURL.lastPathComponent + expect(dependency.name) == "\u{FF0E}" } - it ("should be the directory name if the given URL string is prefixed by './'") { + it ("should be the directory name if the given URL string is (pathologically) prefixed by «./»") { let dependency = Dependency.git(GitURL("./myproject")) - expect(dependency.name) == "myproject" + expect(dependency.name) == "myproject" } - it("should be the directory name if the given URL string is '..'") { + it("should sanitize if the given URL string is (pathologically) «..»") { let dependency = Dependency.git(GitURL("..")) - expect(dependency.name) == temporaryDirectoryURL.deletingLastPathComponent().lastPathComponent + expect(dependency.name) == "\u{FF0E}\u{FF0E}" + } + + it("should sanitize if the given URL string is (pathologically) «...git»") { + let dependency = Dependency.git(GitURL("...git")) + + expect(dependency.name) == "\u{FF0E}\u{FF0E}" } - it ("should be the directory name if the given URL string is prefixed by '../'") { + it ("should be the directory name if the given URL string is (pathologically) prefixed by «../» with (pathologically) no URL scheme") { let dependency = Dependency.git(GitURL("../myproject")) expect(dependency.name) == "myproject" } + + it ("should sanitize if the given URL string is (pathologically) suffixed by «/..»") { + let dependency = Dependency.git(GitURL("../myproject/..")) + + expect(dependency.name) == "\u{FF0E}\u{FF0E}" + } } } diff --git a/Tests/CarthageKitTests/ProjectSpec.swift b/Tests/CarthageKitTests/ProjectSpec.swift index 969fa74d87..ddfe67a01c 100644 --- a/Tests/CarthageKitTests/ProjectSpec.swift +++ b/Tests/CarthageKitTests/ProjectSpec.swift @@ -7,6 +7,7 @@ import Tentacle import Result import ReactiveTask import XCDBLD +import XCTest // swiftlint:disable:this force_try @@ -19,7 +20,7 @@ class ProjectSpec: QuickSpec { let noSharedSchemesDirectoryURL = Bundle(for: type(of: self)).url(forResource: "NoSharedSchemesTest", withExtension: nil)! let noSharedSchemesBuildDirectoryURL = noSharedSchemesDirectoryURL.appendingPathComponent(Constants.binariesFolderPath) - func build(directoryURL url: URL, platforms: Set = [], cacheBuilds: Bool = true, dependenciesToBuild: [String]? = nil) -> [String] { + func build(directoryURL url: URL, platforms: Set? = nil, cacheBuilds: Bool = true, dependenciesToBuild: [String]? = nil) -> [String] { let project = Project(directoryURL: url) let result = project.buildCheckedOutDependenciesWithOptions(BuildOptions(configuration: "Debug", platforms: platforms, cacheBuilds: cacheBuilds), dependenciesToBuild: dependenciesToBuild) .ignoreTaskData() @@ -31,14 +32,17 @@ class ProjectSpec: QuickSpec { .single()! expect(result.error).to(beNil()) - return result.value!.map { $0.name } + if result.value == nil { + _ = XCTFail("no result from buildCheckedOutDependenciesWithOptions") + } + return result.value?.map { $0.name } ?? [] } - func buildDependencyTest(platforms: Set = [], cacheBuilds: Bool = true, dependenciesToBuild: [String]? = nil) -> [String] { + func buildDependencyTest(platforms: Set? = nil, cacheBuilds: Bool = true, dependenciesToBuild: [String]? = nil) -> [String] { return build(directoryURL: directoryURL, platforms: platforms, cacheBuilds: cacheBuilds, dependenciesToBuild: dependenciesToBuild) } - func buildNoSharedSchemesTest(platforms: Set = [], cacheBuilds: Bool = true, dependenciesToBuild: [String]? = nil) -> [String] { + func buildNoSharedSchemesTest(platforms: Set? = nil, cacheBuilds: Bool = true, dependenciesToBuild: [String]? = nil) -> [String] { return build(directoryURL: noSharedSchemesDirectoryURL, platforms: platforms, cacheBuilds: cacheBuilds, dependenciesToBuild: dependenciesToBuild) } @@ -57,7 +61,7 @@ class ProjectSpec: QuickSpec { let macOSexpected = ["TestFramework3_Mac", "TestFramework2_Mac", "TestFramework1_Mac"] let iOSExpected = ["TestFramework3_iOS", "TestFramework2_iOS", "TestFramework1_iOS"] - let result = buildDependencyTest(platforms: [], cacheBuilds: false) + let result = buildDependencyTest(platforms: nil, cacheBuilds: false) expect(result.filter { $0.contains("Mac") }) == macOSexpected expect(result.filter { $0.contains("iOS") }) == iOSExpected @@ -456,14 +460,14 @@ class ProjectSpec: QuickSpec { let actualDefinition = project.downloadBinaryFrameworkDefinition(binary: binary).first()?.value let expectedBinaryProject = BinaryProject(versions: [ - PinnedVersion("1.0"): URL(string: "https://my.domain.com/release/1.0.0/framework.zip")!, - PinnedVersion("1.0.1"): URL(string: "https://my.domain.com/release/1.0.1/framework.zip")!, + PinnedVersion("1.0"): [URL(string: "https://example.com/release/1.0.0/framework.zip")!], + PinnedVersion("1.0.1"): [URL(string: "https://example.com/release/1.0.1/framework.zip")!], ]) expect(actualDefinition) == expectedBinaryProject } it("should return read failed if unable to download") { - let url = URL(string: "file:///thisfiledoesnotexist.json")! + let url = URL(string: "file://var/empty/thisfiledoesnotexist.json")! let binary = BinaryURL(url: url, resolvedDescription: testDefinitionURL.description) let actualError = project.downloadBinaryFrameworkDefinition(binary: binary).first()?.error @@ -601,152 +605,6 @@ class ProjectSpec: QuickSpec { } } - describe("cleanup Carthage directory") { - let baseDirectoryURL = Bundle(for: type(of: self)).url(forResource: "CleanupTest", withExtension: nil)! - - it("should successfully remove files not needed") { - let directoryURL = baseDirectoryURL.appendingPathComponent("Valid", isDirectory: true) - let project = Project(directoryURL: directoryURL) - var events = [ProjectEvent]() - project.projectEvents.observeValues { events.append($0) } - - expect(project.removeUnneededItems().wait().error).to(beNil()) - - let removedItems = events.compactMap { event -> URL? in - guard case let .removingUnneededItem(url) = event else { - fail() - return nil - } - return url - } - - let expectedPaths = [ - ("Build/Mac/TestFramework2.framework.dSYM", true), - ("Build/Mac/TestFramework1.framework.dSYM", true), - ("Build/iOS/TestFramework2.framework.dSYM", true), - ("Build/iOS/TestFramework1.framework.dSYM", true), - ("Build/Mac/TestFramework2.framework", true), - ("Build/Mac/TestFramework1.framework", true), - ("Build/iOS/TestFramework2.framework", true), - ("Build/iOS/TestFramework1.framework", true), - ("Checkouts/TestFramework1", true), - ("Checkouts/TestFramework2", true), - ("Build/iOS/59F47BB3-1D4F-3B7F-A0D3-273E2F5B9526.bcsymbolmap", false), - ("Build/iOS/1047B36A-DF55-31AE-B619-D457C836A39D.bcsymbolmap", false), - ("Build/iOS/D66A4E3C-FAB4-38A5-9863-D1A27A5C4B41.bcsymbolmap", false), - ("Build/iOS/86E1998A-CF88-316A-87F7-EED06C281067.bcsymbolmap", false), - ("Build/.TestFramework2.version", false), - ("Build/.TestFramework1.version", false), - ] - - let expectedItems = Set(expectedPaths.map { - directoryURL.appendingPathComponent("Carthage/\($0)", isDirectory: $1) - }) - - expect(Set(removedItems)) == expectedItems - } - - it("should successfully remove old frameworks when the library changes dynamic framework to static framework") { - let directoryURL = baseDirectoryURL.appendingPathComponent("RemoveDynamic", isDirectory: true) - let project = Project(directoryURL: directoryURL) - var events = [ProjectEvent]() - project.projectEvents.observeValues { events.append($0) } - - expect(project.removeUnneededItems().wait().error).to(beNil()) - - let removedItems = events.compactMap { event -> URL? in - guard case let .removingUnneededItem(url) = event else { - fail() - return nil - } - return url - } - - let expectedPaths = [ - ("Build/Mac/TestFramework.framework.dSYM", true), - ("Build/iOS/TestFramework.framework.dSYM", true), - ("Build/Mac/TestFramework.framework", true), - ("Build/iOS/TestFramework.framework", true), - ("Build/iOS/51899E2B-87E1-3129-97D2-C8FEECF71698.bcsymbolmap", false), - ("Build/iOS/B730142E-39F3-3EB2-A826-4043D39695EE.bcsymbolmap", false), - ] - - let expectedItems = Set(expectedPaths.map { - directoryURL.appendingPathComponent("Carthage/\($0)", isDirectory: $1) - }) - - expect(Set(removedItems)) == expectedItems - } - - it("should successfully remove static frameworks") { - let directoryURL = baseDirectoryURL.appendingPathComponent("StaticFramework", isDirectory: true) - let project = Project(directoryURL: directoryURL) - var events = [ProjectEvent]() - project.projectEvents.observeValues { events.append($0) } - - expect(project.removeUnneededItems().wait().error).to(beNil()) - - let removedItems = events.compactMap { event -> URL? in - guard case let .removingUnneededItem(url) = event else { - fail() - return nil - } - return url - } - - let expectedPaths = [ - ("Build/Mac/Static/TestFramework.framework", true), - ("Build/iOS/Static/TestFramework.framework", true), - ("Checkouts/TestFramework", true), - ("Build/.TestFramework.version", false), - ] - - let expectedItems = Set(expectedPaths.map { - directoryURL.appendingPathComponent("Carthage/\($0)", isDirectory: $1) - }) - - expect(Set(removedItems)) == expectedItems - } - - it("should successfully if there is a lack of a framework") { - let directoryURL = baseDirectoryURL.appendingPathComponent("PlatformUsed", isDirectory: true) - let project = Project(directoryURL: directoryURL) - var events = [ProjectEvent]() - project.projectEvents.observeValues { events.append($0) } - - expect(project.removeUnneededItems().wait().error).to(beNil()) - - let removedItems = events.compactMap { event -> URL? in - guard case let .removingUnneededItem(url) = event else { - fail() - return nil - } - return url - } - - let expectedPaths = [ - ("Build/iOS/TestFramework2.framework.dSYM", true), - ("Build/iOS/TestFramework1.framework.dSYM", true), - ("Build/iOS/TestFramework2.framework", true), - ("Build/iOS/TestFramework1.framework", true), - ("Checkouts/TestFramework1", true), - ("Checkouts/TestFramework2", true), - ("Build/iOS/59F47BB3-1D4F-3B7F-A0D3-273E2F5B9526.bcsymbolmap", false), - ("Build/iOS/1047B36A-DF55-31AE-B619-D457C836A39D.bcsymbolmap", false), - ("Build/iOS/D66A4E3C-FAB4-38A5-9863-D1A27A5C4B41.bcsymbolmap", false), - ("Build/iOS/86E1998A-CF88-316A-87F7-EED06C281067.bcsymbolmap", false), - ("Build/.TestFramework2.version", false), - ("Build/.TestFramework1.version", false), - ] - - let expectedItems = Set(expectedPaths.map { - directoryURL.appendingPathComponent("Carthage/\($0)", isDirectory: $1) - }) - - expect(Set(removedItems)) == expectedItems - } - } - describe("transitiveDependencies") { it("should find the correct dependencies") { let cartfile = """ diff --git a/Tests/CarthageKitTests/ProxyTests.swift b/Tests/CarthageKitTests/ProxyTests.swift index 3f1ebcc288..1e83697ec0 100644 --- a/Tests/CarthageKitTests/ProxyTests.swift +++ b/Tests/CarthageKitTests/ProxyTests.swift @@ -5,7 +5,7 @@ import Quick class ProxySpec: QuickSpec { override func spec() { - describe("createProxyWithNoProxyValues") { + describe("createProxyWithoutProxyValues") { let proxy = Proxy(environment: [:]) it("should have nil dictionary") { @@ -36,6 +36,12 @@ class ProxySpec: QuickSpec { expect(proxy.connectionProxyDictionary![kCFNetworkProxiesHTTPSProxy]).to(beNil()) expect(proxy.connectionProxyDictionary![kCFNetworkProxiesHTTPSPort]).to(beNil()) } + + #if os(OSX) + it("should not set the proxy exceptions") { + expect(proxy.connectionProxyDictionary![kCFNetworkProxiesExceptionsList]).to(beNil()) + } + #endif } describe("createProxyWithHTTPSValues") { @@ -53,7 +59,36 @@ class ProxySpec: QuickSpec { expect(proxy.connectionProxyDictionary![kCFNetworkProxiesHTTPProxy]).to(beNil()) expect(proxy.connectionProxyDictionary![kCFNetworkProxiesHTTPPort]).to(beNil()) } + + #if os(OSX) + it("should not set the proxy exceptions") { + expect(proxy.connectionProxyDictionary![kCFNetworkProxiesExceptionsList]).to(beNil()) + } + #endif } + + #if os(OSX) + describe("createNoProxy") { + let proxy = Proxy(environment: ["no_proxy": "example.com,.github.com", "NO_PROXY": "example.com,.github.com"]) + + it("should set the proxy exceptions") { + expect(proxy.connectionProxyDictionary).toNot(beNil()) + expect(proxy.connectionProxyDictionary![kCFNetworkProxiesExceptionsList] as? [String]) == ["example.com", ".github.com"] + } + + it("should not set the http properties") { + expect(proxy.connectionProxyDictionary![kCFNetworkProxiesHTTPEnable]).to(beNil()) + expect(proxy.connectionProxyDictionary![kCFNetworkProxiesHTTPProxy]).to(beNil()) + expect(proxy.connectionProxyDictionary![kCFNetworkProxiesHTTPPort]).to(beNil()) + } + + it("should not set the https properties") { + expect(proxy.connectionProxyDictionary![kCFNetworkProxiesHTTPSEnable]).to(beNil()) + expect(proxy.connectionProxyDictionary![kCFNetworkProxiesHTTPSProxy]).to(beNil()) + expect(proxy.connectionProxyDictionary![kCFNetworkProxiesHTTPSPort]).to(beNil()) + } + } + #endif describe("createProxyWithHTTPAndHTTPSValues") { let proxy = Proxy(environment: [ diff --git a/Tests/CarthageKitTests/ResolverSpec.swift b/Tests/CarthageKitTests/ResolverSpec.swift index 474d061726..c8e4450373 100644 --- a/Tests/CarthageKitTests/ResolverSpec.swift +++ b/Tests/CarthageKitTests/ResolverSpec.swift @@ -14,7 +14,7 @@ private func ==(lhs: [(A, B)], rhs: [(A, B)]) -> Boo return true } -private func equal(_ expectedValue: [(A, B)]?) -> Predicate<[(A, B)]> { +private func equal(_ expectedValue: [(A, B)]?) -> Nimble.Predicate<[(A, B)]> { return Predicate.define("equal <\(stringify(expectedValue))>") { actualExpression, message in let actualValue = try actualExpression.evaluate() if expectedValue == nil || actualValue == nil { diff --git a/Tests/CarthageKitTests/Resources/BinaryOnly/successful.json b/Tests/CarthageKitTests/Resources/BinaryOnly/successful.json index fa47b696e9..876cf8f021 100644 --- a/Tests/CarthageKitTests/Resources/BinaryOnly/successful.json +++ b/Tests/CarthageKitTests/Resources/BinaryOnly/successful.json @@ -1,4 +1,4 @@ { - "1.0": "https://my.domain.com/release/1.0.0/framework.zip", - "1.0.1": "https://my.domain.com/release/1.0.1/framework.zip" + "1.0": "https://example.com/release/1.0.0/framework.zip", + "1.0.1": "https://example.com/release/1.0.1/framework.zip" } diff --git a/Tests/CarthageKitTests/SimulatorSpec.swift b/Tests/CarthageKitTests/SimulatorSpec.swift index ac0f76faa2..5c3d943b42 100644 --- a/Tests/CarthageKitTests/SimulatorSpec.swift +++ b/Tests/CarthageKitTests/SimulatorSpec.swift @@ -1,4 +1,5 @@ @testable import CarthageKit +import struct XCDBLD.SDK import Foundation import Nimble import Quick @@ -90,22 +91,27 @@ class SimulatorSpec: QuickSpec { } } + func selectAvailableSimulator(ofHeuristic heuristic: String, from data: Data) -> Simulator? { + let source = SDK(name: "", simulatorHeuristic: "Simulator - \(heuristic)") + return CarthageKit.selectAvailableSimulator(of: source, from: data) + } + describe("selectAvailableSimulator(of:from:)") { context("when there are available simulators") { context("Xcode 10.0 or lower") { it("should return the first simulator of the latest version") { let data = loadJSON(for: "Simulators/availables") - let iPhoneSimulator = selectAvailableSimulator(of: .iPhoneSimulator, from: data)! + let iPhoneSimulator = selectAvailableSimulator(ofHeuristic: "iOS", from: data)! expect(iPhoneSimulator.udid).to(equal(UUID(uuidString: "A52BF797-F6F8-47F1-B559-68B66B553B23")!)) expect(iPhoneSimulator.isAvailable).to(beTrue()) expect(iPhoneSimulator.name).to(equal("iPhone 5s")) - let watchSimulator = selectAvailableSimulator(of: .watchSimulator, from: data)! + let watchSimulator = selectAvailableSimulator(ofHeuristic: "watchOS", from: data)! expect(watchSimulator.udid).to(equal(UUID(uuidString: "290C3D57-0FF0-407F-B33C-F1A55EA44019")!)) expect(watchSimulator.isAvailable).to(beTrue()) expect(watchSimulator.name).to(equal("Apple Watch - 38mm")) - let tvSimulator = selectAvailableSimulator(of: .tvSimulator, from: data) + let tvSimulator = selectAvailableSimulator(ofHeuristic: "tvOS", from: data) expect(tvSimulator).to(beNil()) } } @@ -113,17 +119,17 @@ class SimulatorSpec: QuickSpec { context("Xcode 10.1 beta") { it("should return the first simulator of the latest version") { let data = loadJSON(for: "Simulators/availables-xcode101-beta") - let iPhoneSimulator = selectAvailableSimulator(of: .iPhoneSimulator, from: data)! + let iPhoneSimulator = selectAvailableSimulator(ofHeuristic: "iOS", from: data)! expect(iPhoneSimulator.udid).to(equal(UUID(uuidString: "A52BF797-F6F8-47F1-B559-68B66B553B23")!)) expect(iPhoneSimulator.isAvailable).to(beTrue()) expect(iPhoneSimulator.name).to(equal("iPhone 5s")) - let watchSimulator = selectAvailableSimulator(of: .watchSimulator, from: data)! + let watchSimulator = selectAvailableSimulator(ofHeuristic: "watchOS", from: data)! expect(watchSimulator.udid).to(equal(UUID(uuidString: "290C3D57-0FF0-407F-B33C-F1A55EA44019")!)) expect(watchSimulator.isAvailable).to(beTrue()) expect(watchSimulator.name).to(equal("Apple Watch - 38mm")) - let tvSimulator = selectAvailableSimulator(of: .tvSimulator, from: data) + let tvSimulator = selectAvailableSimulator(ofHeuristic: "tvOS", from: data) expect(tvSimulator).to(beNil()) } } @@ -131,17 +137,17 @@ class SimulatorSpec: QuickSpec { context("Xcode 10.1") { it("should return the first simulator of the latest version") { let data = loadJSON(for: "Simulators/availables-xcode101") - let iPhoneSimulator = selectAvailableSimulator(of: .iPhoneSimulator, from: data)! + let iPhoneSimulator = selectAvailableSimulator(ofHeuristic: "iOS", from: data)! expect(iPhoneSimulator.udid).to(equal(UUID(uuidString: "A52BF797-F6F8-47F1-B559-68B66B553B23")!)) expect(iPhoneSimulator.isAvailable).to(beTrue()) expect(iPhoneSimulator.name).to(equal("iPhone 5s")) - let watchSimulator = selectAvailableSimulator(of: .watchSimulator, from: data)! + let watchSimulator = selectAvailableSimulator(ofHeuristic: "watchOS", from: data)! expect(watchSimulator.udid).to(equal(UUID(uuidString: "290C3D57-0FF0-407F-B33C-F1A55EA44019")!)) expect(watchSimulator.isAvailable).to(beTrue()) expect(watchSimulator.name).to(equal("Apple Watch - 38mm")) - let tvSimulator = selectAvailableSimulator(of: .tvSimulator, from: data) + let tvSimulator = selectAvailableSimulator(ofHeuristic: "tvOS", from: data) expect(tvSimulator).to(beNil()) } } @@ -149,17 +155,17 @@ class SimulatorSpec: QuickSpec { context("When the latest installed simulator is unavailable") { it("should return the first simulator of the latest version") { let data = loadJSON(for: "Simulators/availables-xcode102-with-unavailable-latest-simulators") - let iPhoneSimulator = selectAvailableSimulator(of: .iPhoneSimulator, from: data)! + let iPhoneSimulator = selectAvailableSimulator(ofHeuristic: "iOS", from: data)! expect(iPhoneSimulator.udid).to(equal(UUID(uuidString: "12972BD8-0153-452B-83F7-F253EA75C4FE")!)) expect(iPhoneSimulator.isAvailable).to(beTrue()) expect(iPhoneSimulator.name).to(equal("iPhone 5s")) - let watchSimulator = selectAvailableSimulator(of: .watchSimulator, from: data)! + let watchSimulator = selectAvailableSimulator(ofHeuristic: "watchOS", from: data)! expect(watchSimulator.udid).to(equal(UUID(uuidString: "3E3C4790-EB16-445B-9C39-2BD22C54B37A")!)) expect(watchSimulator.isAvailable).to(beTrue()) expect(watchSimulator.name).to(equal("Apple Watch Series 2 - 38mm")) - let tvSimulator = selectAvailableSimulator(of: .tvSimulator, from: data)! + let tvSimulator = selectAvailableSimulator(ofHeuristic: "tvOS", from: data)! expect(tvSimulator.udid).to(equal(UUID(uuidString: "4747A322-2660-4025-B1F7-90373369F808")!)) expect(tvSimulator.isAvailable).to(beTrue()) expect(tvSimulator.name).to(equal("Apple TV")) @@ -169,17 +175,17 @@ class SimulatorSpec: QuickSpec { context("Xcode 10.2 beta") { it("should return the first simulator of the latest version") { let data = loadJSON(for: "Simulators/availables-xcode102-beta") - let iPhoneSimulator = selectAvailableSimulator(of: .iPhoneSimulator, from: data)! + let iPhoneSimulator = selectAvailableSimulator(ofHeuristic: "iOS", from: data)! expect(iPhoneSimulator.udid).to(equal(UUID(uuidString: "A52BF797-F6F8-47F1-B559-68B66B553B23")!)) expect(iPhoneSimulator.isAvailable).to(beTrue()) expect(iPhoneSimulator.name).to(equal("iPhone 5s")) - let watchSimulator = selectAvailableSimulator(of: .watchSimulator, from: data)! + let watchSimulator = selectAvailableSimulator(ofHeuristic: "watchOS", from: data)! expect(watchSimulator.udid).to(equal(UUID(uuidString: "290C3D57-0FF0-407F-B33C-F1A55EA44019")!)) expect(watchSimulator.isAvailable).to(beTrue()) expect(watchSimulator.name).to(equal("Apple Watch - 38mm")) - let tvSimulator = selectAvailableSimulator(of: .tvSimulator, from: data) + let tvSimulator = selectAvailableSimulator(ofHeuristic: "tvOS", from: data) expect(tvSimulator).to(beNil()) } } @@ -188,7 +194,7 @@ class SimulatorSpec: QuickSpec { context("when there is no available simulator") { it("should return nil") { let data = loadJSON(for: "Simulators/unavailable") - expect(selectAvailableSimulator(of: .watchSimulator, from: data)).to(beNil()) + expect(selectAvailableSimulator(ofHeuristic: "watchOS", from: data)).to(beNil()) } } } @@ -205,6 +211,43 @@ class SimulatorSpec: QuickSpec { let platformVersion = parsePlatformVersion(for: "iOS", from: "com.apple.CoreSimulator.SimRuntime.iOS-12-1") expect(platformVersion).to(equal("iOS 12.1")) } + + it("should return the platform case-insensitively") { + zip( + ["ioS", "IOS", "ios"], + repeatElement("com.apple.CoreSimulator.SimRuntime.iOS-12-1", count: .max) + ).forEach { + _ = expect(parsePlatformVersion(for: $0, from: $1)) == "iOS 12.1" + } + } + + it("should validly extract a platform id ending encompasing number") { + let platformVersion = parsePlatformVersion(for: "PlatformBox 360", from: "com.apple.CoreSimulator.SimRuntime.PlatformBox-360-12-1") + expect(platformVersion).to(equal("PlatformBox-360 12.1")) + } + + it("should validly extract a platform id ending encompasing space") { + let platformVersion = parsePlatformVersion(for: "PlatformBox One", from: "com.apple.CoreSimulator.SimRuntime.PlatformBox-One-12-1") + expect(platformVersion).to(equal("PlatformBox-One 12.1")) + } + + it("should not overdraw when platform is prefixed-true but larger") { + let platformVersion = parsePlatformVersion(for: "PlatformBox", from: "com.apple.CoreSimulator.SimRuntime.PlatformBox-One-12-1") + expect(platformVersion).to(beNil()) + } + + context("when the identifier is non-hyphenated and platform id ends with a number") { + it("should extract but fail SemanticVersion parsing") { + let platformVersion = parsePlatformVersion(for: "PlatformBox 360", from: "PlatformBox 360 12.1") + expect(platformVersion).to(equal("PlatformBox 360 12.1")) + expect( + platformVersion.map(PinnedVersion.init).map(SemanticVersion.from)?.error + ).notTo(beNil()) + // as of 2019, never seen this pop up in real world usage, but + // users (as above) users should see success under newer Xcodes + // emitting reverse-dns hyphenated syntax + } + } } } diff --git a/Tests/CarthageKitTests/VersionFileSpec.swift b/Tests/CarthageKitTests/VersionFileSpec.swift index 695c1579b5..bf67048b25 100644 --- a/Tests/CarthageKitTests/VersionFileSpec.swift +++ b/Tests/CarthageKitTests/VersionFileSpec.swift @@ -50,6 +50,8 @@ class VersionFileSpec: QuickSpec { it("should write and read back a version file correctly") { let framework = CachedFramework(name: "TestFramework", + container: nil, + libraryIdentifier: nil, hash: "TestHASH", linking: .dynamic, swiftToolchainVersion: "4.2 (swiftlang-1000.11.37.1 clang-1000.11.45.1)") @@ -124,9 +126,9 @@ class VersionFileSpec: QuickSpec { expect(iosFramework["swiftToolchainVersion"]) == "4.2 (swiftlang-1000.11.37.1 clang-1000.11.45.1)" } - func validate(file: VersionFile, matches: Bool, platform: Platform, commitish: String, hashes: [String?], + func validate(file: VersionFile, matches: Bool, platform: String, commitish: String, hashes: [String?], swiftVersionMatches: [Bool], fileName: FileString = #file, line: UInt = #line) { - _ = file.satisfies(platform: platform, commitish: commitish, hashes: hashes, swiftVersionMatches: swiftVersionMatches) + _ = file.satisfies(platform: SDK(rawValue: platform)!, commitish: commitish, hashes: hashes, swiftVersionMatches: swiftVersionMatches) .on(value: { didMatch in expect(didMatch, file: fileName, line: line) == matches }) @@ -139,37 +141,37 @@ class VersionFileSpec: QuickSpec { // Everything matches validate( - file: versionFile, matches: true, platform: .iOS, + file: versionFile, matches: true, platform: "iphoneos", commitish: "v1.0", hashes: ["ios-framework1-hash", "ios-framework2-hash"], swiftVersionMatches: [true, true] ) // One framework missing validate( - file: versionFile, matches: false, platform: .iOS, + file: versionFile, matches: false, platform: "iphoneos", commitish: "v1.0", hashes: ["ios-framework1-hash", nil], swiftVersionMatches: [true, true] ) // One Swift version mismatch validate( - file: versionFile, matches: false, platform: .iOS, + file: versionFile, matches: false, platform: "iphoneos", commitish: "v1.0", hashes: ["ios-framework1-hash", "ios-framework2-hash"], swiftVersionMatches: [true, false] ) // Mismatched commitish validate( - file: versionFile, matches: false, platform: .iOS, + file: versionFile, matches: false, platform: "iphoneos", commitish: "v1.1", hashes: ["ios-framework1-hash", "ios-framework2-hash"], swiftVersionMatches: [true, true] ) // Version file has empty array for platform validate( - file: versionFile, matches: true, platform: .tvOS, + file: versionFile, matches: true, platform: "tvos", commitish: "v1.0", hashes: [nil, nil], swiftVersionMatches: [true, true] ) // Version file has no entry for platform, should match validate( - file: versionFile, matches: false, platform: .watchOS, + file: versionFile, matches: false, platform: "watchOS", commitish: "v1.0", hashes: [nil, nil], swiftVersionMatches: [true, true] ) } @@ -188,7 +190,7 @@ class VersionFileSpec: QuickSpec { let versionFile: VersionFile! = try? JSONDecoder().decode(VersionFile.self, from: jsonData) validate( - file: versionFile, matches: true, platform: .iOS, + file: versionFile, matches: true, platform: "iphoneos", commitish: "v1.0", hashes: ["TestHASH"], swiftVersionMatches: [true] ) } @@ -196,19 +198,34 @@ class VersionFileSpec: QuickSpec { it("should compute the relative paths of static and dynamic frameworks") { let dynamicFramework = CachedFramework( name: "TestFramework", + container: nil, + libraryIdentifier: nil, hash: "TestHASH", linking: .dynamic, swiftToolchainVersion: "4.2 (swiftlang-1000.11.37.1 clang-1000.11.45.1)" ) let staticFramework = CachedFramework( name: "TestFramework", + container: nil, + libraryIdentifier: nil, hash: "TestHASH", linking: .static, swiftToolchainVersion: "4.2 (swiftlang-1000.11.37.1 clang-1000.11.45.1)" ) + let xcframeworkFramework = CachedFramework( + name: "TestFramework", + container: "TestFramework.xcframework", + libraryIdentifier: "ios-arm64_x86_64-simulator", + hash: "TestHASH", + linking: nil, + swiftToolchainVersion: "5.3 (swiftlang-1200.0.29.2 clang-1200.0.30.1)" + ) + let buildDirectory = URL(fileURLWithPath: "/TestBuild") - expect(dynamicFramework.relativePath) == "TestFramework.framework" - expect(staticFramework.relativePath) == "Static/TestFramework.framework" + expect(dynamicFramework.location(in: buildDirectory, sdk: .iOS).path) == "/TestBuild/iOS/TestFramework.framework" + expect(staticFramework.location(in: buildDirectory, sdk: .iOS).path) == "/TestBuild/iOS/Static/TestFramework.framework" + expect(xcframeworkFramework.location(in: buildDirectory, sdk: .iOS).path) == + "/TestBuild/TestFramework.xcframework/ios-arm64_x86_64-simulator/TestFramework.framework" } } } diff --git a/Tests/CarthageKitTests/XcodeSpec.swift b/Tests/CarthageKitTests/XcodeSpec.swift index 5f071e33af..e2bdbd0e50 100644 --- a/Tests/CarthageKitTests/XcodeSpec.swift +++ b/Tests/CarthageKitTests/XcodeSpec.swift @@ -10,27 +10,42 @@ import XCDBLD class XcodeSpec: QuickSpec { override func spec() { - // The fixture is maintained at https://github.com/ikesyo/carthage-fixtures-ReactiveCocoaLayout - let directoryURL = Bundle(for: type(of: self)).url(forResource: "carthage-fixtures-ReactiveCocoaLayout-master", withExtension: nil)! - let projectURL = directoryURL.appendingPathComponent("ReactiveCocoaLayout.xcodeproj") + let directoryURL = Bundle(for: type(of: self)).url(forResource: "SampleMultipleSubprojectsSameOldSameOld", withExtension: nil)! + let projectURL = directoryURL.appendingPathComponent("SampleMultipleSubprojects.xcworkspace") let buildFolderURL = directoryURL.appendingPathComponent(Constants.binariesFolderPath) + + let selectableFrameworks = [ + "iOS": "SampleiOSFramework", + "Mac": "SampleMacFramework", + "tvOS": "SampleTVFramework", + "watchOS": "SampleWatchFramework", + ] + let targetFolderURL = URL( fileURLWithPath: (NSTemporaryDirectory() as NSString).appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString), isDirectory: true ) beforeEach { - _ = try? FileManager.default.removeItem(at: buildFolderURL) + _ = try? FileManager.default.removeItem(at: buildFolderURL.deletingLastPathComponent()) + expect { try FileManager.default.createDirectory(at: directoryURL.appendingPathComponent(Constants.checkoutsFolderPath), withIntermediateDirectories: true) }.notTo(throwError()) + expect(ReactiveTask.Task( + "/bin/zsh", + arguments: ["--no-globalrcs", "--no-rcs", "-c", "aa archive -d ${PWD} -o /dev/stdout | aa extract -d Carthage/Checkouts/SampleMultipleSubprojects"], + workingDirectoryPath: directoryURL.path + ).launch().wait().error).to(beNil()) expect { try FileManager.default.createDirectory(atPath: targetFolderURL.path, withIntermediateDirectories: true) }.notTo(throwError()) } afterEach { _ = try? FileManager.default.removeItem(at: targetFolderURL) + _ = try? FileManager.default.removeItem(at: buildFolderURL.deletingLastPathComponent()) } describe("determineSwiftInformation:") { let currentSwiftVersion = swiftVersion().single()?.value #if !SWIFT_PACKAGE + /// Note: Most/some aspects of this are covered in cases that _are_ in swift-package built tests, such as "should determine that an ObjC framework is not a Swift framework" let testSwiftFramework = "Quick.framework" let currentDirectory = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true) let testSwiftFrameworkURL = currentDirectory.appendingPathComponent(testSwiftFramework) @@ -154,145 +169,6 @@ class XcodeSpec: QuickSpec { } } - it("should build for all platforms") { - let dependencies = [ - Dependency.gitHub(.dotCom, Repository(owner: "github", name: "Archimedes")), - Dependency.gitHub(.dotCom, Repository(owner: "ReactiveCocoa", name: "ReactiveCocoa")), - ] - let version = PinnedVersion("0.1") - - for dependency in dependencies { - let result = build(dependency: dependency, version: version, directoryURL, withOptions: BuildOptions(configuration: "Debug")) - .ignoreTaskData() - .on(value: { project, scheme in // swiftlint:disable:this end_closure - NSLog("Building scheme \"\(scheme)\" in \(project)") - }) - .wait() - - expect(result.error).to(beNil()) - } - - let result = buildInDirectory(directoryURL, withOptions: BuildOptions(configuration: "Debug"), rootDirectoryURL: directoryURL) - .ignoreTaskData() - .on(value: { project, scheme in // swiftlint:disable:this closure_params_parantheses - NSLog("Building scheme \"\(scheme)\" in \(project)") - }) - .wait() - - expect(result.error).to(beNil()) - - // Verify that the build products exist at the top level. - var dependencyNames = dependencies.map { dependency in dependency.name } - dependencyNames.append("ReactiveCocoaLayout") - - for dependency in dependencyNames { - let macPath = buildFolderURL.appendingPathComponent("Mac/\(dependency).framework").path - let macdSYMPath = (macPath as NSString).appendingPathExtension("dSYM")! - let iOSPath = buildFolderURL.appendingPathComponent("iOS/\(dependency).framework").path - let iOSdSYMPath = (iOSPath as NSString).appendingPathExtension("dSYM")! - - for path in [ macPath, macdSYMPath, iOSPath, iOSdSYMPath ] { - expect(path).to(beExistingDirectory()) - } - } - let frameworkFolderURL = buildFolderURL.appendingPathComponent("iOS/ReactiveCocoaLayout.framework") - - // Verify that the iOS framework is a universal binary for device - // and simulator. - let architectures = architecturesInPackage(frameworkFolderURL) - .collect() - .single() - - expect(architectures?.value).to(contain("i386", "armv7", "arm64")) - - // Verify that our dummy framework in the RCL iOS scheme built as - // well. - let auxiliaryFrameworkPath = buildFolderURL.appendingPathComponent("iOS/AuxiliaryFramework.framework").path - expect(auxiliaryFrameworkPath).to(beExistingDirectory()) - - // Copy ReactiveCocoaLayout.framework to the temporary folder. - let targetURL = targetFolderURL.appendingPathComponent("ReactiveCocoaLayout.framework", isDirectory: true) - - let resultURL = copyProduct(frameworkFolderURL, targetURL).single() - expect(resultURL?.value) == targetURL - expect(targetURL.path).to(beExistingDirectory()) - - let strippingResult = stripFramework(targetURL, keepingArchitectures: [ "armv7", "arm64" ], strippingDebugSymbols: true, codesigningIdentity: "-").wait() - expect(strippingResult.value).notTo(beNil()) - - let strippedArchitectures = architecturesInPackage(targetURL) - .collect() - .single() - - expect(strippedArchitectures?.value).notTo(contain("i386")) - expect(strippedArchitectures?.value).to(contain("armv7", "arm64")) - - /// Check whether the resulting framework contains debug symbols - /// There are many suggestions on how to do this but no one single - /// accepted way. This seems to work best: - /// https://lists.apple.com/archives/unix-porting/2006/Feb/msg00021.html - let hasDebugSymbols = SignalProducer { () -> Result in binaryURL(targetURL) } - .flatMap(.merge) { binaryURL -> SignalProducer in - let nmTask = Task("/usr/bin/xcrun", arguments: [ "nm", "-ap", binaryURL.path]) - return nmTask.launch() - .ignoreTaskData() - .mapError(CarthageError.taskError) - .map { String(data: $0, encoding: .utf8) ?? "" } - .flatMap(.merge) { output -> SignalProducer in - return SignalProducer(value: output.contains("SO ")) - } - }.single() - - expect(hasDebugSymbols?.value).to(equal(false)) - - let modulesDirectoryURL = targetURL.appendingPathComponent("Modules", isDirectory: true) - expect(FileManager.default.fileExists(atPath: modulesDirectoryURL.path)) == false - - var output: String = "" - let codeSign = Task("/usr/bin/xcrun", arguments: [ "codesign", "--verify", "--verbose", targetURL.path ]) - - let codesignResult = codeSign.launch() - .on(value: { taskEvent in - switch taskEvent { - case let .standardError(data): - output += String(data: data, encoding: .utf8)! - - default: - break - } - }) - .wait() - - expect(codesignResult.value).notTo(beNil()) - expect(output).to(contain("satisfies its Designated Requirement")) - } - - it("should build all subprojects for all platforms by default") { - let multipleSubprojects = "SampleMultipleSubprojects" - let _directoryURL = Bundle(for: type(of: self)).url(forResource: multipleSubprojects, withExtension: nil)! - - let result = buildInDirectory(_directoryURL, withOptions: BuildOptions(configuration: "Debug"), rootDirectoryURL: directoryURL) - .ignoreTaskData() - .on(value: { project, scheme in // swiftlint:disable:this end_closure - NSLog("Building scheme \"\(scheme)\" in \(project)") - }) - .wait() - - expect(result.error).to(beNil()) - - let expectedPlatformsFrameworks = [ - ("iOS", "SampleiOSFramework"), - ("Mac", "SampleMacFramework"), - ("tvOS", "SampleTVFramework"), - ("watchOS", "SampleWatchFramework"), - ] - - for (platform, framework) in expectedPlatformsFrameworks { - let path = buildFolderURL.appendingPathComponent("\(platform)/\(framework).framework").path - expect(path).to(beExistingDirectory()) - } - } - it("should skip projects without shared framework schems") { let dependency = "SchemeDiscoverySampleForCarthage" let _directoryURL = Bundle(for: type(of: self)).url(forResource: "\(dependency)-0.2", withExtension: nil)! @@ -355,71 +231,163 @@ class XcodeSpec: QuickSpec { expect(result.error?.description) == "Dependency \"Swell-0.5.0\" has no shared framework schemes for any of the platforms: Mac" } - it("should build for one platform") { - let dependency = Dependency.gitHub(.dotCom, Repository(owner: "github", name: "Archimedes")) - let version = PinnedVersion("0.1") - let result = build(dependency: dependency, version: version, directoryURL, withOptions: BuildOptions(configuration: "Debug", platforms: [ .macOS ])) - .ignoreTaskData() - .on(value: { project, scheme in - NSLog("Building scheme \"\(scheme)\" in \(project)") - }) - .wait() - - expect(result.error).to(beNil()) - - // Verify that the build product exists at the top level. - let path = buildFolderURL.appendingPathComponent("Mac/\(dependency.name).framework").path - expect(path).to(beExistingDirectory()) - - // Verify that the version file exists. - let versionFileURL = URL(fileURLWithPath: buildFolderURL.appendingPathComponent(".Archimedes.version").path) - let versionFile = VersionFile(url: versionFileURL) - expect(versionFile).notTo(beNil()) - - // Verify that the other platform wasn't built. - let incorrectPath = buildFolderURL.appendingPathComponent("iOS/\(dependency.name).framework").path - expect(FileManager.default.fileExists(atPath: incorrectPath, isDirectory: nil)) == false - } + for (condition, platforms) in [ + ["Mac"]: [SDK.macOS] as Set?, + ["iOS", "Mac"]: [SDK.iOS, SDK.macOS], + Array(selectableFrameworks.keys): nil, + ] { + it("should build for \(condition.count == 1 ? "solely " : "")platform \(condition.joined(separator: ",")) in total") { + let dependency = Dependency.gitHub(.dotCom, Repository(owner: "Carthage", name: "SampleMultipleSubprojects")) + let version = PinnedVersion("0.1") + let result = build(dependency: dependency, version: version, directoryURL, withOptions: BuildOptions(configuration: "Debug", platforms: platforms)) + .ignoreTaskData() + .on(value: { project, scheme in + NSLog("Building scheme \"\(scheme)\" in \(project)") + }) + .wait() - it("should build for multiple platforms") { - let dependency = Dependency.gitHub(.dotCom, Repository(owner: "github", name: "Archimedes")) - let version = PinnedVersion("0.1") - let result = build(dependency: dependency, version: version, directoryURL, withOptions: BuildOptions(configuration: "Debug", platforms: [ .macOS, .iOS ])) - .ignoreTaskData() - .on(value: { project, scheme in - NSLog("Building scheme \"\(scheme)\" in \(project)") - }) - .wait() + expect(result.error).to(beNil()) - expect(result.error).to(beNil()) + // Verify that the build products exist at the top level. + var expectationsForPaths = condition + .map { buildFolderURL.appendingPathComponent($0 + "/" + "\(selectableFrameworks[$0] ?? "").framework") } + .map { ($0.path, beExistingDirectory()) } + + expectationsForPaths += [( + buildFolderURL.appendingPathComponent(".SampleMultipleSubprojects.version").path, + Nimble.Predicate { + [FileManager.default.fileExists(atPath: try! $0.evaluate()!)] + .map { ($0, ExpectationMessage.fail("")) } + .map(PredicateResult.init).first! + } + )] + + for (path, expectation) in expectationsForPaths { expect(path).to(expectation) } + expect(VersionFile(url: URL(fileURLWithPath: expectationsForPaths.last!.0))).notTo(beNil()) + + // Verify that the other platforms did not build. + let pathsWithNoExpectedBuild: [String] = selectableFrameworks.keys.reduce(into: []) { + if condition.contains($1) { return } + $0.append(buildFolderURL.appendingPathComponent($1 + "/" + "\(selectableFrameworks[$1] ?? "").framework").path) + $0.append(($0.last! as NSString).appendingPathExtension("dSYM")!) + } - // Verify that the build products of all specified platforms exist - // at the top level. - let macPath = buildFolderURL.appendingPathComponent("Mac/\(dependency.name).framework").path - let iosPath = buildFolderURL.appendingPathComponent("iOS/\(dependency.name).framework").path + for path in pathsWithNoExpectedBuild { expect(path).toNot(beExistingDirectory()) } - for path in [ macPath, iosPath ] { - expect(path).to(beExistingDirectory()) + if condition == Array(selectableFrameworks.keys) { + // Verify that the build products exist at the top level. + let paths: [String] = selectableFrameworks.keys.reduce(into: []) { + $0.append(buildFolderURL.appendingPathComponent($1 + "/" + "\(selectableFrameworks[$1] ?? "").framework").path) + $0.append(($0.last! as NSString).appendingPathExtension("dSYM")!) + } + + for path in paths { expect(path).to(beExistingDirectory()) } + + let frameworkFolderURL = [ + paths.first(where: { Array.firstIndex($0.split(separator: "/"))(of: "iOS") != nil && $0.hasSuffix("dSYM") == false }) + ] + .map { $0 ?? "/var/empty" } + .map { URL.init(fileURLWithPath: $0) } + .first! + + // Verify that the iOS framework is a universal binary for device + // and simulator. + let architectures = architecturesInPackage(frameworkFolderURL) + .single() + + expect(architectures?.value).to(contain("arm64")) + expect(architectures?.value).to(satisfyAnyOf( + contain("i386"), + contain("x86_64") + )) + + // Copy SampleiOSFramework.framework to the temporary folder. + let targetURL = targetFolderURL.appendingPathComponent("iOS/" + "\(selectableFrameworks["iOS"]!).framework", isDirectory: true) + + let resultURL = copyProduct(frameworkFolderURL, targetURL).single() + expect(resultURL?.value) == targetURL + expect(targetURL.path).to(beExistingDirectory()) + + let strippingResult = stripFramework(targetURL, keepingArchitectures: [ "armv7", "arm64" ], strippingDebugSymbols: true, codesigningIdentity: "-").wait() + expect(strippingResult.value).notTo(beNil()) + + let strippedArchitectures = architecturesInPackage(targetURL) + .single() + + expect(strippedArchitectures?.value).notTo(satisfyAnyOf( + contain("i386"), + contain("x86_64") + )) + + expect(strippedArchitectures?.value).to(satisfyAnyOf( + contain("armv7"), + contain("arm64") + )) + + // TODO: Need to verify this conditional. Not exactly sure which Xcode version. + if (XcodeVersion.make()?.majorVersionNumber ?? 99999) < 14 { + expect(strippedArchitectures?.value).to(satisfyAllOf( + contain("armv7"), + contain("arm64") + )) + } + + /// Check whether the resulting framework contains debug symbols + /// There are many suggestions on how to do this but no one single + /// accepted way. This seems to work best: + /// https://lists.apple.com/archives/unix-porting/2006/Feb/msg00021.html + let hasDebugSymbols = SignalProducer { () -> Result in binaryURL(targetURL) } + .flatMap(.merge) { binaryURL -> SignalProducer in + let nmTask = Task("/usr/bin/xcrun", arguments: [ "nm", "-ap", binaryURL.path]) + return nmTask.launch() + .ignoreTaskData() + .mapError(CarthageError.taskError) + .map { String(data: $0, encoding: .utf8) ?? "" } + .flatMap(.merge) { output -> SignalProducer in + return SignalProducer(value: output.contains("SO ")) + } + }.single() + + expect(hasDebugSymbols?.value).to(equal(false)) + + let modulesDirectoryURL = targetURL.appendingPathComponent("Modules", isDirectory: true) + expect(FileManager.default.fileExists(atPath: modulesDirectoryURL.path)) == false + + var output: String = "" + let codeSign = Task("/usr/bin/xcrun", arguments: [ "codesign", "--verify", "--verbose", targetURL.path ]) + + let codesignResult = codeSign.launch() + .on(value: { taskEvent in + switch taskEvent { + case let .standardError(data): + output += String(data: data, encoding: .utf8)! + + default: + break + } + }) + .wait() + + expect(codesignResult.value).notTo(beNil()) + expect(output).to(contain("satisfies its Designated Requirement")) + } } } - it("should locate the project") { + it("should locate the workspace") { let result = ProjectLocator.locate(in: directoryURL).first() expect(result).notTo(beNil()) expect(result?.error).to(beNil()) - expect(result?.value) == .projectFile(projectURL) + expect(result?.value?.fileURL.absoluteString) == projectURL.absoluteString + expect(result?.value) == .workspace(projectURL) } - it("should locate the project from the parent directory") { + it("should locate the workspace from the parent directory") { let result = ProjectLocator.locate(in: directoryURL.deletingLastPathComponent()).collect().first() expect(result).notTo(beNil()) expect(result?.error).to(beNil()) - expect(result?.value).to(contain(.projectFile(projectURL))) - } - - it("should not locate the project from a directory not containing it") { - let result = ProjectLocator.locate(in: directoryURL.appendingPathComponent("ReactiveCocoaLayout")).first() - expect(result).to(beNil()) + expect(result?.value?.map { $0.fileURL.absoluteString }).to(contain(projectURL.absoluteString)) + expect(result?.value).to(contain(.workspace(projectURL))) } it("should build static library and place result to subdirectory") { @@ -466,11 +434,11 @@ class XcodeSpec: QuickSpec { // MARK: Matcher -internal func stillBeFramework(ofType: FrameworkType) -> Predicate { +internal func stillBeFramework(ofType: FrameworkType) -> Nimble.Predicate { return beFramework(ofType: ofType) } -internal func beFramework(ofType: FrameworkType) -> Predicate { +internal func beFramework(ofType: FrameworkType) -> Nimble.Predicate { return Predicate { actualExpression in var message = "exist and be a \(ofType == .static ? "static" : "dynamic") type" let actualPath = try actualExpression.evaluate() @@ -509,7 +477,7 @@ internal func beFramework(ofType: FrameworkType) -> Predicate { } } -internal func beExistingDirectory() -> Predicate { +internal func beExistingDirectory() -> Nimble.Predicate { return Predicate { actualExpression in var message = "exist and be a directory" let actualPath = try actualExpression.evaluate() @@ -534,7 +502,7 @@ internal func beExistingDirectory() -> Predicate { } } -internal func beRelativeSymlinkToDirectory(_ directory: URL) -> Predicate { +internal func beRelativeSymlinkToDirectory(_ directory: URL) -> Nimble.Predicate { return Predicate { actualExpression in let message = "be a relative symlink to \(directory)" let actualURL = try actualExpression.evaluate() diff --git a/Tests/XCDBLDTests/BuildArgumentsSpec.swift b/Tests/XCDBLDTests/BuildArgumentsSpec.swift index acda94af6e..506faaeb73 100644 --- a/Tests/XCDBLDTests/BuildArgumentsSpec.swift +++ b/Tests/XCDBLDTests/BuildArgumentsSpec.swift @@ -66,7 +66,9 @@ class BuildArgumentsSpec: QuickSpec { } describe("specifying the sdk") { - for sdk in SDK.allSDKs.subtracting([.macOSX]) { + let macosx = SDK.knownIn2019YearSDKs.first(where: { $0.rawValue == "macosx" })! + + for sdk in SDK.knownIn2019YearSDKs.subtracting([macosx]) { itCreatesBuildArguments("includes \(sdk) in the argument if specified", arguments: ["-sdk", sdk.rawValue]) { subject in subject.sdk = sdk } @@ -79,7 +81,7 @@ class BuildArgumentsSpec: QuickSpec { // for macOS already, just let xcodebuild figure out the SDK on its // own. itCreatesBuildArguments("does not include the sdk flag if .macOSX is specified", arguments: []) { subject in - subject.sdk = .macOSX + subject.sdk = macosx } } diff --git a/Tests/XCDBLDTests/SDKSpec.swift b/Tests/XCDBLDTests/SDKSpec.swift index 9a756791b1..33b1d4f6c9 100644 --- a/Tests/XCDBLDTests/SDKSpec.swift +++ b/Tests/XCDBLDTests/SDKSpec.swift @@ -4,51 +4,57 @@ import Quick @testable import XCDBLD -class SDKSpec: QuickSpec { +class SDKEncompassingPlatformsSpec: QuickSpec { override func spec() { - describe("\(SDK.self)") { - describe("initializer") { - it("should return nil for empty string") { - expect(SDK(rawValue: "")).to(beNil()) - } - - it("should return nil for unexpected input") { - expect(SDK(rawValue: "speakerOS")).to(beNil()) + describe("platformSimulatorlessFromHeuristic") { + it("should parse from different heuristics correctly") { + let pairs: KeyValuePairs = [ + SDK(name: "platformboxsimulator", simulatorHeuristic: "Simulator - PlatformBox"): (true, "PlatformBox"), + SDK(name: "PlatformBoxSimulator", simulatorHeuristic: ""): (true, "PlatformBox"), + SDK(name: "platformboxsimulator", simulatorHeuristic: ""): (true, "platformbox"), + SDK(name: "PlatformBox", simulatorHeuristic: ""): (false, "PlatformBox"), + SDK(name: "platformbox", simulatorHeuristic: ""): (false, "platformbox"), + SDK(name: "wAtchsiMulator", simulatorHeuristic: ""): (true, "watchOS"), + SDK(name: "macosx", simulatorHeuristic: ""): (false, "Mac"), /* special case */ + ] + + pairs.forEach { sdk, result in + expect(sdk.isSimulator) == result.0 + expect(sdk.platformSimulatorlessFromHeuristic) == result.1 } + } + } + + /* + describe("BuildPlatform") { + it("should parseSet and error where necessary") { + expect { + try BuildPlatform.parseSet(string: "ios,all") + }.to(throwError()) + + expect { + try BuildPlatform.parseSet(string: "all") + } == BuildPlatform.all + + expect { + try BuildPlatform.parseSet(string: "all,all") + } == BuildPlatform.all + + expect { + try BuildPlatform.parseSet(string: "ios") + }.notTo(throwError()) - it("should return a valid value for expected input") { - let watchOS = SDK(rawValue: "watchOS") - expect(watchOS).notTo(beNil()) - expect(watchOS) == SDK.watchOS - - let watchOSSimulator = SDK(rawValue: "wAtchsiMulator") - expect(watchOSSimulator).notTo(beNil()) - expect(watchOSSimulator) == SDK.watchSimulator - - let tvOS1 = SDK(rawValue: "tvOS") - expect(tvOS1).notTo(beNil()) - expect(tvOS1) == SDK.tvOS - - let tvOS2 = SDK(rawValue: "appletvos") - expect(tvOS2).notTo(beNil()) - expect(tvOS2) == SDK.tvOS - - let tvOSSimulator = SDK(rawValue: "appletvsimulator") - expect(tvOSSimulator).notTo(beNil()) - expect(tvOSSimulator) == SDK.tvSimulator - - let macOS = SDK(rawValue: "macosx") - expect(macOS).notTo(beNil()) - expect(macOS) == SDK.macOSX - - let iOS = SDK(rawValue: "iphoneos") - expect(iOS).notTo(beNil()) - expect(iOS) == SDK.iPhoneOS - - let iOSimulator = SDK(rawValue: "iphonesimulator") - expect(iOSimulator).notTo(beNil()) - expect(iOSimulator) == SDK.iPhoneSimulator - } + expect { + try BuildPlatform.parseSet(string: "all,ios") + }.to(throwError()) + */ + + describe("Associated Sets of Known-In-2019-Year SDKs") { + it("should map correctly") { + expect(SDK.associatedSetOfKnownIn2019YearSDKs("TVOS").map { $0.rawValue }.sorted()) + == [ "appletvos", "appletvsimulator" ] + expect(SDK.associatedSetOfKnownIn2019YearSDKs("ios").map { $0.rawValue }.sorted()) + == [ "iphoneos", "iphonesimulator" ] } } } diff --git a/Tests/XCDBLDTests/XcodeVersionSpec.swift b/Tests/XCDBLDTests/XcodeVersionSpec.swift index 9a940700b1..3ebb7dae62 100644 --- a/Tests/XCDBLDTests/XcodeVersionSpec.swift +++ b/Tests/XCDBLDTests/XcodeVersionSpec.swift @@ -28,6 +28,13 @@ class XcodeVersionSpec: QuickSpec { expect(version2?.buildVersion) == "9M189t" } } + + describe("major version") { + it("should return correct major version") { + let version = XcodeVersion(xcodebuildOutput: "Xcode 8.3.2\nBuild version 8E2002") + expect(version?.majorVersionNumber) == 8 + } + } } } } diff --git a/script/copy-fixtures b/script/copy-fixtures index 2441bdc0d1..5a609a5180 100755 --- a/script/copy-fixtures +++ b/script/copy-fixtures @@ -1,10 +1,10 @@ -#!/bin/bash +#!/bin/zsh --no-globalrcs --no-rcs # # Copies fixture projects into the test bundle directly, so they don't have to # be added to the Carthage workspace. This avoids confusing Xcode with # "subprojects" that are actually just originating from a fixture. -if [[ -z $1 ]]; then +if [[ -z "$1" ]]; then echo "Error: You must pass a destination directory." exit 1 fi @@ -18,3 +18,6 @@ do rm -Rf "$destination/$fixture" unzip -q -d "$destination" "$current/../Tests/CarthageKitTests/fixtures/$fixture" done + +rm -Rf "$destination/SampleMultipleSubprojectsSameOldSameOld" +ditto "$destination/SampleMultipleSubprojects" "$destination/SampleMultipleSubprojectsSameOldSameOld" diff --git a/script/strings_of_xcrun_find_ld.zsh b/script/strings_of_xcrun_find_ld.zsh new file mode 100755 index 0000000000..4f6160b689 --- /dev/null +++ b/script/strings_of_xcrun_find_ld.zsh @@ -0,0 +1,8 @@ +#!/bin/zsh --no-globalrcs --no-rcs + +# if the new linker is there, then `dirname $(xcrun --find swift))/../lib/swift_static/macosx` should not be respected as the sentinel value it held in pre-«Xcode 15» days. +# In a subsequent script — the Makefile — we look for '^only one snapshot supported' (which only exists in the old linker) to tell us. +## See for the old linker. +## See for the new linker. + +(/usr/bin/xcrun --find ld | /usr/bin/xargs /usr/bin/strings) || true