Skip to content

Commit

Permalink
Merge pull request #3152 from elliottwilliams/release/0.38.0
Browse files Browse the repository at this point in the history
Release: v0.38.0
  • Loading branch information
elliottwilliams committed May 7, 2021
2 parents 19a7f97 + bd35aef commit 9a3d179
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 50 deletions.
12 changes: 11 additions & 1 deletion Documentation/Artifacts.md
Expand Up @@ -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"
}
```
6 changes: 4 additions & 2 deletions README.md
Expand Up @@ -31,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)
Expand Down Expand Up @@ -291,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 archive is free form but, __frameworks should only appear once in the archive__ as they will be copied
to `Carthage/Build/<platform>` 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
Expand Down
44 changes: 38 additions & 6 deletions Source/CarthageKit/BinaryProject.swift
Expand Up @@ -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<BinaryProject, BinaryJSONError> {
return Result<[String: String], AnyError>(attempt: { try jsonDecoder.decode([String: String].self, from: jsonData) })
.mapError { .invalidJSON($0.error) }
.flatMap { json -> Result<BinaryProject, BinaryJSONError> in
var versions = [PinnedVersion: URL]()
var versions = [PinnedVersion: [URL]]()

for (key, value) in json {
let pinnedVersion: PinnedVersion
Expand All @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion Source/CarthageKit/CarthageKitVersion.swift
Expand Up @@ -4,5 +4,5 @@ import Foundation
public struct CarthageKitVersion {
public let value: SemanticVersion

public static let current = CarthageKitVersion(value: SemanticVersion(0, 37, 0))
public static let current = CarthageKitVersion(value: SemanticVersion(0, 38, 0))
}
5 changes: 4 additions & 1 deletion Source/CarthageKit/Constants.swift
@@ -1,5 +1,6 @@
import Foundation
import Result
import Tentacle

/// A struct including all constants.
public struct Constants {
Expand Down Expand Up @@ -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.
Expand Down
131 changes: 101 additions & 30 deletions Source/CarthageKit/Project.swift
@@ -1,5 +1,6 @@
// swiftlint:disable file_length

import CommonCrypto
import Foundation
import Result
import ReactiveSwift
Expand Down Expand Up @@ -100,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
Expand Down Expand Up @@ -237,7 +238,7 @@ 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.proxiedSession.reactive.data(with: request)
.mapError { CarthageError.readFailed(binary.url, $0 as NSError) }
Expand All @@ -253,8 +254,8 @@ public final class Project { // swiftlint:disable:this type_body_length
}
.startOnQueue(self.cachedBinaryProjectsQueue)
}


/// Builds URL request
///
/// - Parameters:
Expand All @@ -264,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() {
Expand Down Expand Up @@ -717,7 +718,7 @@ public final class Project { // swiftlint:disable:this type_body_length
.then(SignalProducer<URL, CarthageError>(value: directoryURL))
}
}

/// Ensures binary framework has a valid extension and returns url in build folder
private func getBinaryFrameworkURL(url: URL) -> SignalProducer<URL, CarthageError> {
switch url.pathExtension {
Expand All @@ -743,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<Bool, CarthageError> {
private func installBinaries(for dependency: Dependency, pinnedVersion: PinnedVersion, preferXCFrameworks: Bool, toolchain: String?) -> SignalProducer<Bool, CarthageError> {
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<URL, CarthageError> in
if !client.isAuthenticated {
return SignalProducer(error: error)
Expand All @@ -756,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)
)
}
Expand Down Expand Up @@ -785,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<URL, CarthageError> {
return client.execute(repository.release(forTag: pinnedVersion.commitish))
Expand All @@ -811,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<URL, CarthageError> in
return SignalProducer<Release.Asset, CarthageError>(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<Release.Asset, CarthageError>(binaryAssetFilter(prioritizing: potentialFrameworkAssets, preferXCFrameworks: preferXCFrameworks))
.flatMap(.concat) { asset -> SignalProducer<URL, CarthageError> in
let fileURL = fileURLToCachedBinary(dependency, release, asset)

Expand Down Expand Up @@ -1005,17 +1013,21 @@ 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<SemanticVersion, ScannableError>(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)
Expand All @@ -1032,8 +1044,7 @@ public final class Project { // swiftlint:disable:this type_body_length
/// 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<URL, CarthageError> {
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)
Expand Down Expand Up @@ -1182,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)))
}
}
Expand Down Expand Up @@ -1328,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
Expand Down Expand Up @@ -1494,8 +1517,8 @@ internal func dSYMsInDirectory(_ directoryURL: URL) -> SignalProducer<URL, Carth
return filesInDirectory(directoryURL, "com.apple.xcode.dsym")
}

/// Sends the URL of the dSYM for which at least one of the UUIDs are common with
/// those of the given framework, or errors if there was an error parsing a dSYM
/// Sends the URL of the dSYM for which at least one of the UUIDs are common with
/// those of the given framework, or errors if there was an error parsing a dSYM
/// contained within the directory.
private func dSYMForFramework(_ frameworkURL: URL, inDirectoryURL directoryURL: URL) -> SignalProducer<URL, CarthageError> {
return UUIDsForFramework(frameworkURL)
Expand Down Expand Up @@ -1633,3 +1656,51 @@ public func cloneOrFetch(
}
}
}

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<A: AssetNameConvertible>(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 {}

0 comments on commit 9a3d179

Please sign in to comment.