Change the XCFrameworkBuilder to use XcodeBuildController

This commit is contained in:
Pedro Piñera 2020-02-27 19:04:46 +01:00
parent 22a5e70d82
commit a25861b0a5
22 changed files with 548 additions and 277 deletions

View File

@ -1,74 +1,97 @@
import Basic import Basic
import Foundation import Foundation
import RxSwift import RxSwift
import TuistCore
import TuistSupport import TuistSupport
import XcbeautifyLib import XcbeautifyLib
protocol XcodeBuildControlling {
/// Returns an observable to build the given project using xcodebuild.
/// - Parameters:
/// - target: The project or workspace to be built.
/// - scheme: The scheme of the project that should be built.
/// - clean: True if xcodebuild should clean the project before building.
func build(_ target: XcodeBuildTarget, scheme: String, clean: Bool) -> Observable<SystemEvent<String>>
}
public enum XcodeBuildTarget {
/// The target is an Xcode project.
case project(AbsolutePath)
/// The target is an Xcode workspace.
case workspace(AbsolutePath)
/// Returns the arguments that need to be passed to xcodebuild to build this target.
var xcodebuildArguments: [String] {
switch self {
case let .project(path):
return ["-project", path.pathString]
case let .workspace(path):
return ["-workspace", path.pathString]
}
}
}
public final class XcodeBuildController: XcodeBuildControlling { public final class XcodeBuildController: XcodeBuildControlling {
// MARK: - Attributes // MARK: - Attributes
/// Instance to format xcodebuild output. /// Instance to format xcodebuild output.
private let parser: Parsing private let parser: Parsing
public convenience init() {
self.init(parser: Parser())
}
init(parser: Parsing) { init(parser: Parsing) {
self.parser = parser self.parser = parser
} }
func build(_ target: XcodeBuildTarget, scheme: String, clean: Bool = false) -> Observable<SystemEvent<String>> { public func build(_ target: XcodeBuildTarget,
var command = ["/usr/bin/xcrun", "xcodebuild", "-scheme", scheme] scheme: String,
clean: Bool = false,
arguments: XcodeBuildArgument...) -> Observable<SystemEvent<XcodeBuildOutput>> {
var command = ["/usr/bin/xcrun", "xcodebuild"]
// Action
if clean {
command.append("clean")
}
command.append("build")
// Scheme
command.append(contentsOf: ["-scheme", scheme])
// Target // Target
command.append(contentsOf: target.xcodebuildArguments) command.append(contentsOf: target.xcodebuildArguments)
// Arguments
command.append(contentsOf: arguments.flatMap { $0.arguments })
return run(command: command)
}
public func archive(_ target: XcodeBuildTarget,
scheme: String,
clean: Bool,
archivePath: AbsolutePath,
arguments: XcodeBuildArgument...) -> Observable<SystemEvent<XcodeBuildOutput>> {
var command = ["/usr/bin/xcrun", "xcodebuild"]
// Action // Action
command.append("build")
if clean { if clean {
command.append("clean") command.append("clean")
} }
command.append("archive")
// Scheme
command.append(contentsOf: ["-scheme", scheme])
// Target
command.append(contentsOf: target.xcodebuildArguments)
// Archive path
command.append(contentsOf: ["-archivePath", archivePath.pathString])
// Arguments
command.append(contentsOf: arguments.flatMap { $0.arguments })
return run(command: command)
}
public func createXCFramework(frameworks: [AbsolutePath], output: AbsolutePath) -> Observable<SystemEvent<XcodeBuildOutput>> {
var command = ["/usr/bin/xcrun", "xcodebuild", "-create-xcframework"]
command.append(contentsOf: frameworks.flatMap { ["-framework", $0.pathString] })
command.append(contentsOf: ["-output", output.pathString])
return run(command: command)
}
fileprivate func run(command: [String]) -> Observable<SystemEvent<XcodeBuildOutput>> {
let colored = Environment.shared.shouldOutputBeColoured let colored = Environment.shared.shouldOutputBeColoured
return System.shared.observable(command, verbose: true) return System.shared.observable(command, verbose: false)
.compactMap { event -> SystemEvent<String>? in .compactMap { event -> SystemEvent<XcodeBuildOutput>? in
switch event { switch event {
case let .standardError(errorData): case let .standardError(errorData):
guard let line = String(data: errorData, encoding: .utf8) else { return nil } guard let line = String(data: errorData, encoding: .utf8) else { return nil }
guard let formatedOutput = self.parser.parse(line: line, colored: colored) else { return nil } let formatedOutput = self.parser.parse(line: line, colored: colored)
return .standardError(formatedOutput) return .standardError(XcodeBuildOutput(raw: line, formatted: formatedOutput))
case let .standardOutput(outputData): case let .standardOutput(outputData):
guard let line = String(data: outputData, encoding: .utf8) else { return nil } guard let line = String(data: outputData, encoding: .utf8) else { return nil }
guard let formatedOutput = self.parser.parse(line: line, colored: colored) else { return nil } let formatedOutput = self.parser.parse(line: line, colored: colored)
return .standardOutput(formatedOutput) return .standardOutput(XcodeBuildOutput(raw: line, formatted: formatedOutput))
} }
} }
.do(onNext: { event in
Printer.shared.print("\(event.value)")
})
} }
} }

View File

@ -1,175 +0,0 @@
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"
}
}
}
public 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
}
public 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.
public init(printOutput: Bool = true) {
self.printOutput = printOutput
}
// MARK: - XCFrameworkBuilding
public func build(workspacePath: AbsolutePath, target: Target) throws -> AbsolutePath {
try build(arguments: ["-workspace", workspacePath.pathString], target: target)
}
public 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.name)")
// 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.name) 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.name) for simulator")
try runCommand(simulatorArguments)
}
// Build the xcframework
Printer.shared.print(subsection: "Exporting xcframework for \(target.name)")
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", "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
}
}

View File

@ -0,0 +1,147 @@
import Basic
import Foundation
import RxSwift
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"
}
}
}
public protocol XCFrameworkBuilding {
/// Returns an observable build 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 -> Observable<AbsolutePath>
/// Returns an observable to build 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 -> Observable<AbsolutePath>
}
public final class XCFrameworkBuilder: XCFrameworkBuilding {
// MARK: - Attributes
/// Xcode build controller instance to run xcodebuild commands.
private let xcodeBuildController: XcodeBuildControlling
// MARK: - Init
/// Initializes the builder.
/// - Parameter xcodeBuildController: Xcode build controller instance to run xcodebuild commands.
public init(xcodeBuildController: XcodeBuildControlling) {
self.xcodeBuildController = xcodeBuildController
}
// MARK: - XCFrameworkBuilding
public func build(workspacePath: AbsolutePath, target: Target) throws -> Observable<AbsolutePath> {
try build(.workspace(workspacePath), target: target)
}
public func build(projectPath: AbsolutePath, target: Target) throws -> Observable<AbsolutePath> {
try build(.project(projectPath), target: target)
}
// MARK: - Fileprivate
fileprivate func build(_ projectTarget: XcodeBuildTarget, target: Target) throws -> Observable<AbsolutePath> {
if target.product != .framework {
throw XCFrameworkBuilderError.nonFrameworkTarget(target.name)
}
let scheme = target.name.spm_shellEscaped()
// Create temporary directories
let outputDirectory = try TemporaryDirectory(removeTreeOnDeinit: false)
let temporaryPath = try TemporaryDirectory(removeTreeOnDeinit: false)
Printer.shared.print(section: "Building .xcframework for \(target.name)")
// Build for the device
// Without the BUILD_LIBRARY_FOR_DISTRIBUTION argument xcodebuild doesn't generate the .swiftinterface file
let deviceArchivePath = temporaryPath.path.appending(component: "device.xcarchive")
let deviceArchiveObservable = xcodeBuildController.archive(projectTarget,
scheme: scheme,
clean: true,
archivePath: deviceArchivePath,
arguments:
.sdk(target.platform.xcodeDeviceSDK),
.derivedDataPath(temporaryPath.path),
.buildSetting("SKIP_INSTALL", "NO"),
.buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES"))
.do(onSubscribed: {
Printer.shared.print(subsection: "Building \(target.name) for device")
})
// Build for the simulator
var simulatorArchiveObservable: Observable<SystemEvent<XcodeBuildOutput>>?
var simulatorArchivePath: AbsolutePath?
if target.platform.hasSimulators {
simulatorArchivePath = temporaryPath.path.appending(component: "simulator.xcarchive")
simulatorArchiveObservable = xcodeBuildController.archive(projectTarget,
scheme: scheme,
clean: false,
archivePath: simulatorArchivePath!,
arguments:
.sdk(target.platform.xcodeSimulatorSDK!),
.derivedDataPath(temporaryPath.path),
.buildSetting("SKIP_INSTALL", "NO"),
.buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES"))
.do(onSubscribed: {
Printer.shared.print(subsection: "Building \(target.name) for simulator")
})
}
// Build the xcframework
var frameworkpaths = [frameworkPath(fromArchivePath: deviceArchivePath, productName: target.productName)]
if let simulatorArchivePath = simulatorArchivePath {
frameworkpaths.append(frameworkPath(fromArchivePath: simulatorArchivePath, productName: target.productName))
}
let xcframeworkPath = outputDirectory.path.appending(component: "\(target.productName).xcframework")
let xcframeworkObservable = xcodeBuildController.createXCFramework(frameworks: frameworkpaths, output: xcframeworkPath)
.do(onSubscribed: {
Printer.shared.print(subsection: "Exporting xcframework for \(target.platform.caseValue)")
})
return deviceArchiveObservable
.concat(simulatorArchiveObservable ?? Observable.empty())
.concat(xcframeworkObservable)
.ignoreElements()
.andThen(Observable.just(xcframeworkPath))
.do(afterCompleted: {
try FileHandler.shared.delete(temporaryPath.path)
})
}
/// Returns the path to the framework inside the archive.
/// - Parameters:
/// - archivePath: Path to the .xcarchive.
/// - productName: Product name.
fileprivate func frameworkPath(fromArchivePath archivePath: AbsolutePath, productName: String) -> AbsolutePath {
archivePath.appending(RelativePath("Products/Library/Frameworks/\(productName).framework"))
}
}

View File

@ -1,8 +1,8 @@
import Basic import Basic
import Foundation import Foundation
import RxSwift import RxSwift
import TuistCache
import TuistCore import TuistCore
import TuistGalaxy
public final class MockCacheStorage: CacheStoraging { public final class MockCacheStorage: CacheStoraging {
var existsStub: ((String) -> Bool)? var existsStub: ((String) -> Bool)?

View File

@ -1,7 +1,8 @@
import Basic import Basic
import Foundation import Foundation
import RxSwift
import TuistCache
import TuistCore import TuistCore
import TuistGalaxy
public final class MockXCFrameworkBuilder: XCFrameworkBuilding { public final class MockXCFrameworkBuilder: XCFrameworkBuilding {
var buildProjectArgs: [(projectPath: AbsolutePath, target: Target)] = [] var buildProjectArgs: [(projectPath: AbsolutePath, target: Target)] = []
@ -9,21 +10,21 @@ public final class MockXCFrameworkBuilder: XCFrameworkBuilding {
var buildProjectStub: AbsolutePath? var buildProjectStub: AbsolutePath?
var buildWorkspaceStub: AbsolutePath? var buildWorkspaceStub: AbsolutePath?
public func build(projectPath: AbsolutePath, target: Target) throws -> AbsolutePath { public func build(projectPath: AbsolutePath, target: Target) throws -> Observable<AbsolutePath> {
buildProjectArgs.append((projectPath: projectPath, target: target)) buildProjectArgs.append((projectPath: projectPath, target: target))
if let buildProjectStub = buildProjectStub { if let buildProjectStub = buildProjectStub {
return buildProjectStub return Observable.just(buildProjectStub)
} else { } else {
return AbsolutePath.root return Observable.just(AbsolutePath.root)
} }
} }
public func build(workspacePath: AbsolutePath, target: Target) throws -> AbsolutePath { public func build(workspacePath: AbsolutePath, target: Target) throws -> Observable<AbsolutePath> {
buildWorkspaceArgs.append((workspacePath: workspacePath, target: target)) buildWorkspaceArgs.append((workspacePath: workspacePath, target: target))
if let buildWorkspaceStub = buildWorkspaceStub { if let buildWorkspaceStub = buildWorkspaceStub {
return buildWorkspaceStub return Observable.just(buildWorkspaceStub)
} else { } else {
return AbsolutePath.root return Observable.just(AbsolutePath.root)
} }
} }
} }

View File

@ -0,0 +1,16 @@
import Foundation
import RxSwift
import TuistSupport
public extension Observable where Element == SystemEvent<XcodeBuildOutput> {
func printFormattedOutput() -> Observable<SystemEvent<XcodeBuildOutput>> {
self.do(onNext: { event in
switch event {
case let .standardError(error):
Printer.shared.print(errorMessage: "\(error.formatted ?? error.raw)")
case let .standardOutput(output):
Printer.shared.print("\(output.formatted ?? output.raw)")
}
})
}
}

View File

@ -0,0 +1,45 @@
import Basic
import Foundation
/// It represents arguments that can be passed to the xcodebuild command.
public enum XcodeBuildArgument: Equatable, CustomStringConvertible {
/// Use SDK as the name or path of the base SDK when building the project
case sdk(String)
/// Use the destination described by DESTINATIONSPECIFIER (a comma-separated set of key=value pairs describing the destination to use)
case destination(String)
/// Specifies the directory where build products and other derived data will go.
case derivedDataPath(AbsolutePath)
/// To override build settings.
case buildSetting(String, String)
/// It returns the bash arguments that represent this xcodebuild argument.
public var arguments: [String] {
switch self {
case let .sdk(sdk):
return ["-sdk", sdk]
case let .destination(destination):
return ["-destination", "\(destination)"]
case let .derivedDataPath(path):
return ["-derivedDataPath", path.pathString]
case let .buildSetting(key, value):
return ["\(key)=\(value.spm_shellEscaped())"]
}
}
/// The argument's description.
public var description: String {
switch self {
case let .sdk(sdk):
return "Xcodebuild's SDK argument: \(sdk)"
case let .destination(destination):
return "Xcodebuild's destination argument: \(destination)"
case let .derivedDataPath(path):
return "Xcodebuild's derivedDataPath argument: \(path.pathString)"
case let .buildSetting(key, value):
return "Xcodebuild's additional build setting: \(key)=\(value)"
}
}
}

View File

@ -0,0 +1,36 @@
import Basic
import Foundation
import RxSwift
import TuistSupport
public protocol XcodeBuildControlling {
/// Returns an observable to build the given project using xcodebuild.
/// - Parameters:
/// - target: The project or workspace to be built.
/// - scheme: The scheme of the project that should be built.
/// - clean: True if xcodebuild should clean the project before building.
/// - arguments: Extra xcodebuild arguments.
func build(_ target: XcodeBuildTarget,
scheme: String,
clean: Bool,
arguments: XcodeBuildArgument...) -> Observable<SystemEvent<XcodeBuildOutput>>
/// Returns an observable that archives the given project using xcodebuild.
/// - Parameters:
/// - target: The project or workspace to be archived.
/// - scheme: The scheme of the project that should be archived.
/// - clean: True if xcodebuild should clean the project before archiving.
/// - archivePath: Path where the archive will be exported (with extension .xcarchive)
/// - arguments: Extra xcodebuild arguments.
func archive(_ target: XcodeBuildTarget,
scheme: String,
clean: Bool,
archivePath: AbsolutePath,
arguments: XcodeBuildArgument...) -> Observable<SystemEvent<XcodeBuildOutput>>
/// Creates an .xcframework combining the list of given frameworks.
/// - Parameters:
/// - frameworks: Frameworks to be combined.
/// - output: Path to the output .xcframework.
func createXCFramework(frameworks: [AbsolutePath], output: AbsolutePath) -> Observable<SystemEvent<XcodeBuildOutput>>
}

View File

@ -0,0 +1,19 @@
import Foundation
/// It represents an output from the xcodebuild command.
public struct XcodeBuildOutput: Equatable {
/// Output as xcodebuild returns it.
let raw: String
/// Beautified version of the raw output.
let formatted: String?
/// Initializes the output with its arguments.
/// - Parameters:
/// - raw: Output as xcodebuild returns it.
/// - formatted: Beautified version of the raw output.
public init(raw: String, formatted: String?) {
self.raw = raw
self.formatted = formatted
}
}

View File

@ -0,0 +1,20 @@
import Basic
import Foundation
public enum XcodeBuildTarget {
/// The target is an Xcode project.
case project(AbsolutePath)
/// The target is an Xcode workspace.
case workspace(AbsolutePath)
/// Returns the arguments that need to be passed to xcodebuild to build this target.
public var xcodebuildArguments: [String] {
switch self {
case let .project(path):
return ["-project", path.pathString]
case let .workspace(path):
return ["-workspace", path.pathString]
}
}
}

View File

@ -43,7 +43,7 @@ extension Platform {
public var xcodeSimulatorDestination: String? { public var xcodeSimulatorDestination: String? {
switch self { switch self {
case .macOS: return nil case .macOS: return nil
default: return "\(caseValue) Simulator" default: return "platform=\(caseValue) Simulator"
} }
} }

View File

@ -0,0 +1,36 @@
import Basic
import Foundation
import RxSwift
import TuistCore
import TuistSupport
@testable import TuistSupportTesting
final class MockXcodeBuildController: XcodeBuildControlling {
var buildStub: ((XcodeBuildTarget, String, Bool, [XcodeBuildArgument]) -> Observable<SystemEvent<XcodeBuildOutput>>)?
func build(_ target: XcodeBuildTarget, scheme: String, clean: Bool, arguments: XcodeBuildArgument...) -> Observable<SystemEvent<XcodeBuildOutput>> {
if let buildStub = buildStub {
return buildStub(target, scheme, clean, arguments)
} else {
return Observable.error(TestError("\(String(describing: MockXcodeBuildController.self)) received an unexpected call to build"))
}
}
var archiveStub: ((XcodeBuildTarget, String, Bool, AbsolutePath, [XcodeBuildArgument]) -> Observable<SystemEvent<XcodeBuildOutput>>)?
func archive(_ target: XcodeBuildTarget, scheme: String, clean: Bool, archivePath: AbsolutePath, arguments: XcodeBuildArgument...) -> Observable<SystemEvent<XcodeBuildOutput>> {
if let archiveStub = archiveStub {
return archiveStub(target, scheme, clean, archivePath, arguments)
} else {
return Observable.error(TestError("\(String(describing: MockXcodeBuildController.self)) received an unexpected call to archive"))
}
}
var createXCFrameworkStub: (([AbsolutePath], AbsolutePath) -> Observable<SystemEvent<XcodeBuildOutput>>)?
func createXCFramework(frameworks: [AbsolutePath], output: AbsolutePath) -> Observable<SystemEvent<XcodeBuildOutput>> {
if let createXCFrameworkStub = createXCFrameworkStub {
return createXCFrameworkStub(frameworks, output)
} else {
return Observable.error(TestError("\(String(describing: MockXcodeBuildController.self)) received an unexpected call to createXCFramework"))
}
}
}

View File

@ -2,8 +2,9 @@ import Basic
import Foundation import Foundation
import RxBlocking import RxBlocking
import RxSwift import RxSwift
import TuistAutomation
import TuistCache
import TuistCore import TuistCore
import TuistGalaxy
import TuistGenerator import TuistGenerator
import TuistLoader import TuistLoader
import TuistSupport import TuistSupport
@ -32,7 +33,7 @@ final class CacheController: CacheControlling {
init(generator: Generating = Generator(), init(generator: Generating = Generator(),
manifestLoader: ManifestLoading = ManifestLoader(), manifestLoader: ManifestLoading = ManifestLoader(),
xcframeworkBuilder: XCFrameworkBuilding = XCFrameworkBuilder(printOutput: false), xcframeworkBuilder: XCFrameworkBuilding = XCFrameworkBuilder(xcodeBuildController: XcodeBuildController()),
cache: CacheStoraging = Cache(), cache: CacheStoraging = Cache(),
graphContentHasher: GraphContentHashing = GraphContentHasher()) { graphContentHasher: GraphContentHashing = GraphContentHasher()) {
self.generator = generator self.generator = generator
@ -62,9 +63,9 @@ final class CacheController: CacheControlling {
// Build targets sequentially // Build targets sequentially
let xcframeworkPath: AbsolutePath! let xcframeworkPath: AbsolutePath!
if path.extension == "xcworkspace" { if path.extension == "xcworkspace" {
xcframeworkPath = try self.xcframeworkBuilder.build(workspacePath: path, target: target.target) xcframeworkPath = try self.xcframeworkBuilder.build(workspacePath: path, target: target.target).toBlocking().single()
} else { } else {
xcframeworkPath = try self.xcframeworkBuilder.build(projectPath: path, target: target.target) xcframeworkPath = try self.xcframeworkBuilder.build(projectPath: path, target: target.target).toBlocking().single()
} }
// Create tasks to cache and delete the xcframeworks asynchronously // Create tasks to cache and delete the xcframeworks asynchronously

View File

@ -404,6 +404,7 @@ public final class System: Systeming {
exitStatus: result.exitStatus, exitStatus: result.exitStatus,
output: result.output, output: result.output,
stderrOutput: result.stderrOutput.map { _ in errorData }) stderrOutput: result.stderrOutput.map { _ in errorData })
try result.throwIfErrored() try result.throwIfErrored()
observer.onCompleted() observer.onCompleted()
} catch { } catch {

View File

@ -1,7 +1,6 @@
import Foundation import Foundation
import RxSwift import RxSwift
import TuistSupport import TuistSupport
@testable import TuistEnvKit
public final class MockURLSessionScheduler: TuistSupport.URLSessionScheduling { public final class MockURLSessionScheduler: TuistSupport.URLSessionScheduling {
private var stubs: [URLRequest: (error: URLError?, data: Data?)] = [:] private var stubs: [URLRequest: (error: URLError?, data: Data?)] = [:]

View File

@ -478,6 +478,7 @@
B9023E0B239BCDA200666BE6 /* Debug */ = { B9023E0B239BCDA200666BE6 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
@ -493,7 +494,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = io.tuist.iOS; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.iOS;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES; SKIP_INSTALL = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@ -503,6 +504,7 @@
B9023E0C239BCDA200666BE6 /* Release */ = { B9023E0C239BCDA200666BE6 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
@ -518,7 +520,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = io.tuist.iOS; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.iOS;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES; SKIP_INSTALL = NO;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@ -635,6 +637,7 @@
B9023E38239BCE2D00666BE6 /* Debug */ = { B9023E38239BCE2D00666BE6 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
@ -653,7 +656,6 @@
PRODUCT_BUNDLE_IDENTIFIER = io.tuist.macOS; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.macOS;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = macosx; SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };
@ -662,6 +664,7 @@
B9023E39239BCE2D00666BE6 /* Release */ = { B9023E39239BCE2D00666BE6 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
@ -680,7 +683,6 @@
PRODUCT_BUNDLE_IDENTIFIER = io.tuist.macOS; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.macOS;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = macosx; SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };
name = Release; name = Release;

View File

@ -1,40 +1,13 @@
import Basic import Basic
import Foundation import Foundation
import RxBlocking import RxBlocking
import TuistCore
import TuistSupport import TuistSupport
import XCTest import XCTest
@testable import TuistAutomation @testable import TuistAutomation
@testable import TuistSupportTesting @testable import TuistSupportTesting
final class XcodeBuildTargetTests: TuistUnitTestCase {
func test_xcodebuildArguments_returns_the_right_arguments_when_project() throws {
// Given
let path = try temporaryPath()
let xcodeprojPath = path.appending(component: "Project.xcodeproj")
let subject = XcodeBuildTarget.project(xcodeprojPath)
// When
let got = subject.xcodebuildArguments
// Then
XCTAssertEqual(got, ["-project", xcodeprojPath.pathString])
}
func test_xcodebuildArguments_returns_the_right_arguments_when_workspace() throws {
// Given
let path = try temporaryPath()
let xcworkspacePath = path.appending(component: "Project.xcworkspace")
let subject = XcodeBuildTarget.workspace(xcworkspacePath)
// When
let got = subject.xcodebuildArguments
// Then
XCTAssertEqual(got, ["-workspace", xcworkspacePath.pathString])
}
}
private final class MockParser: Parsing { private final class MockParser: Parsing {
var parseStub: ((String, Bool) -> String?)? var parseStub: ((String, Bool) -> String?)?
@ -70,7 +43,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase {
var command = ["/usr/bin/xcrun", "xcodebuild", "-scheme", scheme] var command = ["/usr/bin/xcrun", "xcodebuild", "-scheme", scheme]
command.append(contentsOf: target.xcodebuildArguments) command.append(contentsOf: target.xcodebuildArguments)
command.append(contentsOf: ["build", "clean"]) command.append(contentsOf: ["clean", "build"])
system.succeedCommand(command, output: "output") system.succeedCommand(command, output: "output")
var parseCalls: [(String, Bool)] = [] var parseCalls: [(String, Bool)] = []
@ -91,7 +64,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase {
switch events { switch events {
case let .completed(output): case let .completed(output):
XCTAssertEqual(output, [.standardOutput("formated-output")]) XCTAssertEqual(output, [.standardOutput(XcodeBuildOutput(raw: "output", formatted: "formated-output"))])
case .failed: case .failed:
XCTFail("The command was not expected to fail") XCTFail("The command was not expected to fail")
} }

View File

@ -0,0 +1,27 @@
import XCTest
@testable import TuistCache
@testable import TuistSupportTesting
final class XCFrameworkBuilderErrorTests: TuistUnitTestCase {
func test_type_when_nonFrameworkTarget() {
// Given
let subject = XCFrameworkBuilderError.nonFrameworkTarget("App")
// When
let got = subject.type
// Then
XCTAssertEqual(got, .abort)
}
func test_description_when_nonFrameworkTarget() {
// Given
let subject = XCFrameworkBuilderError.nonFrameworkTarget("App")
// When
let got = subject.description
// Then
XCTAssertEqual(got, "Can't generate an .xcframework from the target 'App' because it's not a framework target")
}
}

View File

@ -0,0 +1,63 @@
import Basic
import Foundation
import XCTest
@testable import TuistCore
@testable import TuistSupportTesting
final class XcodeBuildArgumentTests: TuistUnitTestCase {
func test_arguments_returns_the_right_value_when_sdk() {
// Given
let subject = XcodeBuildArgument.sdk("sdk")
// When
let got = subject.arguments
// Then
XCTAssertEqual(got, ["-sdk", "sdk"])
}
func test_arguments_returns_the_right_value_when_destination() {
// Given
let subject = XcodeBuildArgument.destination("destination")
// When
let got = subject.arguments
// Then
XCTAssertEqual(got, ["-destination", "destination"])
}
func test_arguments_returns_the_right_value_when_derivedDataPath() {
// Given
let path = AbsolutePath.root
let subject = XcodeBuildArgument.derivedDataPath(path)
// When
let got = subject.arguments
// Then
XCTAssertEqual(got, ["-derivedDataPath", path.pathString])
}
func test_arguments_returns_the_right_value_when_buildSetting() {
// Given
let subject = XcodeBuildArgument.buildSetting("key", "value")
// When
let got = subject.arguments
// Then
XCTAssertEqual(got, ["key=value"])
}
func test_arguments_returns_the_right_value_when_buildSetting_with_spaces() {
// Given
let subject = XcodeBuildArgument.buildSetting("key", "value with spaces")
// When
let got = subject.arguments
// Then
XCTAssertEqual(got, ["key=\'value with spaces\'"])
}
}

View File

@ -0,0 +1,37 @@
import Basic
import Foundation
import RxBlocking
import TuistCore
import TuistSupport
import XCTest
@testable import TuistAutomation
@testable import TuistSupportTesting
final class XcodeBuildTargetTests: TuistUnitTestCase {
func test_xcodebuildArguments_returns_the_right_arguments_when_project() throws {
// Given
let path = try temporaryPath()
let xcodeprojPath = path.appending(component: "Project.xcodeproj")
let subject = XcodeBuildTarget.project(xcodeprojPath)
// When
let got = subject.xcodebuildArguments
// Then
XCTAssertEqual(got, ["-project", xcodeprojPath.pathString])
}
func test_xcodebuildArguments_returns_the_right_arguments_when_workspace() throws {
// Given
let path = try temporaryPath()
let xcworkspacePath = path.appending(component: "Project.xcworkspace")
let subject = XcodeBuildTarget.workspace(xcworkspacePath)
// When
let got = subject.xcodebuildArguments
// Then
XCTAssertEqual(got, ["-workspace", xcworkspacePath.pathString])
}
}

View File

@ -1,6 +1,7 @@
import Basic import Basic
import Foundation import Foundation
import SPMUtility import SPMUtility
import TuistAutomation
import TuistCore import TuistCore
import TuistSupport import TuistSupport
import XCTest import XCTest
@ -15,7 +16,7 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
override func setUp() { override func setUp() {
super.setUp() super.setUp()
plistDecoder = PropertyListDecoder() plistDecoder = PropertyListDecoder()
subject = XCFrameworkBuilder(printOutput: false) subject = XCFrameworkBuilder(xcodeBuildController: XcodeBuildController())
} }
override func tearDown() { override func tearDown() {
@ -31,7 +32,7 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
let target = Target.test(name: "iOS", platform: .iOS, product: .framework, productName: "iOS") let target = Target.test(name: "iOS", platform: .iOS, product: .framework, productName: "iOS")
// When // When
let xcframeworkPath = try subject.build(projectPath: projectPath, target: target) let xcframeworkPath = try subject.build(projectPath: projectPath, target: target).toBlocking().single()
let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath) let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath)
// Then // Then
@ -55,7 +56,7 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
let target = Target.test(name: "macOS", platform: .macOS, product: .framework, productName: "macOS") let target = Target.test(name: "macOS", platform: .macOS, product: .framework, productName: "macOS")
// When // When
let xcframeworkPath = try subject.build(projectPath: projectPath, target: target) let xcframeworkPath = try subject.build(projectPath: projectPath, target: target).toBlocking().single()
let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath) let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath)
// Then // Then
@ -77,7 +78,7 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
let target = Target.test(name: "tvOS", platform: .tvOS, product: .framework, productName: "tvOS") let target = Target.test(name: "tvOS", platform: .tvOS, product: .framework, productName: "tvOS")
// When // When
let xcframeworkPath = try subject.build(projectPath: projectPath, target: target) let xcframeworkPath = try subject.build(projectPath: projectPath, target: target).toBlocking().single()
let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath) let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath)
// Then // Then
@ -101,7 +102,7 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
let target = Target.test(name: "watchOS", platform: .watchOS, product: .framework, productName: "watchOS") let target = Target.test(name: "watchOS", platform: .watchOS, product: .framework, productName: "watchOS")
// When // When
let xcframeworkPath = try subject.build(projectPath: projectPath, target: target) let xcframeworkPath = try subject.build(projectPath: projectPath, target: target).toBlocking().single()
let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath) let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath)
// Then // Then

View File

@ -1,15 +1,14 @@
import Foundation import Foundation
import XCTest import TuistCacheTesting
import TuistSupport
import TuistCore import TuistCore
import TuistSupportTesting
import TuistCoreTesting import TuistCoreTesting
import TuistGalaxyTesting import TuistSupport
import TuistSupportTesting
import XCTest
@testable import TuistKit @testable import TuistKit
final class CacheControllerTests: XCTestCase { final class CacheControllerTests: XCTestCase {
var generator: MockGenerator! var generator: MockGenerator!
var xcframeworkBuilder: MockXCFrameworkBuilder! var xcframeworkBuilder: MockXCFrameworkBuilder!
var cache: MockCacheStorage! var cache: MockCacheStorage!