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 Foundation
import RxSwift
import TuistCore
import TuistSupport
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 {
// MARK: - Attributes
/// Instance to format xcodebuild output.
private let parser: Parsing
public convenience init() {
self.init(parser: Parser())
}
init(parser: Parsing) {
self.parser = parser
}
func build(_ target: XcodeBuildTarget, scheme: String, clean: Bool = false) -> Observable<SystemEvent<String>> {
var command = ["/usr/bin/xcrun", "xcodebuild", "-scheme", scheme]
public func build(_ target: XcodeBuildTarget,
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
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
command.append("build")
if 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
return System.shared.observable(command, verbose: true)
.compactMap { event -> SystemEvent<String>? in
return System.shared.observable(command, verbose: false)
.compactMap { event -> SystemEvent<XcodeBuildOutput>? in
switch event {
case let .standardError(errorData):
guard let line = String(data: errorData, encoding: .utf8) else { return nil }
guard let formatedOutput = self.parser.parse(line: line, colored: colored) else { return nil }
return .standardError(formatedOutput)
let formatedOutput = self.parser.parse(line: line, colored: colored)
return .standardError(XcodeBuildOutput(raw: line, formatted: formatedOutput))
case let .standardOutput(outputData):
guard let line = String(data: outputData, encoding: .utf8) else { return nil }
guard let formatedOutput = self.parser.parse(line: line, colored: colored) else { return nil }
return .standardOutput(formatedOutput)
let formatedOutput = self.parser.parse(line: line, colored: colored)
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 Foundation
import RxSwift
import TuistCache
import TuistCore
import TuistGalaxy
public final class MockCacheStorage: CacheStoraging {
var existsStub: ((String) -> Bool)?

View File

@ -1,7 +1,8 @@
import Basic
import Foundation
import RxSwift
import TuistCache
import TuistCore
import TuistGalaxy
public final class MockXCFrameworkBuilder: XCFrameworkBuilding {
var buildProjectArgs: [(projectPath: AbsolutePath, target: Target)] = []
@ -9,21 +10,21 @@ public final class MockXCFrameworkBuilder: XCFrameworkBuilding {
var buildProjectStub: 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))
if let buildProjectStub = buildProjectStub {
return buildProjectStub
return Observable.just(buildProjectStub)
} 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))
if let buildWorkspaceStub = buildWorkspaceStub {
return buildWorkspaceStub
return Observable.just(buildWorkspaceStub)
} 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? {
switch self {
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 RxBlocking
import RxSwift
import TuistAutomation
import TuistCache
import TuistCore
import TuistGalaxy
import TuistGenerator
import TuistLoader
import TuistSupport
@ -32,7 +33,7 @@ final class CacheController: CacheControlling {
init(generator: Generating = Generator(),
manifestLoader: ManifestLoading = ManifestLoader(),
xcframeworkBuilder: XCFrameworkBuilding = XCFrameworkBuilder(printOutput: false),
xcframeworkBuilder: XCFrameworkBuilding = XCFrameworkBuilder(xcodeBuildController: XcodeBuildController()),
cache: CacheStoraging = Cache(),
graphContentHasher: GraphContentHashing = GraphContentHasher()) {
self.generator = generator
@ -50,21 +51,21 @@ final class CacheController: CacheControlling {
Printer.shared.print(section: "Hashing cacheable frameworks")
let targets: [TargetNode: String] = try graphContentHasher.contentHashes(for: graph)
.filter { target, hash in
if let exists = try self.cache.exists(hash: hash).toBlocking().first(), exists {
Printer.shared.print("The target \(.bold(.raw(target.name))) with hash \(.bold(.raw(hash))) is already in the cache. Skipping...")
return false
if let exists = try self.cache.exists(hash: hash).toBlocking().first(), exists {
Printer.shared.print("The target \(.bold(.raw(target.name))) with hash \(.bold(.raw(hash))) is already in the cache. Skipping...")
return false
}
return true
}
return true
}
var completables: [Completable] = []
try targets.forEach { target, hash in
// Build targets sequentially
let xcframeworkPath: AbsolutePath!
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 {
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

View File

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

View File

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

View File

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

View File

@ -1,40 +1,13 @@
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])
}
}
private final class MockParser: Parsing {
var parseStub: ((String, Bool) -> String?)?
@ -70,7 +43,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase {
var command = ["/usr/bin/xcrun", "xcodebuild", "-scheme", scheme]
command.append(contentsOf: target.xcodebuildArguments)
command.append(contentsOf: ["build", "clean"])
command.append(contentsOf: ["clean", "build"])
system.succeedCommand(command, output: "output")
var parseCalls: [(String, Bool)] = []
@ -91,7 +64,7 @@ final class XcodeBuildControllerTests: TuistUnitTestCase {
switch events {
case let .completed(output):
XCTAssertEqual(output, [.standardOutput("formated-output")])
XCTAssertEqual(output, [.standardOutput(XcodeBuildOutput(raw: "output", formatted: "formated-output"))])
case .failed:
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 Foundation
import SPMUtility
import TuistAutomation
import TuistCore
import TuistSupport
import XCTest
@ -15,7 +16,7 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
override func setUp() {
super.setUp()
plistDecoder = PropertyListDecoder()
subject = XCFrameworkBuilder(printOutput: false)
subject = XCFrameworkBuilder(xcodeBuildController: XcodeBuildController())
}
override func tearDown() {
@ -31,7 +32,7 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
let target = Target.test(name: "iOS", platform: .iOS, product: .framework, productName: "iOS")
// 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)
// Then
@ -55,7 +56,7 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
let target = Target.test(name: "macOS", platform: .macOS, product: .framework, productName: "macOS")
// 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)
// Then
@ -77,7 +78,7 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
let target = Target.test(name: "tvOS", platform: .tvOS, product: .framework, productName: "tvOS")
// 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)
// Then
@ -101,7 +102,7 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
let target = Target.test(name: "watchOS", platform: .watchOS, product: .framework, productName: "watchOS")
// 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)
// Then

View File

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