Skip to content

Commit

Permalink
Merge pull request #759 from tuist/xcframeworks
Browse files Browse the repository at this point in the history
Add a utility to build xcframeworks
  • Loading branch information
Pedro Piñera Buendía committed Dec 7, 2019
2 parents 90901a5 + c13948b commit 0b8ea58
Show file tree
Hide file tree
Showing 30 changed files with 1,579 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Expand Up @@ -10,7 +10,7 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: GitHub Action for SwiftLint
uses: pepibumur/action-swiftlint@0d4afd006bb24e4525b5afcefd4ab5e2537193ac
uses: norio-nomura/action-swiftlint@3c67ce2e382be797d968883944140ffa0113f737
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
changelog:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -29,6 +29,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
- `SimulatorController` with method to fetch the runtimes https://github.com/tuist/tuist/pull/746 by @pepibumur.
- Add RxSwift as a dependency of `TuistKit` https://github.com/tuist/tuist/pull/760 by @pepibumur.
- Add cache command https://github.com/tuist/tuist/pull/762 by @pepibumur.
- Utility to build xcframeworks https://github.com/tuist/tuist/pull/759 by @pepibumur.

### Fixed

Expand Down
42 changes: 42 additions & 0 deletions Sources/TuistCore/Models/Platform.swift
Expand Up @@ -30,6 +30,48 @@ extension Platform {
}
}

/// Returns whether the platform has simulators.
public var hasSimulators: Bool {
switch self {
case .macOS: return false
default: return true
}
}

/// It returns the destination that should be used to
/// compile a product for this platform's simulator.
public var xcodeSimulatorDestination: String? {
switch self {
case .macOS: return nil
default: return "\(caseValue) Simulator"
}
}

/// Returns the SDK of the platform's simulator
/// If the platform doesn't have simulators, like macOS, it returns nil.
public var xcodeSimulatorSDK: String? {
switch self {
case .tvOS: return "appletvsimulator"
case .iOS: return "iphonesimulator"
case .watchOS: return "watchsimulator"
case .macOS: return nil
}
}

/// Returns the SDK to build for the platform's device.
public var xcodeDeviceSDK: String {
switch self {
case .tvOS:
return "appletvos"
case .iOS:
return "iphoneos"
case .macOS:
return "macosx"
case .watchOS:
return "watchos"
}
}

public var xcodeSupportedPlatforms: String {
switch self {
case .tvOS:
Expand Down
175 changes: 175 additions & 0 deletions Sources/TuistKit/Cache/XCFrameworkBuilder.swift
@@ -0,0 +1,175 @@
import Basic
import Foundation
import TuistCore
import TuistSupport

enum XCFrameworkBuilderError: FatalError {
case nonFrameworkTarget(String)

/// Error type.
var type: ErrorType {
switch self {
case .nonFrameworkTarget: return .abort
}
}

/// Error description.
var description: String {
switch self {
case let .nonFrameworkTarget(name):
return "Can't generate an .xcframework from the target '\(name)' because it's not a framework target"
}
}
}

protocol XCFrameworkBuilding {
/// It builds an xcframework for the given target.
/// The target must have framework as product.
///
/// - Parameters:
/// - workspacePath: Path to the generated .xcworkspace that contains the given target.
/// - target: Target whose .xcframework will be generated.
/// - Returns: Path to the compiled .xcframework.
func build(workspacePath: AbsolutePath, target: Target) throws -> AbsolutePath

/// It builds an xcframework for the given target.
/// The target must have framework as product.
///
/// - Parameters:
/// - projectPath: Path to the generated .xcodeproj that contains the given target.
/// - target: Target whose .xcframework will be generated.
/// - Returns: Path to the compiled .xcframework.
func build(projectPath: AbsolutePath, target: Target) throws -> AbsolutePath
}

final class XCFrameworkBuilder: XCFrameworkBuilding {
// MARK: - Attributes

/// When true the builder outputs the output from xcodebuild.
private let printOutput: Bool

// MARK: - Init

/// Initializes the builder.
/// - Parameter printOutput: When true the builder outputs the output from xcodebuild.
init(printOutput: Bool = true) {
self.printOutput = printOutput
}

// MARK: - XCFrameworkBuilding

func build(workspacePath: AbsolutePath, target: Target) throws -> AbsolutePath {
try build(arguments: ["-workspace", workspacePath.pathString], target: target)
}

func build(projectPath: AbsolutePath, target: Target) throws -> AbsolutePath {
try build(arguments: ["-project", projectPath.pathString], target: target)
}

// MARK: - Fileprivate

fileprivate func build(arguments: [String], target: Target) throws -> AbsolutePath {
if target.product != .framework {
throw XCFrameworkBuilderError.nonFrameworkTarget(target.name)
}

// Create temporary directories
let outputDirectory = try TemporaryDirectory(removeTreeOnDeinit: false)
let derivedDataPath = try TemporaryDirectory(removeTreeOnDeinit: true)

Printer.shared.print(section: "Building .xcframework for \(target.productName)")

// Build for the device
let deviceArchivePath = derivedDataPath.path.appending(component: "device.xcarchive")
var deviceArguments = xcodebuildCommand(scheme: target.name,
destination: deviceDestination(platform: target.platform),
sdk: target.platform.xcodeDeviceSDK,
derivedDataPath: derivedDataPath.path)
deviceArguments.append(contentsOf: ["-archivePath", deviceArchivePath.pathString])
deviceArguments.append(contentsOf: arguments)
Printer.shared.print(subsection: "Building \(target.productName) for device")
try runCommand(deviceArguments)

// Build for the simulator
var simulatorArchivePath: AbsolutePath?
if target.platform.hasSimulators {
simulatorArchivePath = derivedDataPath.path.appending(component: "simulator.xcarchive")
var simulatorArguments = xcodebuildCommand(scheme: target.name,
destination: target.platform.xcodeSimulatorDestination!,
sdk: target.platform.xcodeSimulatorSDK!,
derivedDataPath: derivedDataPath.path)
simulatorArguments.append(contentsOf: ["-archivePath", simulatorArchivePath!.pathString])
simulatorArguments.append(contentsOf: arguments)
Printer.shared.print(subsection: "Building \(target.productName) for simulator")
try runCommand(simulatorArguments)
}

// Build the xcframework
Printer.shared.print(subsection: "Exporting xcframework for \(target.productName)")
let xcframeworkPath = outputDirectory.path.appending(component: "\(target.productName).xcframework")
let xcframeworkArguments = xcodebuildXcframeworkCommand(deviceArchivePath: deviceArchivePath,
simulatorArchivePath: simulatorArchivePath,
productName: target.productName,
xcframeworkPath: xcframeworkPath)
try runCommand(xcframeworkArguments)

return xcframeworkPath
}

/// Runs the given command.
/// - Parameter arguments: Command arguments.
fileprivate func runCommand(_ arguments: [String]) throws {
if printOutput {
try System.shared.runAndPrint(arguments)
} else {
try System.shared.run(arguments)
}
}

/// Returns the arguments that should be passed to xcodebuild to compile for a device on the given platform.
/// - Parameter platform: Platform we are compiling for.
fileprivate func deviceDestination(platform: Platform) -> String {
switch platform {
case .macOS: return "osx"
default: return "generic/platform=\(platform.caseValue)"
}
}

/// Returns the xcodebuild command to generate the .xcframework from the device
/// and the simulator frameworks.
///
/// - Parameters:
/// - deviceArchivePath: Path to the archive that contains the framework for the device.
/// - simulatorArchivePath: Path to the archive that contains the framework for the simulator.
/// - productName: Name of the product.
/// - xcframeworkPath: Path where the .xcframework should be exported to (e.g. /path/to/MyFeature.xcframework).
fileprivate func xcodebuildXcframeworkCommand(deviceArchivePath: AbsolutePath,
simulatorArchivePath: AbsolutePath?,
productName: String,
xcframeworkPath: AbsolutePath) -> [String] {
var command = ["xcrun", "xcodebuild", "-create-xcframework"]
command.append(contentsOf: ["-framework", deviceArchivePath.appending(RelativePath("Products/Library/Frameworks/\(productName).framework")).pathString])
if let simulatorArchivePath = simulatorArchivePath {
command.append(contentsOf: ["-framework", simulatorArchivePath.appending(RelativePath("Products/Library/Frameworks/\(productName).framework")).pathString])
}
command.append(contentsOf: ["-output", xcframeworkPath.pathString])
return command
}

/// It returns the xcodebuild command to archive the .framework.
/// - Parameters:
/// - scheme: Name of the scheme that archives the framework.
/// - destination: Compilation destination.
/// - sdk: Compilation SDK.
/// - derivedDataPath: Derived data directory.
fileprivate func xcodebuildCommand(scheme: String, destination: String, sdk: String, derivedDataPath: AbsolutePath) -> [String] {
var command = ["xcrun", "xcodebuild", "clean", "archive"]
command.append(contentsOf: ["-scheme", scheme.spm_shellEscaped()])
command.append(contentsOf: ["-sdk", sdk])
command.append(contentsOf: ["-destination='\(destination)'"])
command.append(contentsOf: ["-derivedDataPath", derivedDataPath.pathString])
// Without the BUILD_LIBRARY_FOR_DISTRIBUTION argument xcodebuild doesn't generate the .swiftinterface file
command.append(contentsOf: ["SKIP_INSTALL=NO", "BUILD_LIBRARY_FOR_DISTRIBUTION=YES"])
return command
}
}
8 changes: 8 additions & 0 deletions Sources/TuistSupportTesting/TestCase/TuistTestCase.swift
Expand Up @@ -102,4 +102,12 @@ public class TuistTestCase: XCTestCase {
"""
XCTAssertTrue(printer.standardErrorMatches(with: expected), message, file: file, line: line)
}

public func temporaryFixture(_ pathString: String) throws -> AbsolutePath {
let path = RelativePath(pathString)
let fixturePath = self.fixturePath(path: path)
let destinationPath = (try temporaryPath()).appending(component: path.basename)
try FileHandler.shared.copy(from: fixturePath, to: destinationPath)
return destinationPath
}
}
1 change: 1 addition & 0 deletions Tests/Fixtures/Frameworks/.gitignore
@@ -0,0 +1 @@
!Frameworks.xcodeproj

0 comments on commit 0b8ea58

Please sign in to comment.