Cache targets as frameworks (#1851)
* Remove Workspace in ios_workspace_with_microfeature_architecture fixture * Add a xcframework flag on the cache warm command * Add CacheControllerFactory * Add ArtifactType * Update CacheMapper * Update CacheController * Add --xcframeworks flag in the print hashes command * Add --xcframeworks flag in the focus command * Move the Dependency enum up from XCFrameworkNode up to PrecompiledNode, as it is also needed for FrameworkNode * Refactor the cached binary building code to treat binaries as framework first * Update graph and mapping logic to treat binaries as framework first * Linter * Improved wording * Update TargetContentHasher * Swiftformat * Fix linting * Extend the cache implementation to support storing multiple artifacts * Some fixes and fix tests * Fix the derived data path * Fix all the tests * Fix acceptance tests * Fix acceptance tests * Fix failing tests Co-authored-by: Pedro Piñera <pepibumur@gmail.com>
This commit is contained in:
parent
03fa92321f
commit
59ce5d0aff
|
@ -92,7 +92,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
xcode: ['12_beta']
|
||||
feature: ['cache']
|
||||
feature: ['cache-xcframeworks', 'cache-frameworks']
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Select Xcode
|
||||
|
|
|
@ -151,7 +151,7 @@ let package = Package(
|
|||
),
|
||||
.target(
|
||||
name: "TuistCacheTesting",
|
||||
dependencies: ["TuistCache", "SwiftToolsSupport-auto", "TuistCore", "RxTest", "RxSwift"]
|
||||
dependencies: ["TuistCache", "SwiftToolsSupport-auto", "TuistCore", "RxTest", "RxSwift", "TuistSupportTesting"]
|
||||
),
|
||||
.target(
|
||||
name: "TuistCloud",
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct SimulatorDeviceAndRuntime: Hashable {
|
||||
/// Device
|
||||
let device: SimulatorDevice
|
||||
|
||||
/// Device's runtime.
|
||||
let runtime: SimulatorRuntime
|
||||
}
|
|
@ -168,29 +168,18 @@ public final class XcodeBuildController: XcodeBuildControlling {
|
|||
switch event {
|
||||
case let .standardError(errorData):
|
||||
guard let line = String(data: errorData, encoding: .utf8) else { return Observable.empty() }
|
||||
return Observable.create { observer in
|
||||
let lines = line.split(separator: "\n")
|
||||
lines.map { line in
|
||||
let formatedOutput = self.parser.parse(line: String(line), colored: colored)
|
||||
return SystemEvent.standardError(XcodeBuildOutput(raw: "\(String(line))\n", formatted: formatedOutput.map { "\($0)\n" }))
|
||||
}
|
||||
.forEach(observer.onNext)
|
||||
observer.onCompleted()
|
||||
return Disposables.create()
|
||||
let output = line.split(separator: "\n").map { line -> SystemEvent<XcodeBuildOutput> in
|
||||
let formatedOutput = self.parser.parse(line: String(line), colored: colored)
|
||||
return SystemEvent.standardError(XcodeBuildOutput(raw: "\(String(line))\n", formatted: formatedOutput.map { "\($0)\n" }))
|
||||
}
|
||||
return Observable.from(output)
|
||||
case let .standardOutput(outputData):
|
||||
guard let line = String(data: outputData, encoding: .utf8) else { return Observable.empty() }
|
||||
|
||||
return Observable.create { observer in
|
||||
let lines = line.split(separator: "\n")
|
||||
lines.map { line in
|
||||
let formatedOutput = self.parser.parse(line: String(line), colored: colored)
|
||||
return SystemEvent.standardOutput(XcodeBuildOutput(raw: "\(String(line))\n", formatted: formatedOutput.map { "\($0)\n" }))
|
||||
}
|
||||
.forEach(observer.onNext)
|
||||
observer.onCompleted()
|
||||
return Disposables.create()
|
||||
let output = line.split(separator: "\n").map { line -> SystemEvent<XcodeBuildOutput> in
|
||||
let formatedOutput = self.parser.parse(line: String(line), colored: colored)
|
||||
return SystemEvent.standardOutput(XcodeBuildOutput(raw: "\(String(line))\n", formatted: formatedOutput.map { "\($0)\n" }))
|
||||
}
|
||||
return Observable.from(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ public final class Cache: CacheStoring {
|
|||
}!
|
||||
}
|
||||
|
||||
public func store(hash: String, xcframeworkPath: AbsolutePath) -> Completable {
|
||||
public func store(hash: String, paths: [AbsolutePath]) -> Completable {
|
||||
let storages = storageProvider.storages()
|
||||
return Completable.zip(storages.map { $0.store(hash: hash, xcframeworkPath: xcframeworkPath) })
|
||||
return Completable.zip(storages.map { $0.store(hash: hash, paths: paths) })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ public final class CacheLocalStorage: CacheStoring {
|
|||
// MARK: - Init
|
||||
|
||||
public convenience init() {
|
||||
self.init(cacheDirectory: Environment.shared.xcframeworksCacheDirectory)
|
||||
self.init(cacheDirectory: Environment.shared.buildCacheDirectory)
|
||||
}
|
||||
|
||||
init(cacheDirectory: AbsolutePath) {
|
||||
|
@ -40,14 +40,14 @@ public final class CacheLocalStorage: CacheStoring {
|
|||
|
||||
public func exists(hash: String) -> Single<Bool> {
|
||||
Single.create { (completed) -> Disposable in
|
||||
completed(.success(FileHandler.shared.glob(self.cacheDirectory, glob: "\(hash)/*").count != 0))
|
||||
completed(.success(self.lookupFramework(directory: self.cacheDirectory.appending(component: hash)) != nil))
|
||||
return Disposables.create()
|
||||
}
|
||||
}
|
||||
|
||||
public func fetch(hash: String) -> Single<AbsolutePath> {
|
||||
Single.create { (completed) -> Disposable in
|
||||
if let path = FileHandler.shared.glob(self.cacheDirectory, glob: "\(hash)/*").first {
|
||||
if let path = self.lookupFramework(directory: self.cacheDirectory.appending(component: hash)) {
|
||||
completed(.success(path))
|
||||
} else {
|
||||
completed(.error(CacheLocalStorageError.xcframeworkNotFound(hash: hash)))
|
||||
|
@ -56,21 +56,22 @@ public final class CacheLocalStorage: CacheStoring {
|
|||
}
|
||||
}
|
||||
|
||||
public func store(hash: String, xcframeworkPath: AbsolutePath) -> Completable {
|
||||
public func store(hash: String, paths: [AbsolutePath]) -> Completable {
|
||||
let copy = Completable.create { (completed) -> Disposable in
|
||||
let hashFolder = self.cacheDirectory.appending(component: hash)
|
||||
let destinationPath = hashFolder.appending(component: xcframeworkPath.basename)
|
||||
|
||||
do {
|
||||
if !FileHandler.shared.exists(hashFolder) {
|
||||
try FileHandler.shared.createFolder(hashFolder)
|
||||
}
|
||||
if FileHandler.shared.exists(destinationPath) {
|
||||
try FileHandler.shared.delete(destinationPath)
|
||||
try paths.forEach { sourcePath in
|
||||
let destinationPath = hashFolder.appending(component: sourcePath.basename)
|
||||
if FileHandler.shared.exists(destinationPath) {
|
||||
try FileHandler.shared.delete(destinationPath)
|
||||
}
|
||||
|
||||
try FileHandler.shared.copy(from: sourcePath, to: destinationPath)
|
||||
}
|
||||
|
||||
try FileHandler.shared.copy(from: xcframeworkPath, to: destinationPath)
|
||||
|
||||
} catch {
|
||||
completed(.error(error))
|
||||
return Disposables.create()
|
||||
|
@ -84,6 +85,14 @@ public final class CacheLocalStorage: CacheStoring {
|
|||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
fileprivate func lookupFramework(directory: AbsolutePath) -> AbsolutePath? {
|
||||
let extensions = ["framework", "xcframework"]
|
||||
for ext in extensions {
|
||||
if let filePath = FileHandler.shared.glob(directory, glob: "*.\(ext)").first { return filePath }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func createCacheDirectory() -> Completable {
|
||||
Completable.create { (completed) -> Disposable in
|
||||
do {
|
||||
|
|
|
@ -5,18 +5,18 @@ import TuistCore
|
|||
import TuistSupport
|
||||
|
||||
enum CacheRemoteStorageError: FatalError, Equatable {
|
||||
case archiveDoesNotContainXCFramework(AbsolutePath)
|
||||
case frameworkNotFound(hash: String)
|
||||
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .archiveDoesNotContainXCFramework: return .abort
|
||||
case .frameworkNotFound: return .abort
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .archiveDoesNotContainXCFramework(path):
|
||||
return "Unzipped archive at path \(path.pathString) does not contain any xcframework."
|
||||
case let .frameworkNotFound(hash):
|
||||
return "The downloaded artifact with hash '\(hash)' has an incorrect format and doesn't contain a xcframework nor a framework."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,21 +28,20 @@ public final class CacheRemoteStorage: CacheStoring {
|
|||
private let cloudConfig: Cloud
|
||||
private let cloudClient: CloudClienting
|
||||
private let fileClient: FileClienting
|
||||
private let fileArchiverFactory: FileArchiverManufacturing
|
||||
private var fileArchiverMap: [AbsolutePath: FileArchiving] = [:]
|
||||
private let fileArchiverFactory: FileArchivingFactorying
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public convenience init(cloudConfig: Cloud, cloudClient: CloudClienting) {
|
||||
self.init(cloudConfig: cloudConfig,
|
||||
cloudClient: cloudClient,
|
||||
fileArchiverFactory: FileArchiverFactory(),
|
||||
fileArchiverFactory: FileArchivingFactory(),
|
||||
fileClient: FileClient())
|
||||
}
|
||||
|
||||
init(cloudConfig: Cloud,
|
||||
cloudClient: CloudClienting,
|
||||
fileArchiverFactory: FileArchiverManufacturing,
|
||||
fileArchiverFactory: FileArchivingFactorying,
|
||||
fileClient: FileClienting)
|
||||
{
|
||||
self.cloudConfig = cloudConfig
|
||||
|
@ -79,7 +78,10 @@ public final class CacheRemoteStorage: CacheStoring {
|
|||
return cloudClient
|
||||
.request(resource)
|
||||
.map { $0.object.data.url }
|
||||
.flatMap { (url: URL) in self.fileClient.download(url: url) }
|
||||
.flatMap { (url: URL) in
|
||||
self.fileClient.download(url: url)
|
||||
.do(onSubscribed: { logger.info("Downloading cache artifact with hash \(hash).") })
|
||||
}
|
||||
.flatMap { (filePath: AbsolutePath) in
|
||||
do {
|
||||
let archiveContentPath = try self.unzip(downloadedArchive: filePath, hash: hash)
|
||||
|
@ -93,10 +95,10 @@ public final class CacheRemoteStorage: CacheStoring {
|
|||
}
|
||||
}
|
||||
|
||||
public func store(hash: String, xcframeworkPath: AbsolutePath) -> Completable {
|
||||
public func store(hash: String, paths: [AbsolutePath]) -> Completable {
|
||||
do {
|
||||
let archiver = fileArchiver(for: xcframeworkPath)
|
||||
let destinationZipPath = try archiver.zip()
|
||||
let archiver = try fileArchiverFactory.makeFileArchiver(for: paths)
|
||||
let destinationZipPath = try archiver.zip(name: hash)
|
||||
let resource = try CloudCacheResponse.storeResource(
|
||||
hash: hash,
|
||||
cloud: cloudConfig,
|
||||
|
@ -119,26 +121,31 @@ public final class CacheRemoteStorage: CacheStoring {
|
|||
|
||||
// MARK: - Private
|
||||
|
||||
private func xcframeworkPath(in archive: AbsolutePath) throws -> AbsolutePath? {
|
||||
let folderContent = try FileHandler.shared.contentsOfDirectory(archive)
|
||||
return folderContent.filter { FileHandler.shared.isFolder($0) && $0.extension == "xcframework" }.first
|
||||
private func frameworkPath(in archive: AbsolutePath) -> AbsolutePath? {
|
||||
if let xcframeworkPath = FileHandler.shared.glob(archive, glob: "*.xcframework").first {
|
||||
return xcframeworkPath
|
||||
} else if let frameworkPath = FileHandler.shared.glob(archive, glob: "*.framework").first {
|
||||
return frameworkPath
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func unzip(downloadedArchive: AbsolutePath, hash: String) throws -> AbsolutePath {
|
||||
let zipPath = try FileHandler.shared.changeExtension(path: downloadedArchive, to: "zip")
|
||||
let archiveDestination = Environment.shared.xcframeworksCacheDirectory.appending(component: hash)
|
||||
try fileArchiver(for: zipPath).unzip(to: archiveDestination)
|
||||
guard let xcframework = try xcframeworkPath(in: archiveDestination) else {
|
||||
try FileHandler.shared.delete(archiveDestination)
|
||||
throw CacheRemoteStorageError.archiveDoesNotContainXCFramework(archiveDestination)
|
||||
let archiveDestination = Environment.shared.buildCacheDirectory.appending(component: hash)
|
||||
let fileUnarchiver = try fileArchiverFactory.makeFileUnarchiver(for: zipPath)
|
||||
let unarchivedDirectory = try fileUnarchiver.unzip()
|
||||
defer {
|
||||
try? fileUnarchiver.delete()
|
||||
}
|
||||
return xcframework
|
||||
}
|
||||
|
||||
private func fileArchiver(for path: AbsolutePath) -> FileArchiving {
|
||||
let fileArchiver = fileArchiverMap[path] ?? fileArchiverFactory.makeFileArchiver(for: path)
|
||||
fileArchiverMap[path] = fileArchiver
|
||||
return fileArchiver
|
||||
if frameworkPath(in: unarchivedDirectory) == nil {
|
||||
throw CacheRemoteStorageError.frameworkNotFound(hash: hash)
|
||||
}
|
||||
if !FileHandler.shared.exists(archiveDestination.parentDirectory) {
|
||||
try FileHandler.shared.createFolder(archiveDestination.parentDirectory)
|
||||
}
|
||||
try FileHandler.shared.move(from: unarchivedDirectory, to: archiveDestination)
|
||||
return frameworkPath(in: archiveDestination)!
|
||||
}
|
||||
|
||||
private func deleteZipArchiveCompletable(archiver: FileArchiving) -> Completable {
|
||||
|
@ -152,12 +159,4 @@ public final class CacheRemoteStorage: CacheStoring {
|
|||
return Disposables.create {}
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - Deinit
|
||||
|
||||
deinit {
|
||||
do {
|
||||
try fileArchiverMap.values.forEach { fileArchiver in try fileArchiver.delete() }
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,6 @@ public protocol CacheStoring {
|
|||
/// It stores the xcframework at the given path in the cache.
|
||||
/// - Parameters:
|
||||
/// - hash: Hash of the target the xcframework belongs to.
|
||||
/// - xcframeworkPath: Path to the .xcframework.
|
||||
func store(hash: String, xcframeworkPath: AbsolutePath) -> Completable
|
||||
/// - paths: Path to the files that will be stored.
|
||||
func store(hash: String, paths: [AbsolutePath]) -> Completable
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import TuistCore
|
|||
import TuistSupport
|
||||
|
||||
public protocol GraphContentHashing {
|
||||
func contentHashes(for graph: TuistCore.Graph) throws -> [TargetNode: String]
|
||||
func contentHashes(for graph: TuistCore.Graph, cacheOutputType: CacheOutputType) throws -> [TargetNode: String]
|
||||
}
|
||||
|
||||
/// `GraphContentHasher`
|
||||
|
@ -24,7 +24,7 @@ public final class GraphContentHasher: GraphContentHashing {
|
|||
|
||||
// MARK: - GraphContentHashing
|
||||
|
||||
public func contentHashes(for graph: TuistCore.Graph) throws -> [TargetNode: String] {
|
||||
public func contentHashes(for graph: TuistCore.Graph, cacheOutputType: CacheOutputType) throws -> [TargetNode: String] {
|
||||
var visitedNodes: [TargetNode: Bool] = [:]
|
||||
let hashableTargets = graph.targets.values.flatMap { (targets: [TargetNode]) -> [TargetNode] in
|
||||
targets.compactMap { target in
|
||||
|
@ -35,7 +35,8 @@ public final class GraphContentHasher: GraphContentHashing {
|
|||
}
|
||||
}
|
||||
let hashes = try hashableTargets.map {
|
||||
try targetContentHasher.contentHash(for: $0)
|
||||
try targetContentHasher.contentHash(for: $0,
|
||||
cacheOutputType: cacheOutputType)
|
||||
}
|
||||
return Dictionary(uniqueKeysWithValues: zip(hashableTargets, hashes))
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import TuistCore
|
|||
import TuistSupport
|
||||
|
||||
public protocol TargetContentHashing {
|
||||
func contentHash(for target: TargetNode) throws -> String
|
||||
func contentHash(for target: TargetNode, cacheOutputType: CacheOutputType) throws -> String
|
||||
}
|
||||
|
||||
/// `TargetContentHasher`
|
||||
|
@ -64,7 +64,7 @@ public final class TargetContentHasher: TargetContentHashing {
|
|||
|
||||
// MARK: - TargetContentHashing
|
||||
|
||||
public func contentHash(for targetNode: TargetNode) throws -> String {
|
||||
public func contentHash(for targetNode: TargetNode, cacheOutputType: CacheOutputType) throws -> String {
|
||||
let target = targetNode.target
|
||||
let sourcesHash = try sourceFilesContentHasher.hash(sources: target.sources)
|
||||
let resourcesHash = try resourcesContentHasher.hash(resources: target.resources)
|
||||
|
@ -104,6 +104,7 @@ public final class TargetContentHasher: TargetContentHashing {
|
|||
stringsToHash.append(settingsHash)
|
||||
}
|
||||
|
||||
stringsToHash.append(cacheOutputType.description)
|
||||
return try contentHasher.hash(stringsToHash)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,13 +10,13 @@ protocol CacheGraphMutating {
|
|||
/// to the .xcframeworks in the cache, it mutates the graph to link the enry nodes against the .xcframeworks instead.
|
||||
/// - Parameters:
|
||||
/// - graph: Dependency graph.
|
||||
/// - xcframeworks: Dictionary that maps targets with the paths to their cached .xcframeworks.
|
||||
/// - precompiledFrameworks: Dictionary that maps targets with the paths to their cached `.framework`s or `.xcframework`s.
|
||||
/// - source: Contains a list of targets that won't be replaced with their pre-compiled version from the cache.
|
||||
func map(graph: Graph, xcframeworks: [TargetNode: AbsolutePath], sources: Set<String>) throws -> Graph
|
||||
func map(graph: Graph, precompiledFrameworks: [TargetNode: AbsolutePath], sources: Set<String>) throws -> Graph
|
||||
}
|
||||
|
||||
class CacheGraphMutator: CacheGraphMutating {
|
||||
struct VisitedXCFramework {
|
||||
struct VisitedPrecompiledFramework {
|
||||
let path: AbsolutePath?
|
||||
}
|
||||
|
||||
|
@ -25,54 +25,62 @@ class CacheGraphMutator: CacheGraphMutating {
|
|||
/// Utility to parse an .xcframework from the filesystem and load it into memory.
|
||||
private let xcframeworkLoader: XCFrameworkNodeLoading
|
||||
|
||||
/// Utility to parse a .framework from the filesystem and load it into memory.
|
||||
private let frameworkLoader: FrameworkNodeLoading
|
||||
|
||||
/// Initializes the graph mapper with its attributes.
|
||||
/// - Parameter xcframeworkLoader: Utility to parse an .xcframework from the filesystem and load it into memory.
|
||||
init(xcframeworkLoader: XCFrameworkNodeLoading = XCFrameworkNodeLoader()) {
|
||||
init(frameworkLoader: FrameworkNodeLoading = FrameworkNodeLoader(),
|
||||
xcframeworkLoader: XCFrameworkNodeLoading = XCFrameworkNodeLoader())
|
||||
{
|
||||
self.frameworkLoader = frameworkLoader
|
||||
self.xcframeworkLoader = xcframeworkLoader
|
||||
}
|
||||
|
||||
// MARK: - CacheGraphMapping
|
||||
|
||||
public func map(graph: Graph, xcframeworks: [TargetNode: AbsolutePath], sources: Set<String>) throws -> Graph {
|
||||
var visitedXCFrameworkPaths: [TargetNode: VisitedXCFramework?] = [:]
|
||||
var loadedXCFrameworks: [AbsolutePath: XCFrameworkNode] = [:]
|
||||
public func map(graph: Graph, precompiledFrameworks: [TargetNode: AbsolutePath], sources: Set<String>) throws -> Graph {
|
||||
var visitedPrecompiledFrameworkPaths: [TargetNode: VisitedPrecompiledFramework?] = [:]
|
||||
var loadedPrecompiledNodes: [AbsolutePath: PrecompiledNode] = [:]
|
||||
let userSpecifiedSourceTargets = graph.targets.flatMap { $0.value }.filter { sources.contains($0.target.name) }
|
||||
let userSpecifiedSourceTestTargets = userSpecifiedSourceTargets.flatMap { graph.testTargetsDependingOn(path: $0.path, name: $0.name) }
|
||||
var sourceTargets: Set<TargetNode> = Set(userSpecifiedSourceTargets)
|
||||
|
||||
try (userSpecifiedSourceTargets + userSpecifiedSourceTestTargets)
|
||||
.forEach { try visit(targetNode: $0,
|
||||
xcframeworks: xcframeworks,
|
||||
precompiledFrameworks: precompiledFrameworks,
|
||||
sources: sources,
|
||||
sourceTargets: &sourceTargets,
|
||||
visitedXCFrameworkPaths: &visitedXCFrameworkPaths,
|
||||
loadedXCFrameworks: &loadedXCFrameworks) }
|
||||
visitedPrecompiledFrameworkPaths: &visitedPrecompiledFrameworkPaths,
|
||||
loadedPrecompiledNodes: &loadedPrecompiledNodes) }
|
||||
|
||||
return treeShake(graph: graph, sourceTargets: sourceTargets)
|
||||
let newGraph = treeShake(graph: graph, sourceTargets: sourceTargets)
|
||||
return newGraph
|
||||
}
|
||||
|
||||
fileprivate func visit(targetNode: TargetNode,
|
||||
xcframeworks: [TargetNode: AbsolutePath],
|
||||
precompiledFrameworks: [TargetNode: AbsolutePath],
|
||||
sources: Set<String>,
|
||||
sourceTargets: inout Set<TargetNode>,
|
||||
visitedXCFrameworkPaths: inout [TargetNode: VisitedXCFramework?],
|
||||
loadedXCFrameworks: inout [AbsolutePath: XCFrameworkNode]) throws
|
||||
visitedPrecompiledFrameworkPaths: inout [TargetNode: VisitedPrecompiledFramework?],
|
||||
loadedPrecompiledNodes: inout [AbsolutePath: PrecompiledNode]) throws
|
||||
{
|
||||
sourceTargets.formUnion([targetNode])
|
||||
targetNode.dependencies = try mapDependencies(targetNode.dependencies,
|
||||
xcframeworks: xcframeworks,
|
||||
precompiledFrameworks: precompiledFrameworks,
|
||||
sources: sources,
|
||||
sourceTargets: &sourceTargets,
|
||||
visitedXCFrameworkPaths: &visitedXCFrameworkPaths,
|
||||
loadedXCFrameworks: &loadedXCFrameworks)
|
||||
visitedPrecompiledFrameworkPaths: &visitedPrecompiledFrameworkPaths,
|
||||
loadedPrecompiledFrameworks: &loadedPrecompiledNodes)
|
||||
}
|
||||
|
||||
// swiftlint:disable line_length
|
||||
fileprivate func mapDependencies(_ dependencies: [GraphNode],
|
||||
xcframeworks: [TargetNode: AbsolutePath],
|
||||
precompiledFrameworks: [TargetNode: AbsolutePath],
|
||||
sources: Set<String>,
|
||||
sourceTargets: inout Set<TargetNode>,
|
||||
visitedXCFrameworkPaths: inout [TargetNode: VisitedXCFramework?],
|
||||
loadedXCFrameworks: inout [AbsolutePath: XCFrameworkNode]) throws -> [GraphNode]
|
||||
visitedPrecompiledFrameworkPaths: inout [TargetNode: VisitedPrecompiledFramework?],
|
||||
loadedPrecompiledFrameworks: inout [AbsolutePath: PrecompiledNode]) throws -> [GraphNode]
|
||||
{
|
||||
var newDependencies: [GraphNode] = []
|
||||
try dependencies.forEach { dependency in
|
||||
|
@ -82,40 +90,41 @@ class CacheGraphMutator: CacheGraphMutating {
|
|||
return
|
||||
}
|
||||
|
||||
// If the target cannot be replaced with its associated .xcframework we return
|
||||
guard !sources.contains(targetDependency.target.name), let xcframeworkPath = xcframeworkPath(target: targetDependency,
|
||||
xcframeworks: xcframeworks,
|
||||
visitedXCFrameworkPaths: &visitedXCFrameworkPaths)
|
||||
// If the target cannot be replaced with its associated .(xc)framework we return
|
||||
guard !sources.contains(targetDependency.target.name), let precompiledFrameworkPath = precompiledFrameworkPath(target: targetDependency,
|
||||
precompiledFrameworks: precompiledFrameworks,
|
||||
visitedPrecompiledFrameworkPaths: &visitedPrecompiledFrameworkPaths)
|
||||
else {
|
||||
sourceTargets.formUnion([targetDependency])
|
||||
targetDependency.dependencies = try mapDependencies(targetDependency.dependencies,
|
||||
xcframeworks: xcframeworks,
|
||||
precompiledFrameworks: precompiledFrameworks,
|
||||
sources: sources,
|
||||
sourceTargets: &sourceTargets,
|
||||
visitedXCFrameworkPaths: &visitedXCFrameworkPaths,
|
||||
loadedXCFrameworks: &loadedXCFrameworks)
|
||||
visitedPrecompiledFrameworkPaths: &visitedPrecompiledFrameworkPaths,
|
||||
loadedPrecompiledFrameworks: &loadedPrecompiledFrameworks)
|
||||
newDependencies.append(targetDependency)
|
||||
return
|
||||
}
|
||||
|
||||
// We load the xcframework
|
||||
let xcframework = try self.loadXCFramework(path: xcframeworkPath, loadedXCFrameworks: &loadedXCFrameworks)
|
||||
// We load the .framework (or fallback on .xcframework)
|
||||
let precompiledFramework: PrecompiledNode = try loadPrecompiledFramework(path: precompiledFrameworkPath, loadedPrecompiledFrameworks: &loadedPrecompiledFrameworks)
|
||||
|
||||
try mapDependencies(targetDependency.dependencies,
|
||||
xcframeworks: xcframeworks,
|
||||
precompiledFrameworks: precompiledFrameworks,
|
||||
sources: sources,
|
||||
sourceTargets: &sourceTargets,
|
||||
visitedXCFrameworkPaths: &visitedXCFrameworkPaths,
|
||||
loadedXCFrameworks: &loadedXCFrameworks).forEach { dependency in
|
||||
visitedPrecompiledFrameworkPaths: &visitedPrecompiledFrameworkPaths,
|
||||
loadedPrecompiledFrameworks: &loadedPrecompiledFrameworks).forEach { dependency in
|
||||
if let frameworkDependency = dependency as? FrameworkNode {
|
||||
xcframework.add(dependency: XCFrameworkNode.Dependency.framework(frameworkDependency))
|
||||
precompiledFramework.add(dependency: PrecompiledNode.Dependency.framework(frameworkDependency))
|
||||
} else if let xcframeworkDependency = dependency as? XCFrameworkNode {
|
||||
xcframework.add(dependency: XCFrameworkNode.Dependency.xcframework(xcframeworkDependency))
|
||||
precompiledFramework.add(dependency: PrecompiledNode.Dependency.xcframework(xcframeworkDependency))
|
||||
} else {
|
||||
// Static dependencies fall into this case.
|
||||
// Those are now part of the precompiled xcframework and therefore we don't have to link against them.
|
||||
// Those are now part of the precompiled (xc)framework and therefore we don't have to link against them.
|
||||
}
|
||||
}
|
||||
newDependencies.append(xcframework)
|
||||
newDependencies.append(precompiledFramework)
|
||||
}
|
||||
return newDependencies
|
||||
}
|
||||
|
@ -162,34 +171,41 @@ class CacheGraphMutator: CacheGraphMutating {
|
|||
})
|
||||
}
|
||||
|
||||
fileprivate func loadXCFramework(path: AbsolutePath, loadedXCFrameworks: inout [AbsolutePath: XCFrameworkNode]) throws -> XCFrameworkNode {
|
||||
if let cachedXCFramework = loadedXCFrameworks[path] { return cachedXCFramework }
|
||||
let xcframework = try xcframeworkLoader.load(path: path)
|
||||
loadedXCFrameworks[path] = xcframework
|
||||
return xcframework
|
||||
fileprivate func loadPrecompiledFramework(path: AbsolutePath, loadedPrecompiledFrameworks: inout [AbsolutePath: PrecompiledNode]) throws -> PrecompiledNode {
|
||||
if let cachedFramework = loadedPrecompiledFrameworks[path] {
|
||||
return cachedFramework
|
||||
} else if let framework = try? frameworkLoader.load(path: path) {
|
||||
loadedPrecompiledFrameworks[path] = framework
|
||||
return framework
|
||||
} else {
|
||||
let xcframework = try xcframeworkLoader.load(path: path)
|
||||
loadedPrecompiledFrameworks[path] = xcframework
|
||||
return xcframework
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func xcframeworkPath(target: TargetNode,
|
||||
xcframeworks: [TargetNode: AbsolutePath],
|
||||
visitedXCFrameworkPaths: inout [TargetNode: VisitedXCFramework?]) -> AbsolutePath?
|
||||
fileprivate func precompiledFrameworkPath(target: TargetNode,
|
||||
precompiledFrameworks: [TargetNode: AbsolutePath],
|
||||
visitedPrecompiledFrameworkPaths: inout [TargetNode: VisitedPrecompiledFramework?]) -> AbsolutePath?
|
||||
{
|
||||
// Already visited
|
||||
if let visited = visitedXCFrameworkPaths[target] { return visited?.path }
|
||||
if let visited = visitedPrecompiledFrameworkPaths[target] { return visited?.path }
|
||||
|
||||
// The target doesn't have a cached xcframework
|
||||
if xcframeworks[target] == nil {
|
||||
visitedXCFrameworkPaths[target] = VisitedXCFramework(path: nil)
|
||||
// The target doesn't have a cached .(xc)framework
|
||||
if precompiledFrameworks[target] == nil {
|
||||
visitedPrecompiledFrameworkPaths[target] = VisitedPrecompiledFramework(path: nil)
|
||||
return nil
|
||||
}
|
||||
// The target can be replaced
|
||||
else if let path = xcframeworks[target],
|
||||
target.targetDependencies.allSatisfy({ xcframeworkPath(target: $0, xcframeworks: xcframeworks,
|
||||
visitedXCFrameworkPaths: &visitedXCFrameworkPaths) != nil })
|
||||
else if let path = precompiledFrameworks[target],
|
||||
target.targetDependencies.allSatisfy({ precompiledFrameworkPath(target: $0,
|
||||
precompiledFrameworks: precompiledFrameworks,
|
||||
visitedPrecompiledFrameworkPaths: &visitedPrecompiledFrameworkPaths) != nil })
|
||||
{
|
||||
visitedXCFrameworkPaths[target] = VisitedXCFramework(path: path)
|
||||
visitedPrecompiledFrameworkPaths[target] = VisitedPrecompiledFramework(path: path)
|
||||
return path
|
||||
} else {
|
||||
visitedXCFrameworkPaths[target] = VisitedXCFramework(path: nil)
|
||||
visitedPrecompiledFrameworkPaths[target] = VisitedPrecompiledFramework(path: nil)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ public class CacheMapper: GraphMapping {
|
|||
/// Cache graph mapper.
|
||||
private let cacheGraphMutator: CacheGraphMutating
|
||||
|
||||
/// Configuration object
|
||||
/// Configuration object.
|
||||
private let config: Config
|
||||
|
||||
/// List of targets that will be generated as sources instead of pre-compiled targets from the cache.
|
||||
|
@ -26,22 +26,28 @@ public class CacheMapper: GraphMapping {
|
|||
/// Dispatch queue.
|
||||
private let queue: DispatchQueue
|
||||
|
||||
/// The type of artifact that the hasher is configured with.
|
||||
private let cacheOutputType: CacheOutputType
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public convenience init(config: Config,
|
||||
cacheStorageProvider: CacheStorageProviding,
|
||||
sources: Set<String>)
|
||||
sources: Set<String>,
|
||||
cacheOutputType: CacheOutputType)
|
||||
{
|
||||
self.init(config: config,
|
||||
cache: Cache(storageProvider: cacheStorageProvider),
|
||||
graphContentHasher: GraphContentHasher(),
|
||||
sources: sources)
|
||||
sources: sources,
|
||||
cacheOutputType: cacheOutputType)
|
||||
}
|
||||
|
||||
init(config: Config,
|
||||
cache: CacheStoring,
|
||||
graphContentHasher: GraphContentHashing,
|
||||
sources: Set<String>,
|
||||
cacheOutputType: CacheOutputType,
|
||||
cacheGraphMutator: CacheGraphMutating = CacheGraphMutator(),
|
||||
queue: DispatchQueue = CacheMapper.dispatchQueue())
|
||||
{
|
||||
|
@ -51,6 +57,7 @@ public class CacheMapper: GraphMapping {
|
|||
self.queue = queue
|
||||
self.cacheGraphMutator = cacheGraphMutator
|
||||
self.sources = sources
|
||||
self.cacheOutputType = cacheOutputType
|
||||
}
|
||||
|
||||
// MARK: - GraphMapping
|
||||
|
@ -70,7 +77,8 @@ public class CacheMapper: GraphMapping {
|
|||
fileprivate func hashes(graph: Graph) -> Single<[TargetNode: String]> {
|
||||
Single.create { (observer) -> Disposable in
|
||||
do {
|
||||
let hashes = try self.graphContentHasher.contentHashes(for: graph)
|
||||
let hashes = try self.graphContentHasher.contentHashes(for: graph,
|
||||
cacheOutputType: self.cacheOutputType)
|
||||
observer(.success(hashes))
|
||||
} catch {
|
||||
observer(.error(error))
|
||||
|
@ -83,7 +91,7 @@ public class CacheMapper: GraphMapping {
|
|||
fileprivate func map(graph: Graph, hashes: [TargetNode: String], sources: Set<String>) -> Single<Graph> {
|
||||
fetch(hashes: hashes).map { xcframeworkPaths in
|
||||
try self.cacheGraphMutator.map(graph: graph,
|
||||
xcframeworks: xcframeworkPaths,
|
||||
precompiledFrameworks: xcframeworkPaths,
|
||||
sources: sources)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import TSCBasic
|
||||
import TuistCore
|
||||
|
||||
public protocol CacheArtifactBuilding {
|
||||
/// Returns the type of artifact that the concrete builder processes.
|
||||
var cacheOutputType: CacheOutputType { get }
|
||||
|
||||
/// Builds a given target and outputs the cacheable artifact into the given directory.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - workspacePath: Path to the generated .xcworkspace that contains the given target.
|
||||
/// - target: Target whose artifact will be generated.
|
||||
/// - into: The directory into which the output artifacts will be copied.
|
||||
func build(workspacePath: AbsolutePath, target: Target, into outputDirectory: AbsolutePath) throws
|
||||
|
||||
/// Builds a given target and outputs the cacheable artifact into the given directory.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - projectPath: Path to the generated .xcodeproj that contains the given target.
|
||||
/// - target: Target whose .(xc)framework will be generated.
|
||||
/// - into: The directory into which the output artifacts will be copied.
|
||||
func build(projectPath: AbsolutePath, target: Target, into outputDirectory: AbsolutePath) throws
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import TuistSupport
|
||||
|
||||
enum CacheBinaryBuilderError: FatalError {
|
||||
case nonFrameworkTargetForXCFramework(String)
|
||||
case nonFrameworkTargetForFramework(String)
|
||||
|
||||
/// Error type.
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .nonFrameworkTargetForXCFramework: return .abort
|
||||
case .nonFrameworkTargetForFramework: return .abort
|
||||
}
|
||||
}
|
||||
|
||||
/// Error description.
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .nonFrameworkTargetForXCFramework(name):
|
||||
return "Can't generate an .xcframework from the target '\(name)' because it's not a framework target"
|
||||
case let .nonFrameworkTargetForFramework(name):
|
||||
return "Can't generate a .framework from the target '\(name)' because it's not a framework target"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
import Foundation
|
||||
import RxBlocking
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistSupport
|
||||
|
||||
public enum CacheFrameworkBuilderError: FatalError {
|
||||
case frameworkNotFound(name: String, derivedDataPath: AbsolutePath)
|
||||
case deviceNotFound(platform: String)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case let .frameworkNotFound(name, derivedDataPath):
|
||||
return "Couldn't find framework '\(name)' in the derived data directory: \(derivedDataPath.pathString)"
|
||||
case let .deviceNotFound(platform):
|
||||
return "Couldn't find an available device for platform: '\(platform)'"
|
||||
}
|
||||
}
|
||||
|
||||
public var type: ErrorType {
|
||||
switch self {
|
||||
case .frameworkNotFound: return .bug
|
||||
case .deviceNotFound: return .bug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class CacheFrameworkBuilder: CacheArtifactBuilding {
|
||||
// MARK: - Attributes
|
||||
|
||||
/// Xcode build controller instance to run xcodebuild commands.
|
||||
private let xcodeBuildController: XcodeBuildControlling
|
||||
|
||||
/// Simulator controller.
|
||||
private let simulatorController: SimulatorControlling
|
||||
|
||||
/// Developer's environment.
|
||||
private let developerEnvironment: DeveloperEnvironmenting
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
/// Initialzies the builder.
|
||||
/// - Parameters:
|
||||
/// - xcodeBuildController: Xcode build controller.
|
||||
/// - simulatorController: Simulator controller.
|
||||
/// - developerEnvironment: Developer environment.
|
||||
public init(xcodeBuildController: XcodeBuildControlling,
|
||||
simulatorController: SimulatorControlling = SimulatorController(),
|
||||
developerEnvironment: DeveloperEnvironmenting = DeveloperEnvironment.shared)
|
||||
{
|
||||
self.xcodeBuildController = xcodeBuildController
|
||||
self.simulatorController = simulatorController
|
||||
self.developerEnvironment = developerEnvironment
|
||||
}
|
||||
|
||||
// MARK: - ArtifactBuilding
|
||||
|
||||
/// Returns the type of artifact that the concrete builder processes
|
||||
public var cacheOutputType: CacheOutputType = .framework
|
||||
|
||||
public func build(workspacePath: AbsolutePath,
|
||||
target: Target,
|
||||
into outputDirectory: AbsolutePath) throws
|
||||
{
|
||||
try build(.workspace(workspacePath),
|
||||
target: target,
|
||||
into: outputDirectory)
|
||||
}
|
||||
|
||||
public func build(projectPath: AbsolutePath,
|
||||
target: Target,
|
||||
into outputDirectory: AbsolutePath) throws
|
||||
{
|
||||
try build(.project(projectPath),
|
||||
target: target,
|
||||
into: outputDirectory)
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
fileprivate func build(_ projectTarget: XcodeBuildTarget,
|
||||
target: Target,
|
||||
into outputDirectory: AbsolutePath) throws
|
||||
{
|
||||
guard target.product.isFramework else {
|
||||
throw CacheBinaryBuilderError.nonFrameworkTargetForFramework(target.name)
|
||||
}
|
||||
let scheme = target.name.spm_shellEscaped()
|
||||
|
||||
// Create temporary directories
|
||||
return try FileHandler.shared.inTemporaryDirectory(removeOnCompletion: true) { _ in
|
||||
logger.notice("Building .framework for \(target.name)...", metadata: .section)
|
||||
|
||||
let sdk = self.sdk(target: target)
|
||||
let configuration = "Debug" // TODO: Is it available?
|
||||
|
||||
let arguments = try self.arguments(target: target,
|
||||
sdk: sdk,
|
||||
configuration: configuration,
|
||||
outputDirectory: outputDirectory)
|
||||
try self.xcodebuild(
|
||||
projectTarget: projectTarget,
|
||||
scheme: scheme,
|
||||
target: target,
|
||||
arguments: arguments
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func arguments(target: Target, sdk: String, configuration: String, outputDirectory: AbsolutePath) throws -> [XcodeBuildArgument] {
|
||||
try destination(target: target)
|
||||
.map { (destination: String) -> [XcodeBuildArgument] in
|
||||
[
|
||||
.sdk(sdk),
|
||||
.configuration(configuration),
|
||||
.buildSetting("DEBUG_INFORMATION_FORMAT", "dwarf-with-dsym"),
|
||||
.buildSetting("GCC_GENERATE_DEBUGGING_SYMBOLS", "YES"),
|
||||
.buildSetting("CONFIGURATION_BUILD_DIR", outputDirectory.pathString),
|
||||
.destination(destination),
|
||||
]
|
||||
}
|
||||
.toBlocking()
|
||||
.single()
|
||||
}
|
||||
|
||||
/// https://www.mokacoding.com/blog/xcodebuild-destination-options/
|
||||
/// https://www.mokacoding.com/blog/how-to-always-run-latest-simulator-cli/
|
||||
fileprivate func destination(target: Target) -> Single<String> {
|
||||
var platform: Platform!
|
||||
switch target.platform {
|
||||
case .iOS: platform = .iOS
|
||||
case .watchOS: platform = .watchOS
|
||||
case .tvOS: platform = .tvOS
|
||||
case .macOS: return .just("platform=OS X,arch=x86_64")
|
||||
}
|
||||
|
||||
return simulatorController.devicesAndRuntimes()
|
||||
.map { (simulatorsAndRuntimes) -> [SimulatorDevice] in
|
||||
simulatorsAndRuntimes
|
||||
.filter { $0.runtime.isAvailable && $0.runtime.name.contains(platform.caseValue) }
|
||||
.map { $0.device }
|
||||
}
|
||||
.flatMap { (devices) -> Single<String> in
|
||||
if let device = devices.first {
|
||||
let destination = "platform=\(platform.caseValue) Simulator,name=\(device.name),OS=latest"
|
||||
return .just(destination)
|
||||
} else {
|
||||
return .error(CacheFrameworkBuilderError.deviceNotFound(platform: target.platform.caseValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func sdk(target: Target) -> String {
|
||||
if target.platform == .macOS {
|
||||
return target.platform.xcodeDeviceSDK
|
||||
} else {
|
||||
return target.platform.xcodeSimulatorSDK!
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func xcodebuild(projectTarget: XcodeBuildTarget,
|
||||
scheme: String,
|
||||
target: Target,
|
||||
arguments: [XcodeBuildArgument]) throws
|
||||
{
|
||||
_ = try xcodeBuildController.build(projectTarget,
|
||||
scheme: scheme,
|
||||
clean: false,
|
||||
arguments: arguments)
|
||||
.printFormattedOutput()
|
||||
.do(onSubscribed: {
|
||||
logger.notice("Building \(target.name) as .framework...", metadata: .subsection)
|
||||
})
|
||||
.ignoreElements()
|
||||
.toBlocking()
|
||||
.last()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
import Foundation
|
||||
import RxBlocking
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistSupport
|
||||
|
||||
public final class CacheXCFrameworkBuilder: CacheArtifactBuilding {
|
||||
// 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: - ArtifactBuilding
|
||||
|
||||
/// Returns the type of artifact that the concrete builder processes
|
||||
public var cacheOutputType: CacheOutputType = .xcframework
|
||||
|
||||
public func build(workspacePath: AbsolutePath,
|
||||
target: Target,
|
||||
into outputDirectory: AbsolutePath) throws
|
||||
{
|
||||
try build(.workspace(workspacePath),
|
||||
target: target,
|
||||
into: outputDirectory)
|
||||
}
|
||||
|
||||
public func build(projectPath: AbsolutePath,
|
||||
target: Target,
|
||||
into outputDirectory: AbsolutePath) throws
|
||||
{
|
||||
try build(.project(projectPath),
|
||||
target: target,
|
||||
into: outputDirectory)
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
fileprivate func build(_ projectTarget: XcodeBuildTarget,
|
||||
target: Target,
|
||||
into outputDirectory: AbsolutePath) throws
|
||||
{
|
||||
guard target.product.isFramework else {
|
||||
throw CacheBinaryBuilderError.nonFrameworkTargetForXCFramework(target.name)
|
||||
}
|
||||
let scheme = target.name.spm_shellEscaped()
|
||||
|
||||
// Create temporary directories
|
||||
return try FileHandler.shared.inTemporaryDirectory { temporaryDirectory in
|
||||
logger.notice("Building .xcframework for \(target.name)...", metadata: .section)
|
||||
|
||||
// Build for the simulator
|
||||
var simulatorArchivePath: AbsolutePath?
|
||||
if target.platform.hasSimulators {
|
||||
simulatorArchivePath = temporaryDirectory.appending(component: "simulator.xcarchive")
|
||||
try simulatorBuild(
|
||||
projectTarget: projectTarget,
|
||||
scheme: scheme,
|
||||
target: target,
|
||||
archivePath: simulatorArchivePath!
|
||||
)
|
||||
}
|
||||
|
||||
// Build for the device - if required
|
||||
let deviceArchivePath = temporaryDirectory.appending(component: "device.xcarchive")
|
||||
try deviceBuild(
|
||||
projectTarget: projectTarget,
|
||||
scheme: scheme,
|
||||
target: target,
|
||||
archivePath: deviceArchivePath
|
||||
)
|
||||
|
||||
// Build the xcframework
|
||||
var frameworkpaths = [AbsolutePath]()
|
||||
if let simulatorArchivePath = simulatorArchivePath {
|
||||
frameworkpaths.append(frameworkPath(fromArchivePath: simulatorArchivePath, productName: target.productName))
|
||||
}
|
||||
frameworkpaths.append(frameworkPath(fromArchivePath: deviceArchivePath, productName: target.productName))
|
||||
let xcframeworkPath = outputDirectory.appending(component: "\(target.productName).xcframework")
|
||||
try buildXCFramework(frameworks: frameworkpaths, output: xcframeworkPath, target: target)
|
||||
|
||||
try FileHandler.shared.move(from: xcframeworkPath, to: outputDirectory.appending(component: xcframeworkPath.basename))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func buildXCFramework(frameworks: [AbsolutePath], output: AbsolutePath, target: Target) throws {
|
||||
_ = try xcodeBuildController.createXCFramework(frameworks: frameworks, output: output)
|
||||
.do(onSubscribed: {
|
||||
logger.notice("Exporting xcframework for \(target.platform.caseValue)", metadata: .subsection)
|
||||
})
|
||||
.toBlocking()
|
||||
.single()
|
||||
}
|
||||
|
||||
fileprivate func deviceBuild(projectTarget: XcodeBuildTarget,
|
||||
scheme: String,
|
||||
target: Target,
|
||||
archivePath: AbsolutePath) throws
|
||||
{
|
||||
// Without the BUILD_LIBRARY_FOR_DISTRIBUTION argument xcodebuild doesn't generate the .swiftinterface file
|
||||
_ = try xcodeBuildController.archive(projectTarget,
|
||||
scheme: scheme,
|
||||
clean: false,
|
||||
archivePath: archivePath,
|
||||
arguments: [
|
||||
.sdk(target.platform.xcodeDeviceSDK),
|
||||
.buildSetting("SKIP_INSTALL", "NO"),
|
||||
.buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES"),
|
||||
])
|
||||
.printFormattedOutput()
|
||||
.do(onSubscribed: {
|
||||
logger.notice("Building \(target.name) for device...", metadata: .subsection)
|
||||
})
|
||||
.ignoreElements()
|
||||
.toBlocking()
|
||||
.last()
|
||||
}
|
||||
|
||||
fileprivate func simulatorBuild(projectTarget: XcodeBuildTarget,
|
||||
scheme: String,
|
||||
target: Target,
|
||||
archivePath: AbsolutePath) throws
|
||||
{
|
||||
// Without the BUILD_LIBRARY_FOR_DISTRIBUTION argument xcodebuild doesn't generate the .swiftinterface file
|
||||
_ = try xcodeBuildController.archive(projectTarget,
|
||||
scheme: scheme,
|
||||
clean: false,
|
||||
archivePath: archivePath,
|
||||
arguments: [
|
||||
.sdk(target.platform.xcodeSimulatorSDK!),
|
||||
.buildSetting("SKIP_INSTALL", "NO"),
|
||||
.buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES"),
|
||||
])
|
||||
.printFormattedOutput()
|
||||
.do(onSubscribed: {
|
||||
logger.notice("Building \(target.name) for simulator...", metadata: .subsection)
|
||||
})
|
||||
.ignoreElements()
|
||||
.toBlocking()
|
||||
.last()
|
||||
}
|
||||
|
||||
/// 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"))
|
||||
}
|
||||
}
|
|
@ -1,189 +0,0 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
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.
|
||||
/// - includeDeviceArch: Define whether the .xcframework will also contain the target built for devices (it only contains the target built for simulators by default).
|
||||
/// - Returns: Path to the compiled .xcframework.
|
||||
func build(workspacePath: AbsolutePath, target: Target, includeDeviceArch: Bool) 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.
|
||||
/// - includeDeviceArch: Define whether the .xcframework will also contain the target built for devices (it only contains the target built for simulators by default).
|
||||
/// - Returns: Path to the compiled .xcframework.
|
||||
func build(projectPath: AbsolutePath, target: Target, includeDeviceArch: Bool) 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, includeDeviceArch: Bool) throws -> Observable<AbsolutePath> {
|
||||
try build(.workspace(workspacePath), target: target, includeDeviceArch: includeDeviceArch)
|
||||
}
|
||||
|
||||
public func build(projectPath: AbsolutePath, target: Target, includeDeviceArch: Bool) throws -> Observable<AbsolutePath> {
|
||||
try build(.project(projectPath), target: target, includeDeviceArch: includeDeviceArch)
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
fileprivate func deviceBuild(projectTarget: XcodeBuildTarget,
|
||||
scheme: String,
|
||||
target: Target,
|
||||
deviceArchivePath: AbsolutePath) -> Observable<SystemEvent<XcodeBuildOutput>>
|
||||
{
|
||||
// Without the BUILD_LIBRARY_FOR_DISTRIBUTION argument xcodebuild doesn't generate the .swiftinterface file
|
||||
xcodeBuildController.archive(projectTarget,
|
||||
scheme: scheme,
|
||||
clean: false,
|
||||
archivePath: deviceArchivePath,
|
||||
arguments: [
|
||||
.sdk(target.platform.xcodeDeviceSDK),
|
||||
.buildSetting("SKIP_INSTALL", "NO"),
|
||||
.buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES"),
|
||||
])
|
||||
.printFormattedOutput()
|
||||
.do(onSubscribed: {
|
||||
logger.notice("Building \(target.name) for device...", metadata: .subsection)
|
||||
})
|
||||
}
|
||||
|
||||
fileprivate func simulatorBuild(projectTarget: XcodeBuildTarget,
|
||||
scheme: String,
|
||||
target: Target,
|
||||
simulatorArchivePath: AbsolutePath) -> Observable<SystemEvent<XcodeBuildOutput>>
|
||||
{
|
||||
// Without the BUILD_LIBRARY_FOR_DISTRIBUTION argument xcodebuild doesn't generate the .swiftinterface file
|
||||
xcodeBuildController.archive(projectTarget,
|
||||
scheme: scheme,
|
||||
clean: false,
|
||||
archivePath: simulatorArchivePath,
|
||||
arguments: [
|
||||
.sdk(target.platform.xcodeSimulatorSDK!),
|
||||
.buildSetting("SKIP_INSTALL", "NO"),
|
||||
.buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES"),
|
||||
])
|
||||
.printFormattedOutput()
|
||||
.do(onSubscribed: {
|
||||
logger.notice("Building \(target.name) for simulator...", metadata: .subsection)
|
||||
})
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
fileprivate func build(_ projectTarget: XcodeBuildTarget, target: Target, includeDeviceArch: Bool) throws -> Observable<AbsolutePath> {
|
||||
guard target.product.isFramework else {
|
||||
throw XCFrameworkBuilderError.nonFrameworkTarget(target.name)
|
||||
}
|
||||
let scheme = target.name.spm_shellEscaped()
|
||||
|
||||
// Create temporary directories
|
||||
return try withTemporaryDirectories { outputDirectory, temporaryPath in
|
||||
logger.notice("Building .xcframework for \(target.name)...", metadata: .section)
|
||||
|
||||
// Build for the simulator
|
||||
var simulatorArchiveObservable: Observable<SystemEvent<XcodeBuildOutput>>
|
||||
var simulatorArchivePath: AbsolutePath?
|
||||
if target.platform.hasSimulators {
|
||||
simulatorArchivePath = temporaryPath.appending(component: "simulator.xcarchive")
|
||||
simulatorArchiveObservable = simulatorBuild(
|
||||
projectTarget: projectTarget,
|
||||
scheme: scheme,
|
||||
target: target,
|
||||
simulatorArchivePath: simulatorArchivePath!
|
||||
)
|
||||
} else {
|
||||
simulatorArchiveObservable = Observable.empty()
|
||||
}
|
||||
|
||||
// Build for the device - if required
|
||||
let deviceArchivePath = temporaryPath.appending(component: "device.xcarchive")
|
||||
var deviceArchiveObservable: Observable<SystemEvent<XcodeBuildOutput>>
|
||||
if includeDeviceArch {
|
||||
deviceArchiveObservable = deviceBuild(
|
||||
projectTarget: projectTarget,
|
||||
scheme: scheme,
|
||||
target: target,
|
||||
deviceArchivePath: deviceArchivePath
|
||||
)
|
||||
} else {
|
||||
deviceArchiveObservable = Observable.empty()
|
||||
}
|
||||
|
||||
// Build the xcframework
|
||||
var frameworkpaths: [AbsolutePath] = [AbsolutePath]()
|
||||
if let simulatorArchivePath = simulatorArchivePath {
|
||||
frameworkpaths.append(frameworkPath(fromArchivePath: simulatorArchivePath, productName: target.productName))
|
||||
}
|
||||
if includeDeviceArch {
|
||||
frameworkpaths.append(frameworkPath(fromArchivePath: deviceArchivePath, productName: target.productName))
|
||||
}
|
||||
|
||||
let xcframeworkPath = outputDirectory.appending(component: "\(target.productName).xcframework")
|
||||
let xcframeworkObservable = xcodeBuildController.createXCFramework(frameworks: frameworkpaths, output: xcframeworkPath)
|
||||
.do(onSubscribed: {
|
||||
logger.notice("Exporting xcframework for \(target.platform.caseValue)", metadata: .subsection)
|
||||
})
|
||||
|
||||
return deviceArchiveObservable
|
||||
.concat(simulatorArchiveObservable)
|
||||
.concat(xcframeworkObservable)
|
||||
.ignoreElements()
|
||||
.andThen(Observable.just(xcframeworkPath))
|
||||
.do(afterCompleted: {
|
||||
try FileHandler.shared.delete(temporaryPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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"))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCache
|
||||
import TuistCore
|
||||
import TuistSupportTesting
|
||||
|
||||
public final class MockCacheArtifactBuilder: CacheArtifactBuilding {
|
||||
public init() {}
|
||||
|
||||
public var invokedCacheOutputTypeGetter = false
|
||||
public var invokedCacheOutputTypeGetterCount = 0
|
||||
public var stubbedCacheOutputType: CacheOutputType!
|
||||
|
||||
public var cacheOutputType: CacheOutputType {
|
||||
invokedCacheOutputTypeGetter = true
|
||||
invokedCacheOutputTypeGetterCount += 1
|
||||
return stubbedCacheOutputType
|
||||
}
|
||||
|
||||
public var invokedBuildWorkspacePath = false
|
||||
public var invokedBuildWorkspacePathCount = 0
|
||||
public var invokedBuildWorkspacePathParameters: (workspacePath: AbsolutePath, target: Target, outputDirectory: AbsolutePath)?
|
||||
public var invokedBuildWorkspacePathParametersList = [(workspacePath: AbsolutePath, target: Target, outputDirectory: AbsolutePath)]()
|
||||
public var stubbedBuildWorkspacePathError: Error?
|
||||
|
||||
public func build(workspacePath: AbsolutePath, target: Target, into outputDirectory: AbsolutePath) throws {
|
||||
invokedBuildWorkspacePath = true
|
||||
invokedBuildWorkspacePathCount += 1
|
||||
invokedBuildWorkspacePathParameters = (workspacePath, target, outputDirectory)
|
||||
invokedBuildWorkspacePathParametersList.append((workspacePath, target, outputDirectory))
|
||||
if let error = stubbedBuildWorkspacePathError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public var invokedBuildProjectPath = false
|
||||
public var invokedBuildProjectPathCount = 0
|
||||
public var invokedBuildProjectPathParameters: (projectPath: AbsolutePath, target: Target, outputDirectory: AbsolutePath)?
|
||||
public var invokedBuildProjectPathParametersList = [(projectPath: AbsolutePath, target: Target, outputDirectory: AbsolutePath)]()
|
||||
public var stubbedBuildProjectPathError: Error?
|
||||
|
||||
public func build(projectPath: AbsolutePath, target: Target, into outputDirectory: AbsolutePath) throws {
|
||||
invokedBuildProjectPath = true
|
||||
invokedBuildProjectPathCount += 1
|
||||
invokedBuildProjectPathParameters = (projectPath, target, outputDirectory)
|
||||
invokedBuildProjectPathParametersList.append((projectPath, target, outputDirectory))
|
||||
if let error = stubbedBuildProjectPathError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,10 +30,10 @@ public final class MockCacheStorage: CacheStoring {
|
|||
}
|
||||
}
|
||||
|
||||
var storeStub: ((_ hash: String, _ xcframeworkPath: AbsolutePath) -> Void)?
|
||||
public func store(hash: String, xcframeworkPath: AbsolutePath) -> Completable {
|
||||
var storeStub: ((_ hash: String, _ paths: [AbsolutePath]) -> Void)?
|
||||
public func store(hash: String, paths: [AbsolutePath]) -> Completable {
|
||||
if let storeStub = storeStub {
|
||||
storeStub(hash, xcframeworkPath)
|
||||
storeStub(hash, paths)
|
||||
}
|
||||
return Completable.empty()
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistCache
|
||||
import TuistCore
|
||||
|
||||
public final class MockXCFrameworkBuilder: XCFrameworkBuilding {
|
||||
public var buildProjectArgs: [(projectPath: AbsolutePath, target: Target, includeDeviceArch: Bool)] = []
|
||||
public var buildWorkspaceArgs: [(workspacePath: AbsolutePath, target: Target, includeDeviceArch: Bool)] = []
|
||||
public var buildProjectStub: ((AbsolutePath, Target) -> Result<AbsolutePath, Error>)?
|
||||
public var buildWorkspaceStub: ((AbsolutePath, Target) -> Result<AbsolutePath, Error>)?
|
||||
|
||||
public init() {}
|
||||
|
||||
public func build(projectPath: AbsolutePath, target: Target, includeDeviceArch: Bool) throws -> Observable<AbsolutePath> {
|
||||
buildProjectArgs.append((projectPath: projectPath, target: target, includeDeviceArch: includeDeviceArch))
|
||||
if let buildProjectStub = buildProjectStub {
|
||||
switch buildProjectStub(projectPath, target) {
|
||||
case let .failure(error):
|
||||
return Observable.error(error)
|
||||
case let .success(path):
|
||||
return Observable.just(path)
|
||||
}
|
||||
} else {
|
||||
return Observable.just(AbsolutePath.root)
|
||||
}
|
||||
}
|
||||
|
||||
public func build(workspacePath: AbsolutePath, target: Target, includeDeviceArch: Bool) throws -> Observable<AbsolutePath> {
|
||||
buildWorkspaceArgs.append((workspacePath: workspacePath, target: target, includeDeviceArch: includeDeviceArch))
|
||||
if let buildWorkspaceStub = buildWorkspaceStub {
|
||||
switch buildWorkspaceStub(workspacePath, target) {
|
||||
case let .failure(error):
|
||||
return Observable.error(error)
|
||||
case let .success(path):
|
||||
return Observable.just(path)
|
||||
}
|
||||
} else {
|
||||
return Observable.just(AbsolutePath.root)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,23 +3,23 @@ import TuistCore
|
|||
@testable import TuistCache
|
||||
|
||||
public final class MockGraphContentHasher: GraphContentHashing {
|
||||
public init() {}
|
||||
|
||||
public var invokedContentHashes = false
|
||||
public var invokedContentHashesCount = 0
|
||||
public var invokedContentHashesParameters: (graph: TuistCore.Graph, Void)?
|
||||
public var invokedContentHashesParametersList = [(graph: TuistCore.Graph, Void)]()
|
||||
public var invokedContentHashesParameters: (graph: TuistCore.Graph, cacheOutputType: CacheOutputType)?
|
||||
public var invokedContentHashesParametersList = [(graph: TuistCore.Graph, cacheOutputType: CacheOutputType)]()
|
||||
public var stubbedContentHashesError: Error?
|
||||
public var contentHashesStub: [TargetNode: String]! = [:]
|
||||
public var stubbedContentHashesResult: [TargetNode: String]! = [:]
|
||||
|
||||
public func contentHashes(for graph: TuistCore.Graph) throws -> [TargetNode: String] {
|
||||
public init() {}
|
||||
|
||||
public func contentHashes(for graph: TuistCore.Graph, cacheOutputType: CacheOutputType) throws -> [TargetNode: String] {
|
||||
invokedContentHashes = true
|
||||
invokedContentHashesCount += 1
|
||||
invokedContentHashesParameters = (graph, ())
|
||||
invokedContentHashesParametersList.append((graph, ()))
|
||||
invokedContentHashesParameters = (graph, cacheOutputType)
|
||||
invokedContentHashesParametersList.append((graph, cacheOutputType))
|
||||
if let error = stubbedContentHashesError {
|
||||
throw error
|
||||
}
|
||||
return contentHashesStub
|
||||
return stubbedContentHashesResult
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import Foundation
|
||||
|
||||
/// An enum that represents the type of output that the caching feature can work with.
|
||||
public enum CacheOutputType: CustomStringConvertible {
|
||||
/// Frameworks built for the simulator.
|
||||
case framework
|
||||
|
||||
/// XCFrameworks built for the simulator and device.
|
||||
case xcframework
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .framework:
|
||||
return "framework"
|
||||
case .xcframework:
|
||||
return "xcframework"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -495,7 +495,7 @@ public class Graph: Encodable, Equatable {
|
|||
stack.push(child)
|
||||
}
|
||||
} else if let frameworkNode = node as? FrameworkNode {
|
||||
for child in frameworkNode.dependencies where !visited.contains(child) {
|
||||
for child in frameworkNode.dependencies.map(\.node) where !visited.contains(child) {
|
||||
stack.push(child)
|
||||
}
|
||||
}
|
||||
|
@ -630,8 +630,8 @@ public class Graph: Encodable, Equatable {
|
|||
stack.push(child)
|
||||
}
|
||||
} else if let frameworkNode = node as? FrameworkNode {
|
||||
for child in frameworkNode.dependencies where !visited.contains(child) {
|
||||
stack.push(child)
|
||||
for child in frameworkNode.dependencies where !visited.contains(child.node) {
|
||||
stack.push(child.node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,6 @@ public class FrameworkNode: PrecompiledNode {
|
|||
/// The architectures supported by the binary.
|
||||
public let architectures: [BinaryArchitecture]
|
||||
|
||||
/// Framework dependencies.
|
||||
public let dependencies: [FrameworkNode]
|
||||
|
||||
/// Returns the type of product.
|
||||
public var product: Product {
|
||||
if linking == .static {
|
||||
|
@ -42,14 +39,13 @@ public class FrameworkNode: PrecompiledNode {
|
|||
bcsymbolmapPaths: [AbsolutePath],
|
||||
linking: BinaryLinking,
|
||||
architectures: [BinaryArchitecture] = [],
|
||||
dependencies: [FrameworkNode] = [])
|
||||
dependencies: [Dependency] = [])
|
||||
{
|
||||
self.dsymPath = dsymPath
|
||||
self.bcsymbolmapPaths = bcsymbolmapPaths
|
||||
self.linking = linking
|
||||
self.architectures = architectures
|
||||
self.dependencies = dependencies
|
||||
super.init(path: path)
|
||||
super.init(path: path, dependencies: dependencies)
|
||||
}
|
||||
|
||||
override public func encode(to encoder: Encoder) throws {
|
||||
|
|
|
@ -2,7 +2,7 @@ import Foundation
|
|||
import TSCBasic
|
||||
import TuistSupport
|
||||
|
||||
public class GraphNode: Equatable, Hashable, Encodable, CustomStringConvertible {
|
||||
public class GraphNode: Equatable, Hashable, Encodable, CustomStringConvertible, CustomDebugStringConvertible {
|
||||
// MARK: - Attributes
|
||||
|
||||
/// The path to the node.
|
||||
|
@ -14,6 +14,9 @@ public class GraphNode: Equatable, Hashable, Encodable, CustomStringConvertible
|
|||
/// The description of the node.
|
||||
public var description: String { name }
|
||||
|
||||
/// The debug description of the node.
|
||||
public var debugDescription: String { name }
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(path: AbsolutePath, name: String) {
|
||||
|
|
|
@ -3,11 +3,37 @@ import TSCBasic
|
|||
import TuistSupport
|
||||
|
||||
public class PrecompiledNode: GraphNode {
|
||||
public init(path: AbsolutePath) {
|
||||
/// It represents a dependency of a precompiled node, which can be either a framework, or another .xcframework.
|
||||
public enum Dependency: Equatable, Hashable {
|
||||
case framework(FrameworkNode)
|
||||
case xcframework(XCFrameworkNode)
|
||||
|
||||
/// Path to the dependency.
|
||||
public var path: AbsolutePath {
|
||||
switch self {
|
||||
case let .framework(framework): return framework.path
|
||||
case let .xcframework(xcframework): return xcframework.path
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the node that represents the dependency.
|
||||
public var node: PrecompiledNode {
|
||||
switch self {
|
||||
case let .framework(framework): return framework
|
||||
case let .xcframework(xcframework): return xcframework
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List of other precompiled artifacts this precompiled node depends on.
|
||||
public private(set) var dependencies: [Dependency]
|
||||
|
||||
public init(path: AbsolutePath, dependencies: [Dependency] = []) {
|
||||
/// Returns the name of the precompiled node removing the extension
|
||||
/// Alamofire.framework -> Alamofire
|
||||
/// libAlamofire.a -> libAlamofire
|
||||
let name = String(path.components.last!.split(separator: ".").first!)
|
||||
self.dependencies = dependencies
|
||||
super.init(path: path, name: name)
|
||||
}
|
||||
|
||||
|
@ -29,4 +55,25 @@ public class PrecompiledNode: GraphNode {
|
|||
case product
|
||||
case type
|
||||
}
|
||||
|
||||
/// Adds a new dependency to the xcframework node.
|
||||
/// - Parameter dependency: Dependency to be added.
|
||||
public func add(dependency: Dependency) {
|
||||
dependencies.append(dependency)
|
||||
}
|
||||
|
||||
// MARK: - CustomDebugStringConvertible
|
||||
|
||||
override public var debugDescription: String {
|
||||
if dependencies.isEmpty {
|
||||
return name
|
||||
}
|
||||
var dependenciesDescriptions: [String] = []
|
||||
let uniqueDependencies = Set<Dependency>(dependencies)
|
||||
uniqueDependencies.forEach { dependency in
|
||||
dependenciesDescriptions.append(dependency.node.description)
|
||||
}
|
||||
|
||||
return "\(name) --> [\(dependenciesDescriptions.joined(separator: ", "))]"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,28 +3,6 @@ import TSCBasic
|
|||
import TuistSupport
|
||||
|
||||
public class XCFrameworkNode: PrecompiledNode {
|
||||
/// It represents a dependency of an .xcframework which can be either a framework, or another .xcframework.
|
||||
public enum Dependency: Equatable {
|
||||
case framework(FrameworkNode)
|
||||
case xcframework(XCFrameworkNode)
|
||||
|
||||
/// Path to the dependency.
|
||||
public var path: AbsolutePath {
|
||||
switch self {
|
||||
case let .framework(framework): return framework.path
|
||||
case let .xcframework(xcframework): return xcframework.path
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the node that represents the dependency.
|
||||
public var node: PrecompiledNode {
|
||||
switch self {
|
||||
case let .framework(framework): return framework
|
||||
case let .xcframework(xcframework): return xcframework
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Coding keys.
|
||||
enum XCFrameworkNodeCodingKeys: String, CodingKey {
|
||||
case linking
|
||||
|
@ -43,9 +21,6 @@ public class XCFrameworkNode: PrecompiledNode {
|
|||
/// Returns the type of linking
|
||||
public let linking: BinaryLinking
|
||||
|
||||
/// List of other .xcframeworks this xcframework depends on.
|
||||
public private(set) var dependencies: [Dependency]
|
||||
|
||||
/// Path to the binary.
|
||||
override public var binaryPath: AbsolutePath { primaryBinaryPath }
|
||||
|
||||
|
@ -65,8 +40,7 @@ public class XCFrameworkNode: PrecompiledNode {
|
|||
self.infoPlist = infoPlist
|
||||
self.linking = linking
|
||||
self.primaryBinaryPath = primaryBinaryPath
|
||||
self.dependencies = dependencies
|
||||
super.init(path: path)
|
||||
super.init(path: path, dependencies: dependencies)
|
||||
}
|
||||
|
||||
override public func encode(to encoder: Encoder) throws {
|
||||
|
@ -77,10 +51,4 @@ public class XCFrameworkNode: PrecompiledNode {
|
|||
try container.encode("xcframework", forKey: .type)
|
||||
try container.encode(infoPlist, forKey: .infoPlist)
|
||||
}
|
||||
|
||||
/// Adds a new dependency to the xcframework node.
|
||||
/// - Parameter dependency: Dependency to be added.
|
||||
public func add(dependency: Dependency) {
|
||||
dependencies.append(dependency)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import Foundation
|
|||
import TSCBasic
|
||||
import TuistSupport
|
||||
|
||||
protocol FrameworkMetadataProviding: PrecompiledMetadataProviding {
|
||||
public protocol FrameworkMetadataProviding: PrecompiledMetadataProviding {
|
||||
/// Given the path to a framework, it returns the path to its dSYMs if they exist
|
||||
/// in the same framework directory.
|
||||
/// - Parameter frameworkPath: Path to the .framework directory.
|
||||
|
@ -18,14 +18,14 @@ protocol FrameworkMetadataProviding: PrecompiledMetadataProviding {
|
|||
func product(frameworkPath: AbsolutePath) throws -> Product
|
||||
}
|
||||
|
||||
final class FrameworkMetadataProvider: PrecompiledMetadataProvider, FrameworkMetadataProviding {
|
||||
func dsymPath(frameworkPath: AbsolutePath) -> AbsolutePath? {
|
||||
public final class FrameworkMetadataProvider: PrecompiledMetadataProvider, FrameworkMetadataProviding {
|
||||
public func dsymPath(frameworkPath: AbsolutePath) -> AbsolutePath? {
|
||||
let path = AbsolutePath("\(frameworkPath.pathString).dSYM")
|
||||
if FileHandler.shared.exists(path) { return path }
|
||||
return nil
|
||||
}
|
||||
|
||||
func bcsymbolmapPaths(frameworkPath: AbsolutePath) throws -> [AbsolutePath] {
|
||||
public func bcsymbolmapPaths(frameworkPath: AbsolutePath) throws -> [AbsolutePath] {
|
||||
let binaryPath = FrameworkNode.binaryPath(frameworkPath: frameworkPath)
|
||||
let uuids = try self.uuids(binaryPath: binaryPath)
|
||||
return uuids
|
||||
|
@ -34,7 +34,7 @@ final class FrameworkMetadataProvider: PrecompiledMetadataProvider, FrameworkMet
|
|||
.sorted()
|
||||
}
|
||||
|
||||
func product(frameworkPath: AbsolutePath) throws -> Product {
|
||||
public func product(frameworkPath: AbsolutePath) throws -> Product {
|
||||
let binaryPath = FrameworkNode.binaryPath(frameworkPath: frameworkPath)
|
||||
switch try linking(binaryPath: binaryPath) {
|
||||
case .dynamic:
|
||||
|
|
|
@ -31,7 +31,7 @@ enum PrecompiledMetadataProviderError: FatalError, Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
protocol PrecompiledMetadataProviding {
|
||||
public protocol PrecompiledMetadataProviding {
|
||||
/// It returns the supported architectures of the binary at the given path.
|
||||
/// - Parameter binaryPath: Binary path.
|
||||
func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture]
|
||||
|
@ -46,10 +46,10 @@ protocol PrecompiledMetadataProviding {
|
|||
func uuids(binaryPath: AbsolutePath) throws -> Set<UUID>
|
||||
}
|
||||
|
||||
class PrecompiledMetadataProvider: PrecompiledMetadataProviding {
|
||||
public class PrecompiledMetadataProvider: PrecompiledMetadataProviding {
|
||||
public init() {}
|
||||
|
||||
func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture] {
|
||||
public func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture] {
|
||||
let result = try System.shared.capture("/usr/bin/lipo", "-info", binaryPath.pathString).spm_chuzzle() ?? ""
|
||||
let regexes = [
|
||||
// Non-fat file: path is architecture: x86_64
|
||||
|
@ -70,12 +70,12 @@ class PrecompiledMetadataProvider: PrecompiledMetadataProviding {
|
|||
return architectures
|
||||
}
|
||||
|
||||
func linking(binaryPath: AbsolutePath) throws -> BinaryLinking {
|
||||
public func linking(binaryPath: AbsolutePath) throws -> BinaryLinking {
|
||||
let result = try System.shared.capture("/usr/bin/file", binaryPath.pathString).spm_chuzzle() ?? ""
|
||||
return result.contains("dynamically linked") ? .dynamic : .static
|
||||
}
|
||||
|
||||
func uuids(binaryPath: AbsolutePath) throws -> Set<UUID> {
|
||||
public func uuids(binaryPath: AbsolutePath) throws -> Set<UUID> {
|
||||
let output = try System.shared.capture(["/usr/bin/xcrun", "dwarfdump", "--uuid", binaryPath.pathString])
|
||||
// UUIDs are letters, decimals, or hyphens.
|
||||
var uuidCharacterSet = CharacterSet()
|
||||
|
|
|
@ -13,3 +13,11 @@ public enum BinaryArchitecture: String, Codable {
|
|||
public enum BinaryLinking: String, Codable {
|
||||
case `static`, dynamic
|
||||
}
|
||||
|
||||
public extension Sequence where Element == BinaryArchitecture {
|
||||
/// Returns true if all the architectures are only for simulator.
|
||||
var onlySimulator: Bool {
|
||||
let simulatorArchitectures: [BinaryArchitecture] = [.x8664, .i386]
|
||||
return allSatisfy { simulatorArchitectures.contains($0) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,17 +28,17 @@ public protocol FrameworkNodeLoading {
|
|||
func load(path: AbsolutePath) throws -> FrameworkNode
|
||||
}
|
||||
|
||||
final class FrameworkNodeLoader: FrameworkNodeLoading {
|
||||
public final class FrameworkNodeLoader: FrameworkNodeLoading {
|
||||
/// Framework metadata provider.
|
||||
fileprivate let frameworkMetadataProvider: FrameworkMetadataProviding
|
||||
|
||||
/// Initializes the loader with its attributes.
|
||||
/// - Parameter frameworkMetadataProvider: Framework metadata provider.
|
||||
init(frameworkMetadataProvider: FrameworkMetadataProviding = FrameworkMetadataProvider()) {
|
||||
public init(frameworkMetadataProvider: FrameworkMetadataProviding = FrameworkMetadataProvider()) {
|
||||
self.frameworkMetadataProvider = frameworkMetadataProvider
|
||||
}
|
||||
|
||||
func load(path: AbsolutePath) throws -> FrameworkNode {
|
||||
public func load(path: AbsolutePath) throws -> FrameworkNode {
|
||||
guard FileHandler.shared.exists(path) else {
|
||||
throw FrameworkNodeLoaderError.frameworkNotFound(path)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import Foundation
|
|||
import RxSwift
|
||||
import TuistSupport
|
||||
|
||||
protocol SimulatorControlling {
|
||||
public protocol SimulatorControlling {
|
||||
/// Returns the list of simulator devices that are available in the system.
|
||||
func devices() -> Single<[SimulatorDevice]>
|
||||
|
||||
|
@ -13,26 +13,28 @@ protocol SimulatorControlling {
|
|||
func devicesAndRuntimes() -> Single<[SimulatorDeviceAndRuntime]>
|
||||
}
|
||||
|
||||
enum SimulatorControllerError: FatalError {
|
||||
public enum SimulatorControllerError: FatalError {
|
||||
case simctlError(String)
|
||||
|
||||
var type: ErrorType {
|
||||
public var type: ErrorType {
|
||||
switch self {
|
||||
case .simctlError: return .abort
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case let .simctlError(output): return output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class SimulatorController: SimulatorControlling {
|
||||
private let jsonDecoder: JSONDecoder = JSONDecoder()
|
||||
public final class SimulatorController: SimulatorControlling {
|
||||
private let jsonDecoder = JSONDecoder()
|
||||
|
||||
func devices() -> Single<[SimulatorDevice]> {
|
||||
public init() {}
|
||||
|
||||
public func devices() -> Single<[SimulatorDevice]> {
|
||||
System.shared.observable(["/usr/bin/xcrun", "simctl", "list", "devices", "--json"])
|
||||
.mapToString()
|
||||
.collectOutput()
|
||||
|
@ -63,9 +65,8 @@ final class SimulatorController: SimulatorControlling {
|
|||
}
|
||||
}
|
||||
|
||||
func runtimes() -> Single<[SimulatorRuntime]> {
|
||||
public func runtimes() -> Single<[SimulatorRuntime]> {
|
||||
System.shared.observable(["/usr/bin/xcrun", "simctl", "list", "runtimes", "--json"])
|
||||
.debug()
|
||||
.mapToString()
|
||||
.collectOutput()
|
||||
.asSingle()
|
||||
|
@ -88,7 +89,7 @@ final class SimulatorController: SimulatorControlling {
|
|||
}
|
||||
}
|
||||
|
||||
func devicesAndRuntimes() -> Single<[SimulatorDeviceAndRuntime]> {
|
||||
public func devicesAndRuntimes() -> Single<[SimulatorDeviceAndRuntime]> {
|
||||
runtimes()
|
||||
.flatMap { (runtimes) -> Single<([SimulatorDevice], [SimulatorRuntime])> in
|
||||
self.devices().map { ($0, runtimes) }
|
|
@ -2,40 +2,40 @@ import Foundation
|
|||
import TSCBasic
|
||||
|
||||
/// It represents a simulator device. Devices are obtained using Xcode's CLI simctl
|
||||
struct SimulatorDevice: Decodable, Hashable, CustomStringConvertible {
|
||||
public struct SimulatorDevice: Decodable, Hashable, CustomStringConvertible {
|
||||
/// Device data path.
|
||||
let dataPath: AbsolutePath
|
||||
public let dataPath: AbsolutePath
|
||||
|
||||
/// Device log path.
|
||||
let logPath: AbsolutePath
|
||||
public let logPath: AbsolutePath
|
||||
|
||||
/// Device unique identifier (3A8C9673-C1FD-4E33-8EFA-AEEBF43161CC)
|
||||
let udid: String
|
||||
public let udid: String
|
||||
|
||||
/// Whether the device is available or not.
|
||||
let isAvailable: Bool
|
||||
public let isAvailable: Bool
|
||||
|
||||
/// Device type identifier (e.g. com.apple.CoreSimulator.SimDeviceType.iPad-Air--3rd-generation-)
|
||||
let deviceTypeIdentifier: String?
|
||||
public let deviceTypeIdentifier: String?
|
||||
|
||||
/// Device state (e.g. Shutdown)
|
||||
let state: String
|
||||
public let state: String
|
||||
|
||||
/// Returns true if the device is shutdown.
|
||||
var isShutdown: Bool {
|
||||
public var isShutdown: Bool {
|
||||
state == "Shutdown"
|
||||
}
|
||||
|
||||
/// Device name (e.g. iPad Air (3rd generation))
|
||||
let name: String
|
||||
public let name: String
|
||||
|
||||
/// If the device is not available, this provides a description of the error.
|
||||
let availabilityError: String?
|
||||
public let availabilityError: String?
|
||||
|
||||
/// Device runtime identifier (e.g. com.apple.CoreSimulator.SimRuntime.iOS-13-5)
|
||||
let runtimeIdentifier: String
|
||||
public let runtimeIdentifier: String
|
||||
|
||||
var description: String {
|
||||
public var description: String {
|
||||
name
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import Foundation
|
||||
|
||||
public struct SimulatorDeviceAndRuntime: Hashable {
|
||||
/// Device
|
||||
public let device: SimulatorDevice
|
||||
|
||||
/// Device's runtime.
|
||||
public let runtime: SimulatorRuntime
|
||||
}
|
|
@ -3,27 +3,27 @@ import TSCBasic
|
|||
|
||||
/// It represents a runtime that is available in the system. The list of available runtimes is obtained
|
||||
/// using Xcode's simctl cli tool.
|
||||
struct SimulatorRuntime: Decodable, Hashable, CustomStringConvertible {
|
||||
public struct SimulatorRuntime: Decodable, Hashable, CustomStringConvertible {
|
||||
/// Runtime bundle path (e.g. /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime)
|
||||
let bundlePath: AbsolutePath
|
||||
public let bundlePath: AbsolutePath
|
||||
|
||||
/// Runtime build version (e.g. 17F61)
|
||||
let buildVersion: String
|
||||
public let buildVersion: String
|
||||
|
||||
/// Runtime root (e.g. /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes\/iOS.simruntime\Contents/Resources/RuntimeRoot)
|
||||
let runtimeRoot: AbsolutePath
|
||||
public let runtimeRoot: AbsolutePath
|
||||
|
||||
/// Runtime identifier (e.g. com.apple.CoreSimulator.SimRuntime.iOS-13-5)
|
||||
let identifier: String
|
||||
public let identifier: String
|
||||
|
||||
/// Runtime version (e.g. 13.5)
|
||||
let version: SimulatorRuntimeVersion
|
||||
public let version: SimulatorRuntimeVersion
|
||||
|
||||
// True if the runtime is available.
|
||||
let isAvailable: Bool
|
||||
public let isAvailable: Bool
|
||||
|
||||
// Name of the runtime (e.g. iOS 13.5)
|
||||
let name: String
|
||||
public let name: String
|
||||
|
||||
init(bundlePath: AbsolutePath,
|
||||
buildVersion: String,
|
||||
|
@ -52,7 +52,7 @@ struct SimulatorRuntime: Decodable, Hashable, CustomStringConvertible {
|
|||
case name
|
||||
}
|
||||
|
||||
var description: String {
|
||||
public var description: String {
|
||||
name
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import Foundation
|
||||
|
||||
struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleByStringLiteral, Comparable, Decodable {
|
||||
public struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleByStringLiteral, Comparable, Decodable {
|
||||
// MARK: - Attributes
|
||||
|
||||
let major: Int
|
||||
let minor: Int?
|
||||
let patch: Int?
|
||||
public let major: Int
|
||||
public let minor: Int?
|
||||
public let patch: Int?
|
||||
|
||||
// MARK: - Constructors
|
||||
|
||||
|
@ -15,7 +15,7 @@ struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleBy
|
|||
self.patch = patch
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
self.init(stringLiteral: try container.decode(String.self))
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleBy
|
|||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
var description: String {
|
||||
public var description: String {
|
||||
var version = "\(major)"
|
||||
if let minor = minor {
|
||||
version.append(".\(minor)")
|
||||
|
@ -47,7 +47,7 @@ struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleBy
|
|||
|
||||
// MARK: - Equatable
|
||||
|
||||
static func == (lhs: SimulatorRuntimeVersion, rhs: SimulatorRuntimeVersion) -> Bool {
|
||||
public static func == (lhs: SimulatorRuntimeVersion, rhs: SimulatorRuntimeVersion) -> Bool {
|
||||
lhs.major == rhs.major &&
|
||||
lhs.minor == rhs.minor &&
|
||||
lhs.patch == rhs.patch
|
||||
|
@ -55,7 +55,7 @@ struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleBy
|
|||
|
||||
// MARK: - Comparable
|
||||
|
||||
static func < (lhs: SimulatorRuntimeVersion, rhs: SimulatorRuntimeVersion) -> Bool {
|
||||
public static func < (lhs: SimulatorRuntimeVersion, rhs: SimulatorRuntimeVersion) -> Bool {
|
||||
let lhs = lhs.flattened()
|
||||
let rhs = rhs.flattened()
|
||||
|
||||
|
@ -76,7 +76,7 @@ struct SimulatorRuntimeVersion: CustomStringConvertible, Hashable, ExpressibleBy
|
|||
|
||||
// MARK: - ExpressibleByStringLiteral
|
||||
|
||||
init(stringLiteral value: String) {
|
||||
public init(stringLiteral value: String) {
|
||||
let components = value.split(separator: ".")
|
||||
|
||||
// Major
|
|
@ -72,7 +72,7 @@ public struct ValueGraph: Equatable {
|
|||
if let targetNode = node as? TargetNode {
|
||||
targetNode.dependencies.forEach { nodeDependencies.formUnion([self.dependency(from: $0)]) }
|
||||
} else if let frameworkNode = node as? FrameworkNode {
|
||||
frameworkNode.dependencies.forEach { nodeDependencies.formUnion([self.dependency(from: $0)]) }
|
||||
frameworkNode.dependencies.forEach { nodeDependencies.formUnion([self.dependency(from: $0.node)]) }
|
||||
} else if let xcframeworkNode = node as? XCFrameworkNode {
|
||||
xcframeworkNode.dependencies.forEach { nodeDependencies.formUnion([self.dependency(from: $0.node)]) }
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ public extension FrameworkNode {
|
|||
bcsymbolmapPaths: [AbsolutePath] = [],
|
||||
linking: BinaryLinking = .dynamic,
|
||||
architectures: [BinaryArchitecture] = [],
|
||||
dependencies: [FrameworkNode] = []) -> FrameworkNode
|
||||
dependencies: [PrecompiledNode.Dependency] = []) -> FrameworkNode
|
||||
{
|
||||
FrameworkNode(path: path,
|
||||
dsymPath: dsymPath,
|
||||
|
|
|
@ -8,7 +8,7 @@ public extension XCFrameworkNode {
|
|||
infoPlist: XCFrameworkInfoPlist = .test(),
|
||||
primaryBinaryPath: AbsolutePath = "/MyFramework/MyFramework.xcframework/binary",
|
||||
linking: BinaryLinking = .dynamic,
|
||||
dependencies: [XCFrameworkNode.Dependency] = []) -> XCFrameworkNode
|
||||
dependencies: [PrecompiledNode.Dependency] = []) -> XCFrameworkNode
|
||||
{
|
||||
XCFrameworkNode(path: path,
|
||||
infoPlist: infoPlist,
|
||||
|
|
|
@ -2,7 +2,7 @@ import Foundation
|
|||
import RxSwift
|
||||
import TuistSupport
|
||||
|
||||
@testable import TuistAutomation
|
||||
@testable import TuistCore
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
public final class MockSimulatorController: SimulatorControlling {
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
@testable import TuistAutomation
|
||||
@testable import TuistCore
|
||||
|
||||
extension SimulatorDevice {
|
||||
static func test(dataPath: AbsolutePath = "/Library/Developer/CoreSimulator/Devices/3A8C9673-C1FD-4E33-8EFA-AEEBF43161CC/data",
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
@testable import TuistAutomation
|
||||
@testable import TuistCore
|
||||
|
||||
extension SimulatorDeviceAndRuntime {
|
||||
static func test(device: SimulatorDevice = .test(),
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
@testable import TuistAutomation
|
||||
@testable import TuistCore
|
||||
|
||||
extension SimulatorRuntime {
|
||||
// swiftlint:disable:next line_length
|
|
@ -0,0 +1,23 @@
|
|||
import TuistCore
|
||||
|
||||
/// A struct that groups the configuration for caching.
|
||||
struct CacheConfig {
|
||||
/// A boolean that indicates whether the cache is enabled or not.
|
||||
let cache: Bool
|
||||
|
||||
/// It indicates whether the cache should work with simulator frameworks, or xcframeworks built for simulator and device.
|
||||
let cacheOutputType: CacheOutputType
|
||||
|
||||
/// A static initializer that returns a config with the caching disabled.
|
||||
/// - Returns: An instance of the config.
|
||||
static func withoutCaching() -> CacheConfig {
|
||||
CacheConfig(cache: false, cacheOutputType: .framework)
|
||||
}
|
||||
|
||||
/// A static initializer that returns a config with the caching enabled.
|
||||
/// - Parameter cacheOutputType: It indicates whether the cache should work with simulator frameworks, or xcframeworks built for simulator and device.
|
||||
/// - Returns: An instance of the config.
|
||||
static func withCaching(cacheOutputType: CacheOutputType = .framework) -> CacheConfig {
|
||||
CacheConfig(cache: true, cacheOutputType: cacheOutputType)
|
||||
}
|
||||
}
|
|
@ -14,16 +14,15 @@ protocol CacheControlling {
|
|||
/// Caches the cacheable targets that are part of the workspace or project at the given path.
|
||||
/// - Parameters:
|
||||
/// - path: Path to the directory that contains a workspace or a project.
|
||||
/// - includeDeviceArch: Define whether the .xcframework will also contain the target built for devices (it only contains the target built for simulators by default).
|
||||
func cache(path: AbsolutePath, includeDeviceArch: Bool) throws
|
||||
func cache(path: AbsolutePath) throws
|
||||
}
|
||||
|
||||
final class CacheController: CacheControlling {
|
||||
/// Xcode project generator.
|
||||
private let generator: ProjectGenerating
|
||||
|
||||
/// Utility to build the xcframeworks.
|
||||
private let xcframeworkBuilder: XCFrameworkBuilding
|
||||
/// Utility to build the (xc)frameworks.
|
||||
private let artifactBuilder: CacheArtifactBuilding
|
||||
|
||||
/// Graph content hasher.
|
||||
private let graphContentHasher: GraphContentHashing
|
||||
|
@ -31,46 +30,46 @@ final class CacheController: CacheControlling {
|
|||
/// Cache.
|
||||
private let cache: CacheStoring
|
||||
|
||||
convenience init(cache: CacheStoring) {
|
||||
convenience init(cache: CacheStoring, artifactBuilder: CacheArtifactBuilding) {
|
||||
self.init(cache: cache,
|
||||
artifactBuilder: artifactBuilder,
|
||||
generator: ProjectGenerator(),
|
||||
xcframeworkBuilder: XCFrameworkBuilder(xcodeBuildController: XcodeBuildController()),
|
||||
graphContentHasher: GraphContentHasher())
|
||||
}
|
||||
|
||||
init(cache: CacheStoring,
|
||||
artifactBuilder: CacheArtifactBuilding,
|
||||
generator: ProjectGenerating,
|
||||
xcframeworkBuilder: XCFrameworkBuilding,
|
||||
graphContentHasher: GraphContentHashing)
|
||||
{
|
||||
self.cache = cache
|
||||
self.generator = generator
|
||||
self.xcframeworkBuilder = xcframeworkBuilder
|
||||
self.artifactBuilder = artifactBuilder
|
||||
self.graphContentHasher = graphContentHasher
|
||||
}
|
||||
|
||||
func cache(path: AbsolutePath, includeDeviceArch: Bool) throws {
|
||||
func cache(path: AbsolutePath) throws {
|
||||
let (path, graph) = try generator.generateWithGraph(path: path, projectOnly: false)
|
||||
|
||||
logger.notice("Hashing cacheable frameworks")
|
||||
let cacheableTargets = try self.cacheableTargets(graph: graph)
|
||||
|
||||
let completables = try cacheableTargets.map { try buildAndCacheXCFramework(path: path,
|
||||
target: $0.key,
|
||||
hash: $0.value,
|
||||
includeDeviceArch: includeDeviceArch) }
|
||||
_ = try Completable.zip(completables).toBlocking().last()
|
||||
logger.notice("Building cacheable frameworks as \(artifactBuilder.cacheOutputType.description)s")
|
||||
|
||||
logger.notice("All cacheable frameworks have been cached successfully", metadata: .success)
|
||||
try cacheableTargets.sorted(by: { $0.key.target.name < $1.key.target.name }).forEach { target, hash in
|
||||
try self.buildAndCacheFramework(path: path, target: target, hash: hash)
|
||||
}
|
||||
|
||||
logger.notice("All cacheable frameworks have been cached successfully as \(artifactBuilder.cacheOutputType.description)s", metadata: .success)
|
||||
}
|
||||
|
||||
/// Returns all the targets that are cacheable and their hashes.
|
||||
/// - Parameter graph: Graph that contains all the dependency graph nodes.
|
||||
fileprivate func cacheableTargets(graph: Graph) throws -> [TargetNode: String] {
|
||||
try graphContentHasher.contentHashes(for: graph)
|
||||
try graphContentHasher.contentHashes(for: graph, cacheOutputType: artifactBuilder.cacheOutputType)
|
||||
.filter { target, hash in
|
||||
if let exists = try self.cache.exists(hash: hash).toBlocking().first(), exists {
|
||||
logger.pretty("The target \(.bold(.raw(target.name))) with hash \(.bold(.raw(hash))) is already in the cache. Skipping...")
|
||||
logger.pretty("The target \(.bold(.raw(target.name))) with hash \(.bold(.raw(hash))) and type \(artifactBuilder.cacheOutputType.description) is already in the cache. Skipping...")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
@ -80,40 +79,27 @@ final class CacheController: CacheControlling {
|
|||
/// Builds the .xcframework for the given target and returns an obervable to store them in the cache.
|
||||
/// - Parameters:
|
||||
/// - path: Path to either the .xcodeproj or .xcworkspace that contains the framework to be cached.
|
||||
/// - target: Target whose .xcframework will be built and cached.
|
||||
/// - target: Target whose .(xc)framework will be built and cached.
|
||||
/// - hash: Hash of the target.
|
||||
/// - includeDeviceArch: Define whether the .xcframework will also contain the target built for devices (it only contains the target built for simulators by default).
|
||||
fileprivate func buildAndCacheXCFramework(path: AbsolutePath,
|
||||
target: TargetNode,
|
||||
hash: String,
|
||||
includeDeviceArch: Bool) throws -> Completable
|
||||
fileprivate func buildAndCacheFramework(path: AbsolutePath,
|
||||
target: TargetNode,
|
||||
hash: String) throws
|
||||
{
|
||||
// Build targets sequentially
|
||||
let xcframeworkPath: AbsolutePath!
|
||||
|
||||
// Note: Since building XCFrameworks involves calling xcodebuild, we run the building process sequentially.
|
||||
if path.extension == "xcworkspace" {
|
||||
xcframeworkPath = try xcframeworkBuilder.build(workspacePath: path,
|
||||
target: target.target,
|
||||
includeDeviceArch: includeDeviceArch).toBlocking().single()
|
||||
} else {
|
||||
xcframeworkPath = try xcframeworkBuilder.build(projectPath: path,
|
||||
target: target.target,
|
||||
includeDeviceArch: includeDeviceArch).toBlocking().single()
|
||||
let outputDirectory = try FileHandler.shared.temporaryDirectory()
|
||||
defer {
|
||||
try? FileHandler.shared.delete(outputDirectory)
|
||||
}
|
||||
|
||||
// Create tasks to cache and delete the xcframeworks asynchronously
|
||||
let deleteXCFrameworkCompletable = Completable.create(subscribe: { completed in
|
||||
try? FileHandler.shared.delete(xcframeworkPath)
|
||||
completed(.completed)
|
||||
return Disposables.create()
|
||||
})
|
||||
return cache
|
||||
.store(hash: hash, xcframeworkPath: xcframeworkPath)
|
||||
.concat(deleteXCFrameworkCompletable)
|
||||
.catchError { error in
|
||||
// We propagate the error downstream
|
||||
deleteXCFrameworkCompletable.concat(Completable.error(error))
|
||||
}
|
||||
if path.extension == "xcworkspace" {
|
||||
try artifactBuilder.build(workspacePath: path,
|
||||
target: target.target,
|
||||
into: outputDirectory)
|
||||
} else {
|
||||
try artifactBuilder.build(projectPath: path,
|
||||
target: target.target,
|
||||
into: outputDirectory)
|
||||
}
|
||||
|
||||
_ = try cache.store(hash: hash, paths: FileHandler.shared.glob(outputDirectory, glob: "*")).toBlocking().last()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import TuistAutomation
|
||||
import TuistCache
|
||||
|
||||
/// A factory that returns cache controllers for different type of pre-built artifacts.
|
||||
final class CacheControllerFactory {
|
||||
/// Cache instance
|
||||
let cache: CacheStoring
|
||||
|
||||
/// Default constructor.
|
||||
/// - Parameter cache: Cache instance.
|
||||
init(cache: CacheStoring) {
|
||||
self.cache = cache
|
||||
}
|
||||
|
||||
/// Returns a cache controller that uses frameworks built for the simulator architecture.
|
||||
/// - Returns: A cache controller instance.
|
||||
func makeForSimulatorFramework() -> CacheControlling {
|
||||
let frameworkBuilder = CacheFrameworkBuilder(xcodeBuildController: XcodeBuildController())
|
||||
return CacheController(cache: cache, artifactBuilder: frameworkBuilder)
|
||||
}
|
||||
|
||||
/// Returns a cache controller that uses xcframeworks built for the simulator and device architectures.
|
||||
/// - Returns: A cache controller instance.
|
||||
func makeForXCFramework() -> CacheControlling {
|
||||
let frameworkBuilder = CacheXCFrameworkBuilder(xcodeBuildController: XcodeBuildController())
|
||||
return CacheController(cache: cache, artifactBuilder: frameworkBuilder)
|
||||
}
|
||||
}
|
|
@ -16,7 +16,13 @@ struct CachePrintHashesCommand: ParsableCommand {
|
|||
)
|
||||
var path: String?
|
||||
|
||||
@Flag(
|
||||
name: [.customShort("x"), .long],
|
||||
help: "When passed it caches the targets for simulator and device in a .xcframework"
|
||||
)
|
||||
var xcframeworks: Bool = false
|
||||
|
||||
func run() throws {
|
||||
try CachePrintHashesService().run(path: path.map { AbsolutePath($0) } ?? FileHandler.shared.currentPath)
|
||||
try CachePrintHashesService().run(path: path.map { AbsolutePath($0) } ?? FileHandler.shared.currentPath, xcframeworks: xcframeworks)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import Foundation
|
|||
import TSCBasic
|
||||
import TuistSupport
|
||||
|
||||
/// Command to cache frameworks as .xcframeworks and speed up your and others' build times.
|
||||
/// Command to cache targets as `.(xc)framework`s and speed up your and your peers' build times.
|
||||
struct CacheWarmCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "warm",
|
||||
|
@ -12,18 +12,18 @@ struct CacheWarmCommand: ParsableCommand {
|
|||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path to the directory that contains the project whose targets will be cached",
|
||||
help: "The path to the directory that contains the project whose targets will be cached.",
|
||||
completion: .directory
|
||||
)
|
||||
var path: String?
|
||||
|
||||
@Flag(
|
||||
name: [.customShort("d"), .long],
|
||||
help: "When passed it caches the targets also for device (only targets built for simulator are cached by default)"
|
||||
name: [.customShort("x"), .long],
|
||||
help: "When passed it caches the targets for simulator and device using xcframeworks."
|
||||
)
|
||||
var includeDeviceArch: Bool = false
|
||||
var xcframeworks: Bool = false
|
||||
|
||||
func run() throws {
|
||||
try CacheWarmService().run(path: path, includeDeviceArch: includeDeviceArch)
|
||||
try CacheWarmService().run(path: path, xcframeworks: xcframeworks)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,12 +50,19 @@ struct FocusCommand: ParsableCommand {
|
|||
)
|
||||
var noOpen: Bool = false
|
||||
|
||||
@Flag(
|
||||
name: [.customShort("x"), .long],
|
||||
help: "When passed it uses xcframeworks (simulator and device) from the cache instead of frameworks (only simulator)."
|
||||
)
|
||||
var xcframeworks: Bool = false
|
||||
|
||||
func run() throws {
|
||||
if sources.isEmpty {
|
||||
throw FocusCommandError.noSources
|
||||
}
|
||||
try FocusService().run(path: path,
|
||||
sources: Set(sources),
|
||||
noOpen: noOpen)
|
||||
noOpen: noOpen,
|
||||
xcframeworks: xcframeworks)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,11 @@ protocol GraphMapperProviding {
|
|||
}
|
||||
|
||||
final class GraphMapperProvider: GraphMapperProviding {
|
||||
fileprivate let cache: Bool
|
||||
fileprivate let sources: Set<String>
|
||||
fileprivate let cacheConfig: CacheConfig
|
||||
|
||||
init(cache: Bool = false, sources: Set<String> = Set()) {
|
||||
self.cache = cache
|
||||
init(cacheConfig: CacheConfig = CacheConfig.withoutCaching(), sources: Set<String> = Set()) {
|
||||
self.cacheConfig = cacheConfig
|
||||
self.sources = sources
|
||||
}
|
||||
|
||||
|
@ -29,10 +29,11 @@ final class GraphMapperProvider: GraphMapperProviding {
|
|||
var mappers: [GraphMapping] = []
|
||||
|
||||
// Cache
|
||||
if cache {
|
||||
if cacheConfig.cache {
|
||||
let cacheMapper = CacheMapper(config: config,
|
||||
cacheStorageProvider: CacheStorageProvider(config: config),
|
||||
sources: sources)
|
||||
sources: sources,
|
||||
cacheOutputType: cacheConfig.cacheOutputType)
|
||||
mappers.append(cacheMapper)
|
||||
}
|
||||
|
||||
|
|
|
@ -21,11 +21,12 @@ final class CachePrintHashesService {
|
|||
self.clock = clock
|
||||
}
|
||||
|
||||
func run(path: AbsolutePath) throws {
|
||||
func run(path: AbsolutePath, xcframeworks: Bool) throws {
|
||||
let timer = clock.startTimer()
|
||||
|
||||
let graph = try projectGenerator.load(path: path)
|
||||
let hashes = try graphContentHasher.contentHashes(for: graph)
|
||||
let cacheOutputType: CacheOutputType = xcframeworks ? .xcframework : .framework
|
||||
let hashes = try graphContentHasher.contentHashes(for: graph, cacheOutputType: cacheOutputType)
|
||||
let duration = timer.stop()
|
||||
let time = String(format: "%.3f", duration)
|
||||
guard hashes.count > 0 else {
|
||||
|
|
|
@ -16,12 +16,15 @@ final class CacheWarmService {
|
|||
manifestLinter: manifestLinter)
|
||||
}
|
||||
|
||||
func run(path: String?, includeDeviceArch: Bool) throws {
|
||||
func run(path: String?, xcframeworks: Bool) throws {
|
||||
let path = self.path(path)
|
||||
let config = try generatorModelLoader.loadConfig(at: currentPath)
|
||||
let cache = Cache(storageProvider: CacheStorageProvider(config: config))
|
||||
let cacheController = CacheController(cache: cache)
|
||||
try cacheController.cache(path: path, includeDeviceArch: includeDeviceArch)
|
||||
let cacheControllerFactory = CacheControllerFactory(cache: cache)
|
||||
|
||||
let cacheController = xcframeworks ? cacheControllerFactory.makeForXCFramework()
|
||||
: cacheControllerFactory.makeForSimulatorFramework()
|
||||
try cacheController.cache(path: path)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
|
|
@ -9,12 +9,14 @@ import TuistLoader
|
|||
import TuistSupport
|
||||
|
||||
protocol FocusServiceProjectGeneratorFactorying {
|
||||
func generator(sources: Set<String>) -> ProjectGenerating
|
||||
func generator(sources: Set<String>, xcframeworks: Bool) -> ProjectGenerating
|
||||
}
|
||||
|
||||
final class FocusServiceProjectGeneratorFactory: FocusServiceProjectGeneratorFactorying {
|
||||
func generator(sources: Set<String>) -> ProjectGenerating {
|
||||
ProjectGenerator(graphMapperProvider: GraphMapperProvider(cache: true, sources: sources))
|
||||
func generator(sources: Set<String>, xcframeworks: Bool) -> ProjectGenerating {
|
||||
let cacheOutputType: CacheOutputType = xcframeworks ? .xcframework : .framework
|
||||
let cacheConfig = CacheConfig.withCaching(cacheOutputType: cacheOutputType)
|
||||
return ProjectGenerator(graphMapperProvider: GraphMapperProvider(cacheConfig: cacheConfig, sources: sources))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,12 +51,12 @@ final class FocusService {
|
|||
self.projectGeneratorFactory = projectGeneratorFactory
|
||||
}
|
||||
|
||||
func run(path: String?, sources: Set<String>, noOpen: Bool) throws {
|
||||
func run(path: String?, sources: Set<String>, noOpen: Bool, xcframeworks: Bool) throws {
|
||||
let path = self.path(path)
|
||||
if isWorkspace(path: path) {
|
||||
throw FocusServiceError.cacheWorkspaceNonSupported
|
||||
}
|
||||
let generator = projectGeneratorFactory.generator(sources: sources)
|
||||
let generator = projectGeneratorFactory.generator(sources: sources, xcframeworks: xcframeworks)
|
||||
let workspacePath = try generator.generate(path: path, projectOnly: false)
|
||||
if !noOpen {
|
||||
try opener.open(path: workspacePath)
|
||||
|
|
|
@ -9,7 +9,7 @@ protocol GenerateServiceProjectGeneratorFactorying {
|
|||
|
||||
final class GenerateServiceProjectGeneratorFactory: GenerateServiceProjectGeneratorFactorying {
|
||||
func generator() -> ProjectGenerating {
|
||||
ProjectGenerator(graphMapperProvider: GraphMapperProvider(cache: false, sources: Set()))
|
||||
ProjectGenerator(graphMapperProvider: GraphMapperProvider())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@ public class CachedManifestLoader: ManifestLoading {
|
|||
private let fileHandler: FileHandling
|
||||
private let environment: Environmenting
|
||||
private let tuistVersion: String
|
||||
private let decoder: JSONDecoder = JSONDecoder()
|
||||
private let encoder: JSONEncoder = JSONEncoder()
|
||||
private let decoder = JSONDecoder()
|
||||
private let encoder = JSONEncoder()
|
||||
private var helpersCache: [AbsolutePath: String?] = [:]
|
||||
|
||||
public convenience init(manifestLoader: ManifestLoading = ManifestLoader()) {
|
||||
|
|
|
@ -13,7 +13,7 @@ enum ResourceLocatingError: FatalError {
|
|||
var description: String {
|
||||
switch self {
|
||||
case let .notFound(name):
|
||||
return "Couldn't find \(name)"
|
||||
return "Couldn't find resource named \(name)"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -423,14 +423,19 @@ public final class System: Systeming {
|
|||
|
||||
public func observable(_ arguments: [String], verbose: Bool, environment: [String: String]) -> Observable<SystemEvent<Data>> {
|
||||
Observable.create { (observer) -> Disposable in
|
||||
let synchronizationQueue = DispatchQueue(label: "io.tuist.support.system")
|
||||
var errorData: [UInt8] = []
|
||||
let process = Process(arguments: arguments,
|
||||
environment: environment,
|
||||
outputRedirection: .stream(stdout: { bytes in
|
||||
observer.onNext(.standardOutput(Data(bytes)))
|
||||
synchronizationQueue.async {
|
||||
observer.onNext(.standardOutput(Data(bytes)))
|
||||
}
|
||||
}, stderr: { bytes in
|
||||
errorData.append(contentsOf: bytes)
|
||||
observer.onNext(.standardError(Data(bytes)))
|
||||
synchronizationQueue.async {
|
||||
errorData.append(contentsOf: bytes)
|
||||
observer.onNext(.standardError(Data(bytes)))
|
||||
}
|
||||
}),
|
||||
verbose: verbose,
|
||||
startNewProcessGroup: false)
|
||||
|
@ -443,9 +448,13 @@ public final class System: Systeming {
|
|||
output: result.output,
|
||||
stderrOutput: result.stderrOutput.map { _ in errorData })
|
||||
try result.throwIfErrored()
|
||||
observer.onCompleted()
|
||||
synchronizationQueue.sync {
|
||||
observer.onCompleted()
|
||||
}
|
||||
} catch {
|
||||
observer.onError(error)
|
||||
synchronizationQueue.sync {
|
||||
observer.onError(error)
|
||||
}
|
||||
}
|
||||
return Disposables.create {
|
||||
if process.launched {
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
|
||||
public protocol DeveloperEnvironmenting {
|
||||
/// Returns the derived data directory selected in the environment.
|
||||
var derivedDataDirectory: AbsolutePath { get }
|
||||
}
|
||||
|
||||
public final class DeveloperEnvironment: DeveloperEnvironmenting {
|
||||
/// Shared instance to be used publicly.
|
||||
/// Since the environment doesn't change during the execution of Tuist, we can cache
|
||||
/// state internally to speed up future access to environment attributes.
|
||||
public static let shared: DeveloperEnvironmenting = DeveloperEnvironment()
|
||||
|
||||
/// File handler instance.
|
||||
let fileHandler: FileHandling
|
||||
|
||||
private init(fileHandler: FileHandling = FileHandler()) {
|
||||
self.fileHandler = fileHandler
|
||||
}
|
||||
|
||||
/// https://pewpewthespells.com/blog/xcode_build_locations.html/// https://pewpewthespells.com/blog/xcode_build_locations.html
|
||||
// swiftlint:disable identifier_name
|
||||
private var _derivedDataDirectory: AbsolutePath?
|
||||
public var derivedDataDirectory: AbsolutePath {
|
||||
if let _derivedDataDirectory = _derivedDataDirectory {
|
||||
return _derivedDataDirectory
|
||||
}
|
||||
let location: AbsolutePath
|
||||
if let customLocation = try? System.shared.capture("/usr/bin/defaults", "read", "com.apple.dt.Xcode IDECustomDerivedDataLocation") {
|
||||
location = AbsolutePath(customLocation.chomp())
|
||||
} else {
|
||||
// Default location
|
||||
location = fileHandler.homeDirectory.appending(RelativePath("Library/Developer/Xcode/DerivedData/"))
|
||||
}
|
||||
_derivedDataDirectory = location
|
||||
return location
|
||||
}
|
||||
|
||||
// swiftlint:enable identifier_name
|
||||
}
|
|
@ -20,8 +20,8 @@ public protocol Environmenting: AnyObject {
|
|||
/// Returns the directory where the project description helper modules are cached.
|
||||
var projectDescriptionHelpersCacheDirectory: AbsolutePath { get }
|
||||
|
||||
/// Returns the directory where the xcframeworks are cached.
|
||||
var xcframeworksCacheDirectory: AbsolutePath { get }
|
||||
/// Returns the directory where the build artifacts are cached.
|
||||
var buildCacheDirectory: AbsolutePath { get }
|
||||
|
||||
/// Returns all the environment variables that are specific to Tuist (prefixed with TUIST_)
|
||||
var tuistVariables: [String: String] { get }
|
||||
|
@ -38,7 +38,7 @@ public class Environment: Environmenting {
|
|||
public static var shared: Environmenting = Environment()
|
||||
|
||||
/// Returns the default local directory.
|
||||
static let defaultDirectory: AbsolutePath = AbsolutePath(URL(fileURLWithPath: NSHomeDirectory()).path).appending(component: ".tuist")
|
||||
static let defaultDirectory = AbsolutePath(URL(fileURLWithPath: NSHomeDirectory()).path).appending(component: ".tuist")
|
||||
|
||||
// MARK: - Attributes
|
||||
|
||||
|
@ -105,9 +105,9 @@ public class Environment: Environmenting {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the directory where the xcframeworks are cached.
|
||||
public var xcframeworksCacheDirectory: AbsolutePath {
|
||||
cacheDirectory.appending(component: "xcframeworks")
|
||||
/// Returns the directory where the build artifacts are cached.
|
||||
public var buildCacheDirectory: AbsolutePath {
|
||||
cacheDirectory.appending(component: "BuildCache")
|
||||
}
|
||||
|
||||
/// Returns the directory where the project description helper modules are cached.
|
||||
|
|
|
@ -2,34 +2,37 @@ import Foundation
|
|||
import TSCBasic
|
||||
import Zip
|
||||
|
||||
/// An interface to archive files in a zip file.
|
||||
public protocol FileArchiving {
|
||||
func zip() throws -> AbsolutePath
|
||||
func unzip(to: AbsolutePath) throws
|
||||
/// Zips files and outputs them in a zip file with the given name.
|
||||
/// - Parameter name: Name of the output zip file.
|
||||
func zip(name: String) throws -> AbsolutePath
|
||||
|
||||
/// Call this method to delete the temporary directory where the .zip file has been generated.
|
||||
func delete() throws
|
||||
}
|
||||
|
||||
public class FileArchiver: FileArchiving {
|
||||
private let path: AbsolutePath
|
||||
private var temporaryArtefact: AbsolutePath!
|
||||
/// Paths to be archived.
|
||||
private let paths: [AbsolutePath]
|
||||
|
||||
init(path: AbsolutePath) {
|
||||
self.path = path
|
||||
/// Temporary directory in which the .zip file will be generated.
|
||||
private var temporaryDirectory: AbsolutePath
|
||||
|
||||
/// Initializes the archiver with a list of files to archive.
|
||||
/// - Parameter paths: Paths to archive
|
||||
public init(paths: [AbsolutePath]) throws {
|
||||
self.paths = paths
|
||||
temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: false).path
|
||||
}
|
||||
|
||||
public func zip() throws -> AbsolutePath {
|
||||
let temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: false)
|
||||
temporaryArtefact = temporaryDirectory.path
|
||||
let destinationZipPath = temporaryDirectory.path.appending(component: "\(path.basenameWithoutExt).zip")
|
||||
try Zip.zipFiles(paths: [path.url], zipFilePath: destinationZipPath.url, password: nil, progress: nil)
|
||||
public func zip(name: String) throws -> AbsolutePath {
|
||||
let destinationZipPath = temporaryDirectory.appending(component: "\(name).zip")
|
||||
try Zip.zipFiles(paths: paths.map(\.url), zipFilePath: destinationZipPath.url, password: nil, progress: nil)
|
||||
return destinationZipPath
|
||||
}
|
||||
|
||||
public func unzip(to: AbsolutePath) throws {
|
||||
temporaryArtefact = path
|
||||
try Zip.unzipFile(path.url, destination: to.url, overwrite: true, password: nil)
|
||||
}
|
||||
|
||||
public func delete() throws {
|
||||
try FileHandler.shared.delete(temporaryArtefact)
|
||||
try FileHandler.shared.delete(temporaryDirectory)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import TSCBasic
|
||||
|
||||
public protocol FileArchiverManufacturing {
|
||||
func makeFileArchiver(for path: AbsolutePath) -> FileArchiving
|
||||
}
|
||||
|
||||
public class FileArchiverFactory: FileArchiverManufacturing {
|
||||
public init() {}
|
||||
|
||||
public func makeFileArchiver(for path: AbsolutePath) -> FileArchiving {
|
||||
FileArchiver(path: path)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import TSCBasic
|
||||
|
||||
/// An interface that defines a factory of archiver and unarchivers.
|
||||
public protocol FileArchivingFactorying {
|
||||
/// Returns an archiver to archive the given paths.
|
||||
/// - Parameter paths: Files to archive.
|
||||
func makeFileArchiver(for paths: [AbsolutePath]) throws -> FileArchiving
|
||||
|
||||
/// Returns an unarchiver to unarchive the given zip file.
|
||||
/// - Parameter path: Path to the .zip file.
|
||||
func makeFileUnarchiver(for path: AbsolutePath) throws -> FileUnarchiving
|
||||
}
|
||||
|
||||
public class FileArchivingFactory: FileArchivingFactorying {
|
||||
public init() {}
|
||||
|
||||
public func makeFileArchiver(for paths: [AbsolutePath]) throws -> FileArchiving {
|
||||
try FileArchiver(paths: paths)
|
||||
}
|
||||
|
||||
public func makeFileUnarchiver(for path: AbsolutePath) throws -> FileUnarchiving {
|
||||
try FileUnarchiver(path: path)
|
||||
}
|
||||
}
|
|
@ -45,86 +45,21 @@ public protocol FileHandling: AnyObject {
|
|||
/// Returns `AbsolutePath` to home directory
|
||||
var homeDirectory: AbsolutePath { get }
|
||||
|
||||
/// Replaces a file/directory in a given path with another one.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - to: The file/directory to be replaced.
|
||||
/// - with: The replacement file or directory.
|
||||
func replace(_ to: AbsolutePath, with: AbsolutePath) throws
|
||||
|
||||
/// Returns true if there's a folder or file at the given path.
|
||||
///
|
||||
/// - Parameter path: Path to check.
|
||||
/// - Returns: True if there's a folder or file at the given path.
|
||||
func exists(_ path: AbsolutePath) -> Bool
|
||||
|
||||
/// Move a file from a location to another location
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - from: File/Folder to be moved.
|
||||
/// - to: Path where the file/folder will be moved.
|
||||
/// - Throws: An error if from doesn't exist or to does.
|
||||
func move(from: AbsolutePath, to: AbsolutePath) throws
|
||||
|
||||
/// It copies a file or folder to another path.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - from: File/Folder to be copied.
|
||||
/// - to: Path where the file/folder will be copied.
|
||||
/// - Throws: An error if from doesn't exist or to does.
|
||||
func copy(from: AbsolutePath, to: AbsolutePath) throws
|
||||
|
||||
/// Reads a file at the given path and returns its data.
|
||||
///
|
||||
/// - Parameter at: Path to the text file.
|
||||
/// - Returns: The content of the file.
|
||||
/// - Throws: An error if the file doesn't exist
|
||||
func readFile(_ at: AbsolutePath) throws -> Data
|
||||
|
||||
/// Reads a text file at the given path and returns it.
|
||||
///
|
||||
/// - Parameter at: Path to the text file.
|
||||
/// - Returns: The content of the text file.
|
||||
/// - Throws: An error if the file doesn't exist or it's not a valid text file.
|
||||
func readTextFile(_ at: AbsolutePath) throws -> String
|
||||
|
||||
/// Reads a plist file at the given path and return decoded data
|
||||
///
|
||||
/// - Parameter at: Path to the plist file.
|
||||
/// - Returns: The content of the plist file in data format
|
||||
/// - Throws: An error if the file doesn't exist or it's not a valid plist file.
|
||||
func readPlistFile<T: Decodable>(_ at: AbsolutePath) throws -> T
|
||||
|
||||
/// Runs the given closure passing a temporary directory to it. When the closure
|
||||
/// finishes its execution, the temporary directory gets destroyed.
|
||||
///
|
||||
/// - Parameter closure: Closure to be executed with the temporary directory.
|
||||
/// - Throws: An error if the temporary directory cannot be created or the closure throws.
|
||||
func temporaryDirectory() throws -> AbsolutePath
|
||||
func inTemporaryDirectory(_ closure: (AbsolutePath) throws -> Void) throws
|
||||
|
||||
/// Writes a string into the given path (using the utf8 encoding)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - content: Content to be written.
|
||||
/// - path: Path where the content will be written into.
|
||||
/// - atomically: Whether the content should be written atomically.
|
||||
/// - Throws: An error if the writing fails.
|
||||
func inTemporaryDirectory(removeOnCompletion: Bool, _ closure: (AbsolutePath) throws -> Void) throws
|
||||
func inTemporaryDirectory<Result>(_ closure: (AbsolutePath) throws -> Result) throws -> Result
|
||||
func inTemporaryDirectory<Result>(removeOnCompletion: Bool, _ closure: (AbsolutePath) throws -> Result) throws -> Result
|
||||
func write(_ content: String, path: AbsolutePath, atomically: Bool) throws
|
||||
|
||||
/// Traverses the parent directories until the given path is found.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - from: A path to a directory from which search the Config.swift.
|
||||
/// - Returns: The found path.
|
||||
func locateDirectoryTraversingParents(from: AbsolutePath, path: String) -> AbsolutePath?
|
||||
|
||||
/// It traverses up the directories hierarchy appending the given path and returning the
|
||||
/// resulting path if it exists.
|
||||
/// - Parameters:
|
||||
/// - path: Relative path to append to each path in the hierarchy.
|
||||
/// - from: Path to traverse the hierarchy from.
|
||||
func locateDirectory(_ path: String, traversingFrom from: AbsolutePath) -> AbsolutePath?
|
||||
|
||||
func glob(_ path: AbsolutePath, glob: String) -> [AbsolutePath]
|
||||
func throwingGlob(_ path: AbsolutePath, glob: String) throws -> [AbsolutePath]
|
||||
func linkFile(atPath: AbsolutePath, toPath: AbsolutePath) throws
|
||||
|
@ -133,31 +68,9 @@ public protocol FileHandling: AnyObject {
|
|||
func isFolder(_ path: AbsolutePath) -> Bool
|
||||
func touch(_ path: AbsolutePath) throws
|
||||
func contentsOfDirectory(_ path: AbsolutePath) throws -> [AbsolutePath]
|
||||
|
||||
/// Gives a md5 representation of the given file
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: File to be assessed.
|
||||
/// - Returns: The file’s md5 as an utf8 encoded String.
|
||||
/// - Throws: An error if path's file data content can't be accessed.
|
||||
func md5(path: AbsolutePath) throws -> String
|
||||
|
||||
/// Gives a base 64 md5 representation of the given file
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: File to be assessed.
|
||||
/// - Returns: The file’s md5 as an utf8 base 64 encoded String.
|
||||
/// - Throws: An error if path's file data content can't be accessed.
|
||||
func base64MD5(path: AbsolutePath) throws -> String
|
||||
|
||||
/// Gives the size of the given file, in bytes
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: File to be assessed.
|
||||
/// - Returns: The file’s size in bytes.
|
||||
/// - Throws: An error if path's file size can't be retrieved.
|
||||
func fileSize(path: AbsolutePath) throws -> UInt64
|
||||
|
||||
func changeExtension(path: AbsolutePath, to newExtension: String) throws -> AbsolutePath
|
||||
}
|
||||
|
||||
|
@ -202,8 +115,25 @@ public class FileHandler: FileHandling {
|
|||
_ = try fileManager.replaceItemAt(to.url, withItemAt: tempUrl)
|
||||
}
|
||||
|
||||
public func temporaryDirectory() throws -> AbsolutePath {
|
||||
let directory = try TemporaryDirectory(removeTreeOnDeinit: false)
|
||||
return directory.path
|
||||
}
|
||||
|
||||
public func inTemporaryDirectory<Result>(_ closure: (AbsolutePath) throws -> Result) throws -> Result {
|
||||
try withTemporaryDirectory(removeTreeOnDeinit: true, closure)
|
||||
}
|
||||
|
||||
public func inTemporaryDirectory(removeOnCompletion: Bool, _ closure: (AbsolutePath) throws -> Void) throws {
|
||||
try withTemporaryDirectory(removeTreeOnDeinit: removeOnCompletion, closure)
|
||||
}
|
||||
|
||||
public func inTemporaryDirectory(_ closure: (AbsolutePath) throws -> Void) throws {
|
||||
try withTemporaryDirectory(closure)
|
||||
try withTemporaryDirectory(removeTreeOnDeinit: true, closure)
|
||||
}
|
||||
|
||||
public func inTemporaryDirectory<Result>(removeOnCompletion: Bool, _ closure: (AbsolutePath) throws -> Result) throws -> Result {
|
||||
try withTemporaryDirectory(removeTreeOnDeinit: removeOnCompletion, closure)
|
||||
}
|
||||
|
||||
public func exists(_ path: AbsolutePath) -> Bool {
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
import Zip
|
||||
|
||||
/// An interface to unarchive files from a zip file.
|
||||
public protocol FileUnarchiving {
|
||||
/// Unarchives the files into a temporary directory and returns the path to that directory.
|
||||
func unzip() throws -> AbsolutePath
|
||||
|
||||
/// Call this method to delete the temporary directory where the .zip file has been generated.
|
||||
func delete() throws
|
||||
}
|
||||
|
||||
public class FileUnarchiver: FileUnarchiving {
|
||||
/// Path to the .zip file to unarchive
|
||||
private let path: AbsolutePath
|
||||
|
||||
/// Temporary directory in which the .zip file will be generated.
|
||||
private var temporaryDirectory: AbsolutePath
|
||||
|
||||
/// Initializes the unarchiver with the path to the file to unarchive.
|
||||
/// - Parameter path: Path to the .zip file to unarchive.
|
||||
public init(path: AbsolutePath) throws {
|
||||
self.path = path
|
||||
temporaryDirectory = try TemporaryDirectory(removeTreeOnDeinit: false).path
|
||||
}
|
||||
|
||||
public func unzip() throws -> AbsolutePath {
|
||||
try Zip.unzipFile(path.url, destination: temporaryDirectory.url, overwrite: true, password: nil)
|
||||
return temporaryDirectory
|
||||
}
|
||||
|
||||
public func delete() throws {
|
||||
try FileHandler.shared.delete(temporaryDirectory)
|
||||
}
|
||||
}
|
|
@ -49,8 +49,6 @@ public final class TemporaryDirectory {
|
|||
deinit {
|
||||
if shouldRemoveTreeOnDeinit {
|
||||
_ = try? FileManager.default.removeItem(atPath: path.pathString)
|
||||
} else {
|
||||
rmdir(path.pathString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,15 +23,6 @@ public final class MockFileHandler: FileHandler {
|
|||
// swiftlint:disable:next force_try
|
||||
override public var currentPath: AbsolutePath { try! temporaryDirectory() }
|
||||
|
||||
public var stubInTemporaryDirectory: AbsolutePath?
|
||||
override public func inTemporaryDirectory(_ closure: (AbsolutePath) throws -> Void) throws {
|
||||
guard let stubInTemporaryDirectory = stubInTemporaryDirectory else {
|
||||
try super.inTemporaryDirectory(closure)
|
||||
return
|
||||
}
|
||||
try closure(stubInTemporaryDirectory)
|
||||
}
|
||||
|
||||
public var stubExists: ((AbsolutePath) -> Bool)?
|
||||
override public func exists(_ path: AbsolutePath) -> Bool {
|
||||
guard let stubExists = stubExists else {
|
||||
|
@ -39,6 +30,22 @@ public final class MockFileHandler: FileHandler {
|
|||
}
|
||||
return stubExists(path)
|
||||
}
|
||||
|
||||
override public func inTemporaryDirectory<Result>(removeOnCompletion _: Bool, _ closure: (AbsolutePath) throws -> Result) throws -> Result {
|
||||
try closure(temporaryDirectory())
|
||||
}
|
||||
|
||||
override public func inTemporaryDirectory(_ closure: (AbsolutePath) throws -> Void) throws {
|
||||
try closure(temporaryDirectory())
|
||||
}
|
||||
|
||||
override public func inTemporaryDirectory(removeOnCompletion _: Bool, _ closure: (AbsolutePath) throws -> Void) throws {
|
||||
try closure(temporaryDirectory())
|
||||
}
|
||||
|
||||
override public func inTemporaryDirectory<Result>(_ closure: (AbsolutePath) throws -> Result) throws -> Result {
|
||||
try closure(temporaryDirectory())
|
||||
}
|
||||
}
|
||||
|
||||
public class TuistTestCase: XCTestCase {
|
||||
|
@ -97,7 +104,7 @@ public class TuistTestCase: XCTestCase {
|
|||
@discardableResult
|
||||
public func createFolders(_ folders: [String]) throws -> [AbsolutePath] {
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let fileHandler = FileHandler()
|
||||
let fileHandler = FileHandler.shared
|
||||
let paths = folders.map { temporaryPath.appending(RelativePath($0)) }
|
||||
try paths.forEach {
|
||||
try fileHandler.createFolder($0)
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
@testable import TuistSupport
|
||||
|
||||
public final class MockDeveloperEnvironment: DeveloperEnvironmenting {
|
||||
public init() {}
|
||||
|
||||
public var invokedDerivedDataDirectoryGetter = false
|
||||
public var invokedDerivedDataDirectoryGetterCount = 0
|
||||
public var stubbedDerivedDataDirectory: AbsolutePath!
|
||||
|
||||
public var derivedDataDirectory: AbsolutePath {
|
||||
invokedDerivedDataDirectoryGetter = true
|
||||
invokedDerivedDataDirectoryGetterCount += 1
|
||||
return stubbedDerivedDataDirectory
|
||||
}
|
||||
}
|
|
@ -4,9 +4,9 @@ import TuistSupport
|
|||
import XCTest
|
||||
|
||||
public class MockEnvironment: Environmenting {
|
||||
let directory: TemporaryDirectory
|
||||
var setupCallCount: UInt = 0
|
||||
var setupErrorStub: Error?
|
||||
fileprivate let directory: TemporaryDirectory
|
||||
fileprivate var setupCallCount: UInt = 0
|
||||
fileprivate var setupErrorStub: Error?
|
||||
|
||||
init() throws {
|
||||
directory = try TemporaryDirectory(removeTreeOnDeinit: true)
|
||||
|
@ -39,8 +39,8 @@ public class MockEnvironment: Environmenting {
|
|||
cacheDirectory.appending(component: "ProjectDescriptionHelpers")
|
||||
}
|
||||
|
||||
public var xcframeworksCacheDirectory: AbsolutePath {
|
||||
cacheDirectory.appending(component: "xcframeworks")
|
||||
public var buildCacheDirectory: AbsolutePath {
|
||||
cacheDirectory.appending(component: "BuildCache")
|
||||
}
|
||||
|
||||
func path(version: String) -> AbsolutePath {
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
import TuistSupport
|
||||
|
||||
public class MockFileArchiverFactory: FileArchiverManufacturing {
|
||||
public init() {}
|
||||
|
||||
public var invokedMakeFileArchiver = false
|
||||
public var invokedMakeFileArchiverCount = 0
|
||||
public var invokedMakeFileArchiverParameters: (path: AbsolutePath, Void)?
|
||||
public var invokedMakeFileArchiverParametersList = [(path: AbsolutePath, Void)]()
|
||||
public var stubbedMakeFileArchiverResult: FileArchiving = MockFileArchiver()
|
||||
|
||||
public func makeFileArchiver(for path: AbsolutePath) -> FileArchiving {
|
||||
invokedMakeFileArchiver = true
|
||||
invokedMakeFileArchiverCount += 1
|
||||
invokedMakeFileArchiverParameters = (path, ())
|
||||
invokedMakeFileArchiverParametersList.append((path, ()))
|
||||
return stubbedMakeFileArchiverResult
|
||||
}
|
||||
|
||||
public var invokedMakeFileArchiverFor = false
|
||||
public var invokedMakeFileArchiverForCount = 0
|
||||
public var invokedMakeFileArchiverForParameters: (path: AbsolutePath, fileHandler: FileHandling)?
|
||||
public var invokedMakeFileArchiverForParametersList = [(path: AbsolutePath, fileHandler: FileHandling)]()
|
||||
public var stubbedMakeFileArchiverForResult: FileArchiving = MockFileArchiver()
|
||||
|
||||
public func makeFileArchiver(for path: AbsolutePath, fileHandler: FileHandling) -> FileArchiving {
|
||||
invokedMakeFileArchiverFor = true
|
||||
invokedMakeFileArchiverForCount += 1
|
||||
invokedMakeFileArchiverForParameters = (path, fileHandler)
|
||||
invokedMakeFileArchiverForParametersList.append((path, fileHandler))
|
||||
return stubbedMakeFileArchiverForResult
|
||||
}
|
||||
}
|
||||
|
||||
public class MockFileArchiver: FileArchiving {
|
||||
public var invokedZip = false
|
||||
public var invokedZipCount = 0
|
||||
public var stubbedZipError: Error?
|
||||
public var stubbedZipResult: AbsolutePath!
|
||||
|
||||
public func zip() throws -> AbsolutePath {
|
||||
invokedZip = true
|
||||
invokedZipCount += 1
|
||||
if let error = stubbedZipError {
|
||||
throw error
|
||||
}
|
||||
return stubbedZipResult
|
||||
}
|
||||
|
||||
public var invokedUnzip = false
|
||||
public var invokedUnzipCount = 0
|
||||
public var invokedUnzipParameters: (to: AbsolutePath, Void)?
|
||||
public var invokedUnzipParametersList = [(to: AbsolutePath, Void)]()
|
||||
public var stubbedUnzipError: Error?
|
||||
|
||||
public func unzip(to: AbsolutePath) throws {
|
||||
invokedUnzip = true
|
||||
invokedUnzipCount += 1
|
||||
invokedUnzipParameters = (to, ())
|
||||
invokedUnzipParametersList.append((to, ()))
|
||||
if let error = stubbedUnzipError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public var invokedDelete = false
|
||||
public var invokedDeleteCount = 0
|
||||
public var stubbedDeleteError: Error?
|
||||
|
||||
public func delete() throws {
|
||||
invokedDelete = true
|
||||
invokedDeleteCount += 1
|
||||
if let error = stubbedDeleteError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
import TuistSupport
|
||||
|
||||
public class MockFileArchivingFactory: FileArchivingFactorying {
|
||||
public init() {}
|
||||
|
||||
public var invokedMakeFileArchiver = false
|
||||
public var invokedMakeFileArchiverCount = 0
|
||||
public var invokedMakeFileArchiverParameters: (paths: [AbsolutePath], Void)?
|
||||
public var invokedMakeFileArchiverParametersList = [(paths: [AbsolutePath], Void)]()
|
||||
public var stubbedMakeFileArchiverError: Error?
|
||||
public var stubbedMakeFileArchiverResult: FileArchiving!
|
||||
|
||||
public func makeFileArchiver(for paths: [AbsolutePath]) throws -> FileArchiving {
|
||||
invokedMakeFileArchiver = true
|
||||
invokedMakeFileArchiverCount += 1
|
||||
invokedMakeFileArchiverParameters = (paths, ())
|
||||
invokedMakeFileArchiverParametersList.append((paths, ()))
|
||||
if let error = stubbedMakeFileArchiverError {
|
||||
throw error
|
||||
}
|
||||
return stubbedMakeFileArchiverResult
|
||||
}
|
||||
|
||||
public var invokedMakeFileUnarchiver = false
|
||||
public var invokedMakeFileUnarchiverCount = 0
|
||||
public var invokedMakeFileUnarchiverParameters: (path: AbsolutePath, Void)?
|
||||
public var invokedMakeFileUnarchiverParametersList = [(path: AbsolutePath, Void)]()
|
||||
public var stubbedMakeFileUnarchiverError: Error?
|
||||
public var stubbedMakeFileUnarchiverResult: FileUnarchiving!
|
||||
|
||||
public func makeFileUnarchiver(for path: AbsolutePath) throws -> FileUnarchiving {
|
||||
invokedMakeFileUnarchiver = true
|
||||
invokedMakeFileUnarchiverCount += 1
|
||||
invokedMakeFileUnarchiverParameters = (path, ())
|
||||
invokedMakeFileUnarchiverParametersList.append((path, ()))
|
||||
if let error = stubbedMakeFileUnarchiverError {
|
||||
throw error
|
||||
}
|
||||
return stubbedMakeFileUnarchiverResult
|
||||
}
|
||||
}
|
||||
|
||||
public class MockFileArchiver: FileArchiving {
|
||||
public init() {}
|
||||
|
||||
public var invokedZip = false
|
||||
public var invokedZipCount = 0
|
||||
public var invokedZipParameters: (name: String, Void)?
|
||||
public var invokedZipParametersList = [(name: String, Void)]()
|
||||
public var stubbedZipError: Error?
|
||||
public var stubbedZipResult: AbsolutePath!
|
||||
|
||||
public func zip(name: String) throws -> AbsolutePath {
|
||||
invokedZip = true
|
||||
invokedZipCount += 1
|
||||
invokedZipParameters = (name, ())
|
||||
invokedZipParametersList.append((name, ()))
|
||||
if let error = stubbedZipError {
|
||||
throw error
|
||||
}
|
||||
return stubbedZipResult
|
||||
}
|
||||
|
||||
public var invokedDelete = false
|
||||
public var invokedDeleteCount = 0
|
||||
public var stubbedDeleteError: Error?
|
||||
|
||||
public func delete() throws {
|
||||
invokedDelete = true
|
||||
invokedDeleteCount += 1
|
||||
if let error = stubbedDeleteError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MockFileUnarchiver: FileUnarchiving {
|
||||
public init() {}
|
||||
|
||||
public var invokedUnzip = false
|
||||
public var invokedUnzipCount = 0
|
||||
public var stubbedUnzipError: Error?
|
||||
public var stubbedUnzipResult: AbsolutePath!
|
||||
|
||||
public func unzip() throws -> AbsolutePath {
|
||||
invokedUnzip = true
|
||||
invokedUnzipCount += 1
|
||||
if let error = stubbedUnzipError {
|
||||
throw error
|
||||
}
|
||||
return stubbedUnzipResult
|
||||
}
|
||||
|
||||
public var invokedDelete = false
|
||||
public var invokedDeleteCount = 0
|
||||
public var stubbedDeleteError: Error?
|
||||
|
||||
public func delete() throws {
|
||||
invokedDelete = true
|
||||
invokedDeleteCount += 1
|
||||
if let error = stubbedDeleteError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
7
Tests/Fixtures/Frameworks/Frameworks.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
Tests/Fixtures/Frameworks/Frameworks.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
|
@ -79,7 +79,7 @@ final class CacheLocalStorageIntegrationTests: TuistTestCase {
|
|||
try FileHandler.shared.createFolder(xcframeworkPath)
|
||||
|
||||
// When
|
||||
_ = try subject.store(hash: hash, xcframeworkPath: xcframeworkPath).toBlocking().first()
|
||||
_ = try subject.store(hash: hash, paths: [xcframeworkPath]).toBlocking().first()
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(FileHandler.shared.exists(cacheDirectory.appending(RelativePath("\(hash)/framework.xcframework"))))
|
||||
|
|
|
@ -72,7 +72,7 @@ final class ContentHashingIntegrationTests: TuistTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let contentHash = try subject.contentHashes(for: graph)
|
||||
let contentHash = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(contentHash[framework1], contentHash[framework2])
|
||||
|
@ -88,7 +88,7 @@ final class ContentHashingIntegrationTests: TuistTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let contentHash = try subject.contentHashes(for: graph)
|
||||
let contentHash = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
|
||||
// Then
|
||||
XCTAssertNotEqual(contentHash[framework1], contentHash[framework2])
|
||||
|
@ -104,11 +104,29 @@ final class ContentHashingIntegrationTests: TuistTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let contentHash = try subject.contentHashes(for: graph)
|
||||
let contentHash = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(contentHash[framework1], "959a33d298f7d1815d8f747e557240f7")
|
||||
XCTAssertEqual(contentHash[framework2], "95d3a5a751b713a854957b4b30d996eb")
|
||||
XCTAssertEqual(contentHash[framework1], "781fffb97990f6f88e6ba3d889d6bab6")
|
||||
XCTAssertEqual(contentHash[framework2], "1f83a8a2083cdecc1cce85c5ee0ad688")
|
||||
}
|
||||
|
||||
func test_contentHashes_hashChangesWithCacheOutputType() throws {
|
||||
// Given
|
||||
let temporaryDirectoryPath = try temporaryPath()
|
||||
let framework1 = makeFramework(named: "f1", sources: [source1, source2])
|
||||
let framework2 = makeFramework(named: "f2", sources: [source3, source4])
|
||||
let graph = Graph.test(targets: [
|
||||
temporaryDirectoryPath: [framework1, framework2],
|
||||
])
|
||||
|
||||
// When
|
||||
let contentFrameworkHash = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
let contentXCFrameworkHash = try subject.contentHashes(for: graph, cacheOutputType: .xcframework)
|
||||
|
||||
// Then
|
||||
XCTAssertNotEqual(contentFrameworkHash[framework1], contentXCFrameworkHash[framework1])
|
||||
XCTAssertNotEqual(contentFrameworkHash[framework2], contentXCFrameworkHash[framework2])
|
||||
}
|
||||
|
||||
// MARK: - Resources
|
||||
|
@ -123,7 +141,7 @@ final class ContentHashingIntegrationTests: TuistTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let contentHash = try subject.contentHashes(for: graph)
|
||||
let contentHash = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
|
||||
// Then
|
||||
XCTAssertNotEqual(contentHash[framework1], contentHash[framework2])
|
||||
|
@ -139,7 +157,7 @@ final class ContentHashingIntegrationTests: TuistTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let contentHash = try subject.contentHashes(for: graph)
|
||||
let contentHash = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
|
||||
// Then
|
||||
XCTAssertNotEqual(contentHash[framework1], contentHash[framework2])
|
||||
|
@ -156,7 +174,7 @@ final class ContentHashingIntegrationTests: TuistTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let contentHash = try subject.contentHashes(for: graph)
|
||||
let contentHash = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(contentHash[framework1], contentHash[framework2])
|
||||
|
@ -174,7 +192,7 @@ final class ContentHashingIntegrationTests: TuistTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let contentHash = try subject.contentHashes(for: graph)
|
||||
let contentHash = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
|
||||
// Then
|
||||
XCTAssertNotEqual(contentHash[framework1], contentHash[framework2])
|
||||
|
@ -190,7 +208,7 @@ final class ContentHashingIntegrationTests: TuistTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let contentHash = try subject.contentHashes(for: graph)
|
||||
let contentHash = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(contentHash[framework1], contentHash[framework2])
|
||||
|
@ -210,7 +228,7 @@ final class ContentHashingIntegrationTests: TuistTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let contentHash = try subject.contentHashes(for: graph)
|
||||
let contentHash = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
|
||||
XCTAssertNotEqual(contentHash[framework1], contentHash[framework2])
|
||||
}
|
||||
|
@ -227,7 +245,7 @@ final class ContentHashingIntegrationTests: TuistTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let contentHash = try subject.contentHashes(for: graph)
|
||||
let contentHash = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
|
||||
XCTAssertNotEqual(contentHash[framework1], contentHash[framework2])
|
||||
}
|
||||
|
|
|
@ -10,11 +10,15 @@ import XCTest
|
|||
// Alternative: https://dot-to-ascii.ggerganov.com/
|
||||
final class CacheGraphMapperTests: TuistUnitTestCase {
|
||||
var xcframeworkLoader: MockXCFrameworkNodeLoader!
|
||||
var frameworkLoader: MockFrameworkNodeLoader!
|
||||
|
||||
var subject: CacheGraphMutator!
|
||||
|
||||
override func setUp() {
|
||||
xcframeworkLoader = MockXCFrameworkNodeLoader()
|
||||
subject = CacheGraphMutator(xcframeworkLoader: xcframeworkLoader)
|
||||
frameworkLoader = MockFrameworkNodeLoader()
|
||||
subject = CacheGraphMutator(frameworkLoader: frameworkLoader,
|
||||
xcframeworkLoader: xcframeworkLoader)
|
||||
super.setUp()
|
||||
}
|
||||
|
||||
|
@ -25,11 +29,11 @@ final class CacheGraphMapperTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
// First scenario
|
||||
// +---->B (Cached Framework)+
|
||||
// +---->B (Cached XCFramework)+
|
||||
// | |
|
||||
// App| +------>D (Cached Framework)
|
||||
// App| +------>D (Cached XCFramework)
|
||||
// | |
|
||||
// +---->C (Cached Framework)+
|
||||
// +---->C (Cached XCFramework)+
|
||||
func test_map_when_first_scenario() throws {
|
||||
let path = try temporaryPath()
|
||||
|
||||
|
@ -76,8 +80,12 @@ final class CacheGraphMapperTests: TuistUnitTestCase {
|
|||
else { fatalError("Unexpected load call") }
|
||||
}
|
||||
|
||||
frameworkLoader.loadStub = { _ in
|
||||
throw "Can't find .framework here"
|
||||
}
|
||||
|
||||
// When
|
||||
let got = try subject.map(graph: graph, xcframeworks: xcframeworks, sources: Set(["App"]))
|
||||
let got = try subject.map(graph: graph, precompiledFrameworks: xcframeworks, sources: Set(["App"]))
|
||||
|
||||
// Then
|
||||
let appNode = try XCTUnwrap(got.entryNodes.first as? TargetNode)
|
||||
|
@ -98,11 +106,11 @@ final class CacheGraphMapperTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
// Second scenario
|
||||
// +---->B (Cached Framework)+
|
||||
// +---->B (Cached XCFramework)+
|
||||
// | |
|
||||
// App| +------>D Precompiled .framework
|
||||
// | |
|
||||
// +---->C (Cached Framework)+
|
||||
// +---->C (Cached XCFramework)+
|
||||
func test_map_when_second_scenario() throws {
|
||||
let path = try temporaryPath()
|
||||
|
||||
|
@ -144,8 +152,13 @@ final class CacheGraphMapperTests: TuistUnitTestCase {
|
|||
else { fatalError("Unexpected load call") }
|
||||
}
|
||||
|
||||
frameworkLoader.loadStub = { path in
|
||||
if path == dFrameworkPath { return dFramework }
|
||||
throw "Can't find .framework here"
|
||||
}
|
||||
|
||||
// When
|
||||
let got = try subject.map(graph: graph, xcframeworks: xcframeworks, sources: Set(["App"]))
|
||||
let got = try subject.map(graph: graph, precompiledFrameworks: xcframeworks, sources: Set(["App"]))
|
||||
|
||||
// Then
|
||||
let app = try XCTUnwrap(got.entryNodes.first as? TargetNode)
|
||||
|
@ -164,11 +177,11 @@ final class CacheGraphMapperTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
// Third scenario
|
||||
// +---->B (Cached Framework)+
|
||||
// +---->B (Cached XCFramework)+
|
||||
// | |
|
||||
// App| +------>D Precompiled .framework
|
||||
// | |
|
||||
// +---->C (Cached Framework)+------>E Precompiled .xcframework
|
||||
// +---->C (Cached XCFramework)+------>E Precompiled .xcframework
|
||||
func test_map_when_third_scenario() throws {
|
||||
let path = try temporaryPath()
|
||||
|
||||
|
@ -216,8 +229,13 @@ final class CacheGraphMapperTests: TuistUnitTestCase {
|
|||
else { fatalError("Unexpected load call") }
|
||||
}
|
||||
|
||||
frameworkLoader.loadStub = { path in
|
||||
if path == dFrameworkPath { return dFramework }
|
||||
throw "Can't find .framework here"
|
||||
}
|
||||
|
||||
// When
|
||||
let got = try subject.map(graph: graph, xcframeworks: xcframeworks, sources: Set(["App"]))
|
||||
let got = try subject.map(graph: graph, precompiledFrameworks: xcframeworks, sources: Set(["App"]))
|
||||
|
||||
// Then
|
||||
let app = try XCTUnwrap(got.entryNodes.first as? TargetNode)
|
||||
|
@ -241,7 +259,7 @@ final class CacheGraphMapperTests: TuistUnitTestCase {
|
|||
// |
|
||||
// App|
|
||||
// |
|
||||
// +---->C (Cached Framework)+------>E Precompiled .xcframework
|
||||
// +---->C (Cached XCFramework)+------>E Precompiled .xcframework
|
||||
func test_map_when_fourth_scenario() throws {
|
||||
let path = try temporaryPath()
|
||||
|
||||
|
@ -284,8 +302,13 @@ final class CacheGraphMapperTests: TuistUnitTestCase {
|
|||
else { fatalError("Unexpected load call") }
|
||||
}
|
||||
|
||||
frameworkLoader.loadStub = { path in
|
||||
if path == dFrameworkPath { return dFramework }
|
||||
throw "Can't find .framework here"
|
||||
}
|
||||
|
||||
// When
|
||||
let got = try subject.map(graph: graph, xcframeworks: xcframeworks, sources: Set(["App"]))
|
||||
let got = try subject.map(graph: graph, precompiledFrameworks: xcframeworks, sources: Set(["App"]))
|
||||
|
||||
// Then
|
||||
let app = try XCTUnwrap(got.entryNodes.first as? TargetNode)
|
||||
|
@ -303,7 +326,7 @@ final class CacheGraphMapperTests: TuistUnitTestCase {
|
|||
XCTAssertFalse(gotProjects.contains(cFrameworkNode.project))
|
||||
}
|
||||
|
||||
// Fith scenario
|
||||
// Fifth scenario
|
||||
//
|
||||
// App ---->B (Framework)+------>C (Framework that depends on XCTest)
|
||||
func test_map_when_fith_scenario() throws {
|
||||
|
@ -328,7 +351,7 @@ final class CacheGraphMapperTests: TuistUnitTestCase {
|
|||
let graph = Graph.test(entryNodes: [appTargetNode], projects: graphProjects(targetNodes), targets: graphTargets(targetNodes))
|
||||
|
||||
// When
|
||||
let got = try subject.map(graph: graph, xcframeworks: [:], sources: Set(["App"]))
|
||||
let got = try subject.map(graph: graph, precompiledFrameworks: [:], sources: Set(["App"]))
|
||||
|
||||
// Then
|
||||
let app = try XCTUnwrap(got.entryNodes.first as? TargetNode)
|
||||
|
@ -344,6 +367,306 @@ final class CacheGraphMapperTests: TuistUnitTestCase {
|
|||
XCTAssertTrue(gotProjects.contains(cFrameworkNode.project))
|
||||
}
|
||||
|
||||
/// Scenari with cached .framework
|
||||
|
||||
// Sixth scenario
|
||||
// +---->B (Cached Framework)+
|
||||
// | |
|
||||
// App| +------>D (Cached Framework)
|
||||
// | |
|
||||
// +---->C (Cached Framework)+
|
||||
func test_map_when_sixth_scenario() throws {
|
||||
let path = try temporaryPath()
|
||||
|
||||
// Given: D
|
||||
let dFramework = Target.test(name: "D", platform: .iOS, product: .framework)
|
||||
let dProject = Project.test(path: path.appending(component: "D"), name: "D", targets: [dFramework])
|
||||
let dFrameworkNode = TargetNode.test(project: dProject, target: dFramework)
|
||||
|
||||
// Given: B
|
||||
let bFramework = Target.test(name: "B", platform: .iOS, product: .framework)
|
||||
let bProject = Project.test(path: path.appending(component: "B"), name: "B", targets: [bFramework])
|
||||
let bFrameworkNode = TargetNode.test(project: bProject, target: bFramework, dependencies: [dFrameworkNode])
|
||||
|
||||
// Given: C
|
||||
let cFramework = Target.test(name: "C", platform: .iOS, product: .framework)
|
||||
let cProject = Project.test(path: path.appending(component: "C"), name: "C", targets: [cFramework])
|
||||
let cFrameworkNode = TargetNode.test(project: cProject, target: cFramework, dependencies: [dFrameworkNode])
|
||||
|
||||
// Given: App
|
||||
let app = Target.test(name: "App", platform: .iOS, product: .app)
|
||||
let appProject = Project.test(path: path.appending(component: "App"), name: "App", targets: [app])
|
||||
let appTargetNode = TargetNode.test(project: appProject, target: app, dependencies: [bFrameworkNode, cFrameworkNode])
|
||||
|
||||
let targetNodes = [bFrameworkNode, cFrameworkNode, dFrameworkNode, appTargetNode]
|
||||
let graph = Graph.test(entryNodes: [appTargetNode], projects: graphProjects(targetNodes), targets: graphTargets(targetNodes))
|
||||
|
||||
// Given xcframeworks
|
||||
let dCachedFrameworkPath = path.appending(component: "D.framework")
|
||||
let dCachedFramework = FrameworkNode.test(path: dCachedFrameworkPath)
|
||||
let bCachedFrameworkPath = path.appending(component: "B.framework")
|
||||
let bCachedFramework = FrameworkNode.test(path: bCachedFrameworkPath)
|
||||
let cCachedFrameworkPath = path.appending(component: "C.framework")
|
||||
let cCachedFramework = FrameworkNode.test(path: cCachedFrameworkPath)
|
||||
let frameworks = [
|
||||
dFrameworkNode: dCachedFrameworkPath,
|
||||
bFrameworkNode: bCachedFrameworkPath,
|
||||
cFrameworkNode: cCachedFrameworkPath,
|
||||
]
|
||||
|
||||
frameworkLoader.loadStub = { path in
|
||||
if path == dCachedFrameworkPath { return dCachedFramework }
|
||||
else if path == bCachedFrameworkPath { return bCachedFramework }
|
||||
else if path == cCachedFrameworkPath { return cCachedFramework }
|
||||
else { fatalError("Unexpected load call") }
|
||||
}
|
||||
|
||||
xcframeworkLoader.loadStub = { _ in
|
||||
throw "Can't find an .xcframework here"
|
||||
}
|
||||
|
||||
// When
|
||||
let got = try subject.map(graph: graph, precompiledFrameworks: frameworks, sources: Set(["App"]))
|
||||
|
||||
// Then
|
||||
let appNode = try XCTUnwrap(got.entryNodes.first as? TargetNode)
|
||||
let b = try XCTUnwrap(appNode.dependencies.compactMap { $0 as? FrameworkNode }.first(where: { $0.path == bCachedFrameworkPath }))
|
||||
let c = try XCTUnwrap(appNode.dependencies.compactMap { $0 as? FrameworkNode }.first(where: { $0.path == cCachedFrameworkPath }))
|
||||
XCTAssertTrue(b.dependencies.contains(where: { $0.path == dCachedFrameworkPath }))
|
||||
XCTAssertTrue(c.dependencies.contains(where: { $0.path == dCachedFrameworkPath }))
|
||||
|
||||
// Treeshake
|
||||
let gotTargets = Set(got.targets.flatMap { $0.value })
|
||||
let gotProjects = Set(got.projects)
|
||||
XCTAssertFalse(gotTargets.contains(bFrameworkNode))
|
||||
XCTAssertFalse(gotTargets.contains(cFrameworkNode))
|
||||
XCTAssertFalse(gotTargets.contains(dFrameworkNode))
|
||||
XCTAssertFalse(gotProjects.contains(bFrameworkNode.project))
|
||||
XCTAssertFalse(gotProjects.contains(cFrameworkNode.project))
|
||||
XCTAssertFalse(gotProjects.contains(dFrameworkNode.project))
|
||||
}
|
||||
|
||||
// Seventh scenario
|
||||
// +---->B (Cached Framework)+
|
||||
// | |
|
||||
// App| +------>D Precompiled .framework
|
||||
// | |
|
||||
// +---->C (Cached Framework)+
|
||||
func test_map_when_seventh_scenario() throws {
|
||||
let path = try temporaryPath()
|
||||
|
||||
// Given: D
|
||||
let dFrameworkPath = path.appending(component: "D.framework")
|
||||
let dFramework = FrameworkNode.test(path: dFrameworkPath)
|
||||
|
||||
// Given: B
|
||||
let bFramework = Target.test(name: "B", platform: .iOS, product: .framework)
|
||||
let bProject = Project.test(path: path.appending(component: "B"), name: "B", targets: [bFramework])
|
||||
let bFrameworkNode = TargetNode.test(project: bProject, target: bFramework, dependencies: [dFramework])
|
||||
|
||||
// Given: C
|
||||
let cFramework = Target.test(name: "C", platform: .iOS, product: .framework)
|
||||
let cProject = Project.test(path: path.appending(component: "C"), name: "C", targets: [cFramework])
|
||||
let cFrameworkNode = TargetNode.test(project: cProject, target: cFramework, dependencies: [dFramework])
|
||||
|
||||
// Given: App
|
||||
let appTarget = Target.test(name: "App", platform: .iOS, product: .app)
|
||||
let appProject = Project.test(path: path.appending(component: "App"), name: "App", targets: [appTarget])
|
||||
let appTargetNode = TargetNode.test(project: appProject, target: appTarget, dependencies: [bFrameworkNode, cFrameworkNode])
|
||||
|
||||
let targetNodes = [bFrameworkNode, cFrameworkNode, appTargetNode]
|
||||
let graph = Graph.test(entryNodes: [appTargetNode], projects: graphProjects(targetNodes), targets: graphTargets(targetNodes))
|
||||
|
||||
// Given xcframeworks
|
||||
let bCachedFrameworkPath = path.appending(component: "B.framework")
|
||||
let bCachedFramework = FrameworkNode.test(path: bCachedFrameworkPath)
|
||||
let cCachedFrameworkPath = path.appending(component: "C.framework")
|
||||
let cCachedFramework = FrameworkNode.test(path: cCachedFrameworkPath)
|
||||
let frameworks = [
|
||||
bFrameworkNode: bCachedFrameworkPath,
|
||||
cFrameworkNode: cCachedFrameworkPath,
|
||||
]
|
||||
|
||||
frameworkLoader.loadStub = { path in
|
||||
if path == bCachedFrameworkPath { return bCachedFramework }
|
||||
else if path == cCachedFrameworkPath { return cCachedFramework }
|
||||
else if path == dFrameworkPath { return dFramework }
|
||||
else { fatalError("Unexpected load call") }
|
||||
}
|
||||
|
||||
xcframeworkLoader.loadStub = { _ in
|
||||
throw "Can't find .framework here"
|
||||
}
|
||||
|
||||
// When
|
||||
let got = try subject.map(graph: graph, precompiledFrameworks: frameworks, sources: Set(["App"]))
|
||||
|
||||
// Then
|
||||
let app = try XCTUnwrap(got.entryNodes.first as? TargetNode)
|
||||
let b = try XCTUnwrap(app.dependencies.compactMap { $0 as? FrameworkNode }.first(where: { $0.path == bCachedFrameworkPath }))
|
||||
let c = try XCTUnwrap(app.dependencies.compactMap { $0 as? FrameworkNode }.first(where: { $0.path == cCachedFrameworkPath }))
|
||||
XCTAssertTrue(b.dependencies.contains(where: { $0.path == dFrameworkPath }))
|
||||
XCTAssertTrue(c.dependencies.contains(where: { $0.path == dFrameworkPath }))
|
||||
|
||||
// Treeshake
|
||||
let gotTargets = Set(got.targets.flatMap { $0.value })
|
||||
let gotProjects = Set(got.projects)
|
||||
XCTAssertFalse(gotTargets.contains(bFrameworkNode))
|
||||
XCTAssertFalse(gotTargets.contains(cFrameworkNode))
|
||||
XCTAssertFalse(gotProjects.contains(bFrameworkNode.project))
|
||||
XCTAssertFalse(gotProjects.contains(cFrameworkNode.project))
|
||||
}
|
||||
|
||||
// Eighth scenario
|
||||
// +---->B (Cached Framework)+
|
||||
// | |
|
||||
// App| +------>D Precompiled .framework
|
||||
// | |
|
||||
// +---->C (Cached Framework)+------>E Precompiled .xcframework
|
||||
func test_map_when_eighth_scenario() throws {
|
||||
let path = try temporaryPath()
|
||||
|
||||
// Given nodes
|
||||
|
||||
// Given E
|
||||
let eXCFrameworkPath = path.appending(component: "E.xcframework")
|
||||
let eXCFramework = XCFrameworkNode.test(path: eXCFrameworkPath)
|
||||
|
||||
// Given: D
|
||||
let dFrameworkPath = path.appending(component: "D.framework")
|
||||
let dFramework = FrameworkNode.test(path: dFrameworkPath)
|
||||
|
||||
// Given: B
|
||||
let bFramework = Target.test(name: "B", platform: .iOS, product: .framework)
|
||||
let bProject = Project.test(path: path.appending(component: "B"), name: "B", targets: [bFramework])
|
||||
let bFrameworkNode = TargetNode.test(project: bProject, target: bFramework, dependencies: [dFramework])
|
||||
|
||||
// Given: C
|
||||
let cFramework = Target.test(name: "C", platform: .iOS, product: .framework)
|
||||
let cProject = Project.test(path: path.appending(component: "C"), name: "C", targets: [cFramework])
|
||||
let cFrameworkNode = TargetNode.test(project: cProject, target: cFramework, dependencies: [dFramework, eXCFramework])
|
||||
|
||||
// Given: App
|
||||
let appTarget = Target.test(name: "App", platform: .iOS, product: .app)
|
||||
let appProject = Project.test(path: path.appending(component: "App"), name: "App", targets: [appTarget])
|
||||
let appTargetNode = TargetNode.test(project: appProject, target: appTarget, dependencies: [bFrameworkNode, cFrameworkNode])
|
||||
|
||||
let targetNodes = [bFrameworkNode, cFrameworkNode, appTargetNode]
|
||||
let graph = Graph.test(entryNodes: [appTargetNode], projects: graphProjects(targetNodes), targets: graphTargets(targetNodes))
|
||||
|
||||
// Given xcframeworks
|
||||
let bCachedFrameworkPath = path.appending(component: "B.framework")
|
||||
let bCachedFramework = FrameworkNode.test(path: bCachedFrameworkPath)
|
||||
let cCachedFrameworkPath = path.appending(component: "C.framework")
|
||||
let cCachedFramework = FrameworkNode.test(path: cCachedFrameworkPath)
|
||||
let frameworks = [
|
||||
bFrameworkNode: bCachedFrameworkPath,
|
||||
cFrameworkNode: cCachedFrameworkPath,
|
||||
]
|
||||
|
||||
frameworkLoader.loadStub = { path in
|
||||
if path == bCachedFrameworkPath { return bCachedFramework }
|
||||
else if path == cCachedFrameworkPath { return cCachedFramework }
|
||||
else if path == dFrameworkPath { return dFramework }
|
||||
else { fatalError("Unexpected load call") }
|
||||
}
|
||||
|
||||
xcframeworkLoader.loadStub = { path in
|
||||
if path == eXCFrameworkPath { return eXCFramework }
|
||||
throw "Can't find an .xcframework here"
|
||||
}
|
||||
|
||||
// When
|
||||
let got = try subject.map(graph: graph, precompiledFrameworks: frameworks, sources: Set(["App"]))
|
||||
|
||||
// Then
|
||||
let app = try XCTUnwrap(got.entryNodes.first as? TargetNode)
|
||||
let b = try XCTUnwrap(app.dependencies.compactMap { $0 as? FrameworkNode }.first(where: { $0.path == bCachedFrameworkPath }))
|
||||
let c = try XCTUnwrap(app.dependencies.compactMap { $0 as? FrameworkNode }.first(where: { $0.path == cCachedFrameworkPath }))
|
||||
XCTAssertTrue(b.dependencies.contains(where: { $0.path == dFrameworkPath }))
|
||||
XCTAssertTrue(c.dependencies.contains(where: { $0.path == dFrameworkPath }))
|
||||
XCTAssertTrue(c.dependencies.contains(where: { $0.path == eXCFrameworkPath }))
|
||||
|
||||
// Treeshake
|
||||
let gotTargets = Set(got.targets.flatMap { $0.value })
|
||||
let gotProjects = Set(got.projects)
|
||||
XCTAssertFalse(gotTargets.contains(bFrameworkNode))
|
||||
XCTAssertFalse(gotTargets.contains(cFrameworkNode))
|
||||
XCTAssertFalse(gotProjects.contains(bFrameworkNode.project))
|
||||
XCTAssertFalse(gotProjects.contains(cFrameworkNode.project))
|
||||
}
|
||||
|
||||
// 9th scenario
|
||||
// +---->B (Framework)+------>D Precompiled .framework
|
||||
// |
|
||||
// App|
|
||||
// |
|
||||
// +---->C (Cached Framework)+------>E Precompiled .xcframework
|
||||
func test_map_when_nineth_scenario() throws {
|
||||
let path = try temporaryPath()
|
||||
|
||||
// Given nodes
|
||||
|
||||
// Given: E
|
||||
let eXCFrameworkPath = path.appending(component: "E.xcframework")
|
||||
let eXCFramework = XCFrameworkNode.test(path: eXCFrameworkPath)
|
||||
|
||||
// Given: D
|
||||
let dFrameworkPath = path.appending(component: "D.framework")
|
||||
let dFramework = FrameworkNode.test(path: dFrameworkPath)
|
||||
|
||||
// Given: B
|
||||
let bFramework = Target.test(name: "B", platform: .iOS, product: .framework)
|
||||
let bProject = Project.test(path: path.appending(component: "B"), name: "B", targets: [bFramework])
|
||||
let bFrameworkNode = TargetNode.test(project: bProject, target: bFramework, dependencies: [dFramework])
|
||||
|
||||
// Given: C
|
||||
let cProject = Project.test(path: path.appending(component: "C"), name: "C")
|
||||
let cFramework = Target.test(name: "C", platform: .iOS, product: .framework)
|
||||
let cFrameworkNode = TargetNode.test(project: cProject, target: cFramework, dependencies: [eXCFramework])
|
||||
|
||||
// Given: App
|
||||
let appProject = Project.test(path: path.appending(component: "App"), name: "App")
|
||||
let appTargetNode = TargetNode.test(project: appProject, target: Target.test(name: "App", platform: .iOS, product: .app), dependencies: [bFrameworkNode, cFrameworkNode])
|
||||
|
||||
let targetNodes = [bFrameworkNode, cFrameworkNode, appTargetNode]
|
||||
let graph = Graph.test(entryNodes: [appTargetNode], projects: graphProjects(targetNodes), targets: graphTargets(targetNodes))
|
||||
|
||||
// Given xcframeworks
|
||||
let cCachedFrameworkPath = path.appending(component: "C.xcframework")
|
||||
let cCachedFramework = FrameworkNode.test(path: cCachedFrameworkPath)
|
||||
let frameworks = [
|
||||
cFrameworkNode: cCachedFrameworkPath,
|
||||
]
|
||||
|
||||
frameworkLoader.loadStub = { path in
|
||||
if path == cCachedFrameworkPath { return cCachedFramework }
|
||||
else { fatalError("Unexpected load call") }
|
||||
}
|
||||
|
||||
xcframeworkLoader.loadStub = { _ in
|
||||
throw "Can't find an .xcframework here"
|
||||
}
|
||||
|
||||
// When
|
||||
let got = try subject.map(graph: graph, precompiledFrameworks: frameworks, sources: Set(["App"]))
|
||||
|
||||
// Then
|
||||
let app = try XCTUnwrap(got.entryNodes.first as? TargetNode)
|
||||
let b = try XCTUnwrap(app.dependencies.compactMap { $0 as? TargetNode }.first(where: { $0.name == "B" }))
|
||||
let c = try XCTUnwrap(app.dependencies.compactMap { $0 as? FrameworkNode }.first(where: { $0.path == cCachedFrameworkPath }))
|
||||
XCTAssertTrue(b.dependencies.contains(where: { $0.path == dFrameworkPath }))
|
||||
XCTAssertTrue(c.dependencies.contains(where: { $0.path == eXCFrameworkPath }))
|
||||
|
||||
// Treeshake
|
||||
let gotTargets = Set(got.targets.flatMap { $0.value })
|
||||
let gotProjects = Set(got.projects)
|
||||
XCTAssertTrue(gotTargets.contains(bFrameworkNode))
|
||||
XCTAssertFalse(gotTargets.contains(cFrameworkNode))
|
||||
XCTAssertTrue(gotProjects.contains(bFrameworkNode.project))
|
||||
XCTAssertFalse(gotProjects.contains(cFrameworkNode.project))
|
||||
}
|
||||
|
||||
fileprivate func graphProjects(_ targets: [TargetNode]) -> [Project] {
|
||||
let projects = targets.reduce(into: Set<Project>()) { acc, target in
|
||||
acc.formUnion([target.project])
|
||||
|
|
|
@ -14,8 +14,9 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
var subject: CacheRemoteStorage!
|
||||
var cloudClient: CloudClienting!
|
||||
var config: Config!
|
||||
var fileArchiverFactory: MockFileArchiverFactory!
|
||||
var fileArchiverFactory: MockFileArchivingFactory!
|
||||
var fileArchiver: MockFileArchiver!
|
||||
var fileUnarchiver: MockFileUnarchiver!
|
||||
var fileClient: MockFileClient!
|
||||
var zipPath: AbsolutePath!
|
||||
|
||||
|
@ -25,10 +26,12 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
config = TuistCore.Config.test()
|
||||
zipPath = fixturePath(path: RelativePath("uUI.xcframework.zip"))
|
||||
|
||||
fileArchiverFactory = MockFileArchiverFactory()
|
||||
fileArchiverFactory = MockFileArchivingFactory()
|
||||
fileArchiver = MockFileArchiver()
|
||||
fileUnarchiver = MockFileUnarchiver()
|
||||
fileArchiver.stubbedZipResult = zipPath
|
||||
fileArchiverFactory.stubbedMakeFileArchiverResult = fileArchiver
|
||||
fileArchiverFactory.stubbedMakeFileUnarchiverResult = fileUnarchiver
|
||||
fileClient = MockFileClient()
|
||||
fileClient.stubbedDownloadResult = Single.just(zipPath)
|
||||
|
||||
|
@ -158,36 +161,6 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
func test_fetch_whenArchiveContainsIncorrectRootFolderAfterUnzipping_expectArchiveDeleted() throws {
|
||||
// Given
|
||||
typealias ResponseType = CloudResponse<CloudCacheResponse>
|
||||
typealias ErrorType = CloudResponseError
|
||||
|
||||
let httpResponse: HTTPURLResponse = .test()
|
||||
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
||||
let config = Cloud.test()
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudConfig: config, cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
let hash = "acho tio"
|
||||
let paths = try createFolders(["Cache/xcframeworks/\(hash)/IncorrectRootFolderAfterUnzipping"])
|
||||
let expectedDeletedPath = AbsolutePath(paths.first!.dirname)
|
||||
|
||||
// When
|
||||
let result = subject.fetch(hash: hash)
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
// Then
|
||||
switch result {
|
||||
case .completed:
|
||||
XCTFail("Expected result to complete with error, but result was successful.")
|
||||
case .failed:
|
||||
XCTAssertFalse(FileHandler.shared.exists(expectedDeletedPath))
|
||||
}
|
||||
}
|
||||
|
||||
func test_fetch_whenArchiveContainsIncorrectRootFolderAfterUnzipping_expectErrorThrown() throws {
|
||||
// Given
|
||||
typealias ResponseType = CloudResponse<CloudCacheResponse>
|
||||
|
@ -199,9 +172,9 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudConfig: config, cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
let hash = "acho tio"
|
||||
let paths = try createFolders(["Cache/xcframeworks/\(hash)/IncorrectRootFolderAfterUnzipping"])
|
||||
let expectedPath = AbsolutePath(paths.first!.dirname)
|
||||
let hash = "foobar"
|
||||
let paths = try createFolders(["Unarchived/\(hash)/IncorrectRootFolderAfterUnzipping"])
|
||||
fileUnarchiver.stubbedUnzipResult = paths.first
|
||||
|
||||
// When
|
||||
let result = subject.fetch(hash: hash)
|
||||
|
@ -213,7 +186,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
case .completed:
|
||||
XCTFail("Expected result to complete with error, but result was successful.")
|
||||
case let .failed(_, error) where error is CacheRemoteStorageError:
|
||||
XCTAssertEqual(error as! CacheRemoteStorageError, CacheRemoteStorageError.archiveDoesNotContainXCFramework(expectedPath))
|
||||
XCTAssertEqual(error as! CacheRemoteStorageError, CacheRemoteStorageError.frameworkNotFound(hash: hash))
|
||||
default:
|
||||
XCTFail("Expected result to complete with error, but result error wasn't the expected type.")
|
||||
}
|
||||
|
@ -227,12 +200,13 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
let httpResponse: HTTPURLResponse = .test()
|
||||
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
||||
let config = Cloud.test()
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "success", data: cacheResponse)
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudConfig: config, cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
let hash = "acho tio"
|
||||
let paths = try createFolders(["Cache/xcframeworks/\(hash)/myFramework.xcframework"])
|
||||
let hash = "bar_foo"
|
||||
let paths = try createFolders(["Unarchived/\(hash)/myFramework.xcframework"])
|
||||
fileUnarchiver.stubbedUnzipResult = paths.first?.parentDirectory
|
||||
|
||||
// When
|
||||
let result = try subject.fetch(hash: hash)
|
||||
|
@ -240,7 +214,8 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
.single()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(result, paths.first!)
|
||||
let expectedPath = Environment.shared.buildCacheDirectory.appending(RelativePath("\(hash)/myFramework.xcframework"))
|
||||
XCTAssertEqual(result, expectedPath)
|
||||
}
|
||||
|
||||
func test_fetch_whenClientReturnsASuccess_givesFileClientTheCorrectURL() throws {
|
||||
|
@ -249,15 +224,16 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
typealias ErrorType = CloudResponseError
|
||||
|
||||
let httpResponse: HTTPURLResponse = .test()
|
||||
let url: URL = URL(string: "https://shaki.ra/acho/tio")!
|
||||
let url = URL(string: "https://tuist.io/acho/tio")!
|
||||
let config = Cloud.test()
|
||||
let cacheResponse = CloudCacheResponse(url: url, expiresAt: 123)
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudConfig: config, cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
let hash = "acho tio"
|
||||
_ = try createFolders(["Cache/xcframeworks/\(hash)/myFramework.xcframework"])
|
||||
let hash = "foo_bar"
|
||||
let paths = try createFolders(["Unarchived/\(hash)/myFramework.xcframework"])
|
||||
fileUnarchiver.stubbedUnzipResult = paths.first!.parentDirectory
|
||||
|
||||
// When
|
||||
_ = try subject.fetch(hash: hash)
|
||||
|
@ -280,8 +256,10 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(object: cloudResponse, response: httpResponse)
|
||||
subject = CacheRemoteStorage(cloudConfig: config, cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
let hash = "acho tio"
|
||||
let paths = try createFolders(["Cache/xcframeworks/\(hash)/myFramework.xcframework"])
|
||||
let paths = try createFolders(["Unarchived/\(hash)/Framework.framework"])
|
||||
fileUnarchiver.stubbedUnzipResult = paths.first?.parentDirectory
|
||||
|
||||
let hash = "foo_bar"
|
||||
|
||||
// When
|
||||
_ = try subject.fetch(hash: hash)
|
||||
|
@ -289,7 +267,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
.single()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(fileArchiver.invokedUnzipParameters?.to, paths.first!.parentDirectory)
|
||||
XCTAssertTrue(fileUnarchiver.invokedUnzip)
|
||||
}
|
||||
|
||||
// - store
|
||||
|
@ -304,7 +282,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
subject = CacheRemoteStorage(cloudConfig: config, cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
let result = subject.store(hash: "acho tio", xcframeworkPath: .root)
|
||||
let result = subject.store(hash: "acho tio", paths: [.root])
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
|
@ -324,7 +302,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
typealias ResponseType = CloudResponse<CloudCacheResponse>
|
||||
typealias ErrorType = CloudResponseError
|
||||
|
||||
let url: URL = URL(string: "https://shaki.ra/acho/tio")!
|
||||
let url = URL(string: "https://shaki.ra/acho/tio")!
|
||||
let config = Cloud.test()
|
||||
let cacheResponse = CloudCacheResponse(url: url, expiresAt: 123)
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
||||
|
@ -335,7 +313,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
subject = CacheRemoteStorage(cloudConfig: config, cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
_ = subject.store(hash: "acho tio", xcframeworkPath: .root)
|
||||
_ = subject.store(hash: "foo_bar", paths: [.root])
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
|
@ -352,7 +330,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
typealias ResponseType = CloudResponse<CloudCacheResponse>
|
||||
typealias ErrorType = CloudResponseError
|
||||
|
||||
let hash = "acho tio hash"
|
||||
let hash = "foo_bar"
|
||||
let config = Cloud.test()
|
||||
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
||||
|
@ -363,7 +341,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
subject = CacheRemoteStorage(cloudConfig: config, cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
_ = subject.store(hash: hash, xcframeworkPath: .root)
|
||||
_ = subject.store(hash: hash, paths: [.root])
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
|
@ -380,10 +358,10 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
typealias ResponseType = CloudResponse<CloudCacheResponse>
|
||||
typealias ErrorType = CloudResponseError
|
||||
|
||||
let hash = "acho tio hash"
|
||||
let hash = "foo_bar"
|
||||
let config = Cloud.test()
|
||||
let cacheResponse = CloudCacheResponse(url: .test(), expiresAt: 123)
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "shaki", data: cacheResponse)
|
||||
let cloudResponse = CloudResponse<CloudCacheResponse>(status: "waka", data: cacheResponse)
|
||||
cloudClient = MockCloudClienting<ResponseType, ErrorType>.makeForSuccess(
|
||||
object: cloudResponse,
|
||||
response: .test()
|
||||
|
@ -393,7 +371,7 @@ final class CacheRemoteStorageTests: TuistUnitTestCase {
|
|||
subject = CacheRemoteStorage(cloudConfig: config, cloudClient: cloudClient, fileArchiverFactory: fileArchiverFactory, fileClient: fileClient)
|
||||
|
||||
// When
|
||||
_ = subject.store(hash: hash, xcframeworkPath: .root)
|
||||
_ = subject.store(hash: hash, paths: [.root])
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ final class GraphContentHasherTests: TuistUnitTestCase {
|
|||
let graph = Graph.test()
|
||||
|
||||
// When
|
||||
let hashes = try subject.contentHashes(for: graph)
|
||||
let hashes = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(hashes, Dictionary())
|
||||
|
@ -51,7 +51,7 @@ final class GraphContentHasherTests: TuistUnitTestCase {
|
|||
let expectedCachableTargets = [frameworkTarget, secondFrameworkTarget, staticFrameworkTarget].sorted(by: { $0.target.name < $1.target.name })
|
||||
|
||||
// When
|
||||
let hashes = try subject.contentHashes(for: graph)
|
||||
let hashes = try subject.contentHashes(for: graph, cacheOutputType: .framework)
|
||||
let hashedTargets: [TargetNode] = hashes.keys.sorted { left, right -> Bool in
|
||||
left.project.path.pathString < right.project.path.pathString
|
||||
}.sorted(by: { $0.target.name < $1.target.name })
|
||||
|
|
|
@ -25,6 +25,7 @@ final class CacheMapperTests: TuistUnitTestCase {
|
|||
cache: cache,
|
||||
graphContentHasher: graphContentHasher,
|
||||
sources: [],
|
||||
cacheOutputType: .framework,
|
||||
cacheGraphMutator: cacheGraphMutator,
|
||||
queue: DispatchQueue.main)
|
||||
super.setUp()
|
||||
|
@ -39,7 +40,7 @@ final class CacheMapperTests: TuistUnitTestCase {
|
|||
subject = nil
|
||||
}
|
||||
|
||||
func test_map_when_all_xcframeworks_are_fetched_successfully() throws {
|
||||
func test_map_when_all_binaries_are_fetched_successfully() throws {
|
||||
let path = try temporaryPath()
|
||||
|
||||
// Given
|
||||
|
@ -65,7 +66,7 @@ final class CacheMapperTests: TuistUnitTestCase {
|
|||
bNode: bHash,
|
||||
appNode: appHash,
|
||||
]
|
||||
graphContentHasher.contentHashesStub = contentHashes
|
||||
graphContentHasher.stubbedContentHashesResult = contentHashes
|
||||
|
||||
cache.existsStub = { hash in
|
||||
if hash == bHash { return true }
|
||||
|
@ -87,7 +88,7 @@ final class CacheMapperTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(got.name, outputGraph.name)
|
||||
}
|
||||
|
||||
func test_map_when_one_of_the_xcframeworks_fails_cannot_be_fetched() throws {
|
||||
func test_map_when_one_of_the_binaries_fails_cannot_be_fetched() throws {
|
||||
let path = try temporaryPath()
|
||||
|
||||
// Given
|
||||
|
@ -113,7 +114,7 @@ final class CacheMapperTests: TuistUnitTestCase {
|
|||
appNode: appHash,
|
||||
]
|
||||
let error = TestError("error downloading C")
|
||||
graphContentHasher.contentHashesStub = contentHashes
|
||||
graphContentHasher.stubbedContentHashesResult = contentHashes
|
||||
|
||||
cache.existsStub = { hash in
|
||||
if hash == bHash { return true }
|
||||
|
@ -131,4 +132,34 @@ final class CacheMapperTests: TuistUnitTestCase {
|
|||
// Then
|
||||
XCTAssertThrowsSpecific(try subject.map(graph: inputGraph), error)
|
||||
}
|
||||
|
||||
func test_map_forwards_correct_artifactType_to_hasher() throws {
|
||||
// Given
|
||||
subject = CacheMapper(config: config,
|
||||
cache: cache,
|
||||
graphContentHasher: graphContentHasher,
|
||||
sources: [],
|
||||
cacheOutputType: .xcframework,
|
||||
cacheGraphMutator: cacheGraphMutator,
|
||||
queue: DispatchQueue.main)
|
||||
|
||||
let cFramework = Target.test(name: "C", platform: .iOS, product: .framework)
|
||||
let cNode = TargetNode.test(target: cFramework, dependencies: [])
|
||||
|
||||
let bFramework = Target.test(name: "B", platform: .iOS, product: .framework)
|
||||
let bNode = TargetNode.test(target: bFramework, dependencies: [cNode])
|
||||
|
||||
let app = Target.test(name: "App", platform: .iOS, product: .app)
|
||||
let appNode = TargetNode.test(target: app, dependencies: [bNode])
|
||||
|
||||
let inputGraph = Graph.test(name: "output", entryNodes: [appNode])
|
||||
let outputGraph = Graph.test(name: "output")
|
||||
cacheGraphMutator.stubbedMapResult = outputGraph
|
||||
|
||||
// When
|
||||
_ = try subject.map(graph: inputGraph)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(graphContentHasher.invokedContentHashesParameters?.cacheOutputType, .xcframework)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,16 +9,16 @@ import XCTest
|
|||
final class MockCacheGraphMutator: CacheGraphMutating {
|
||||
var invokedMap = false
|
||||
var invokedMapCount = 0
|
||||
var invokedMapParameters: (graph: Graph, xcframeworks: [TargetNode: AbsolutePath], sources: Set<String>)?
|
||||
var invokedMapParametersList = [(graph: Graph, xcframeworks: [TargetNode: AbsolutePath], sources: Set<String>)]()
|
||||
var invokedMapParameters: (graph: Graph, precompiledFrameworks: [TargetNode: AbsolutePath], sources: Set<String>)?
|
||||
var invokedMapParametersList = [(graph: Graph, precompiledFrameworks: [TargetNode: AbsolutePath], sources: Set<String>)]()
|
||||
var stubbedMapError: Error?
|
||||
var stubbedMapResult: Graph!
|
||||
|
||||
func map(graph: Graph, xcframeworks: [TargetNode: AbsolutePath], sources: Set<String>) throws -> Graph {
|
||||
func map(graph: Graph, precompiledFrameworks: [TargetNode: AbsolutePath], sources: Set<String>) throws -> Graph {
|
||||
invokedMap = true
|
||||
invokedMapCount += 1
|
||||
invokedMapParameters = (graph, xcframeworks, sources)
|
||||
invokedMapParametersList.append((graph, xcframeworks, sources))
|
||||
invokedMapParameters = (graph, precompiledFrameworks, sources)
|
||||
invokedMapParametersList.append((graph, precompiledFrameworks, sources))
|
||||
if let error = stubbedMapError {
|
||||
throw error
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import XCTest
|
||||
@testable import TuistCache
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class CacheBinaryBuilderErrorTests: TuistUnitTestCase {
|
||||
func test_type_when_nonFrameworkTargetForXCFramework() {
|
||||
// Given
|
||||
let subject = CacheBinaryBuilderError.nonFrameworkTargetForXCFramework("App")
|
||||
|
||||
// When
|
||||
let got = subject.type
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(got, .abort)
|
||||
}
|
||||
|
||||
func test_description_when_nonFrameworkTargetForXCFramework() {
|
||||
// Given
|
||||
let subject = CacheBinaryBuilderError.nonFrameworkTargetForXCFramework("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")
|
||||
}
|
||||
|
||||
func test_type_when_nonFrameworkTargetForFramework() {
|
||||
// Given
|
||||
let subject = CacheBinaryBuilderError.nonFrameworkTargetForFramework("App")
|
||||
|
||||
// When
|
||||
let got = subject.type
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(got, .abort)
|
||||
}
|
||||
|
||||
func test_description_when_nonFrameworkTargetForFramework() {
|
||||
// Given
|
||||
let subject = CacheBinaryBuilderError.nonFrameworkTargetForFramework("App")
|
||||
|
||||
// When
|
||||
let got = subject.description
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(got, "Can't generate a .framework from the target 'App' because it's not a framework target")
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
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")
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import TuistAutomation
|
||||
@testable import TuistCore
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class SimulatorRuntimeVersionTests: TuistUnitTestCase {
|
|
@ -23,7 +23,7 @@ final class ValueGraphTests: TuistUnitTestCase {
|
|||
let bFrameworkNode = FrameworkNode.test(path: bFrameworkPath,
|
||||
linking: .dynamic,
|
||||
architectures: [.armv7],
|
||||
dependencies: [aFrameworkNode])
|
||||
dependencies: [.framework(aFrameworkNode)])
|
||||
|
||||
// Given: SDK
|
||||
let xctestNode = SDKNode.xctest(platform: .iOS, status: .required)
|
||||
|
|
|
@ -25,7 +25,6 @@ final class EnvUpdaterTests: TuistUnitTestCase {
|
|||
func test_update() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
fileHandler.stubInTemporaryDirectory = temporaryPath
|
||||
let downloadURL = URL(string: "https://file.download.com/tuistenv.zip")!
|
||||
googleCloudStorageClient.latestTuistEnvBundleURLStub = downloadURL
|
||||
let downloadPath = temporaryPath.appending(component: "tuistenv.zip")
|
||||
|
|
|
@ -279,6 +279,10 @@ class WorkspaceStructureGeneratorTests: XCTestCase {
|
|||
}
|
||||
|
||||
fileprivate class InMemoryFileHandler: FileHandling {
|
||||
func temporaryDirectory() throws -> AbsolutePath {
|
||||
currentPath
|
||||
}
|
||||
|
||||
private enum Node {
|
||||
case file
|
||||
case folder
|
||||
|
@ -312,6 +316,14 @@ class WorkspaceStructureGeneratorTests: XCTestCase {
|
|||
}
|
||||
|
||||
func inTemporaryDirectory(_: (AbsolutePath) throws -> Void) throws {}
|
||||
func inTemporaryDirectory(removeOnCompletion _: Bool, _: (AbsolutePath) throws -> Void) throws {}
|
||||
func inTemporaryDirectory<Result>(_ closure: (AbsolutePath) throws -> Result) throws -> Result {
|
||||
try closure(currentPath)
|
||||
}
|
||||
|
||||
func inTemporaryDirectory<Result>(removeOnCompletion _: Bool, _ closure: (AbsolutePath) throws -> Result) throws -> Result {
|
||||
try closure(currentPath)
|
||||
}
|
||||
|
||||
func glob(_: AbsolutePath, glob _: String) -> [AbsolutePath] {
|
||||
[]
|
||||
|
|
|
@ -25,7 +25,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase {
|
|||
|
||||
func testGenerateThrowsLintingErrorWhenConfigurationsAreEmpty() throws {
|
||||
// Given
|
||||
let projectSettings: Settings = Settings(configurations: [:])
|
||||
let projectSettings = Settings(configurations: [:])
|
||||
let targetSettings: Settings? = nil
|
||||
|
||||
// When / Then
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
import TuistAutomation
|
||||
import TuistSupport
|
||||
import XCTest
|
||||
|
||||
@testable import TuistCache
|
||||
@testable import TuistCore
|
||||
@testable import TuistCoreTesting
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class CacheFrameworkBuilderIntegrationTests: TuistTestCase {
|
||||
var subject: CacheFrameworkBuilder!
|
||||
var frameworkMetadataProvider: FrameworkMetadataProvider!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
frameworkMetadataProvider = FrameworkMetadataProvider()
|
||||
subject = CacheFrameworkBuilder(xcodeBuildController: XcodeBuildController())
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
subject = nil
|
||||
frameworkMetadataProvider = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_build_ios() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let frameworksPath = try temporaryFixture("Frameworks")
|
||||
let projectPath = frameworksPath.appending(component: "Frameworks.xcodeproj")
|
||||
let target = Target.test(name: "iOS", platform: .iOS, product: .framework, productName: "iOS")
|
||||
|
||||
// When
|
||||
try subject.build(projectPath: projectPath, target: target, into: temporaryPath)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(FileHandler.shared.glob(temporaryPath, glob: "*.framework").count, 1)
|
||||
XCTAssertEqual(FileHandler.shared.glob(temporaryPath, glob: "*.dSYM").count, 1)
|
||||
let frameworkPath = try XCTUnwrap(FileHandler.shared.glob(temporaryPath, glob: "*.framework").first)
|
||||
XCTAssertEqual(try binaryLinking(path: frameworkPath), .dynamic)
|
||||
XCTAssertTrue((try architectures(path: frameworkPath)).onlySimulator)
|
||||
XCTAssertEqual(try architectures(path: frameworkPath).count, 1)
|
||||
}
|
||||
|
||||
func test_build_macos() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let frameworksPath = try temporaryFixture("Frameworks")
|
||||
let projectPath = frameworksPath.appending(component: "Frameworks.xcodeproj")
|
||||
let target = Target.test(name: "macOS", platform: .macOS, product: .framework, productName: "macOS")
|
||||
|
||||
// When
|
||||
try subject.build(projectPath: projectPath, target: target, into: temporaryPath)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(FileHandler.shared.glob(temporaryPath, glob: "*.framework").count, 1)
|
||||
XCTAssertEqual(FileHandler.shared.glob(temporaryPath, glob: "*.dSYM").count, 1)
|
||||
let frameworkPath = try XCTUnwrap(FileHandler.shared.glob(temporaryPath, glob: "*.framework").first)
|
||||
XCTAssertEqual(try binaryLinking(path: frameworkPath), .dynamic)
|
||||
XCTAssertTrue(try architectures(path: frameworkPath).contains(.x8664))
|
||||
XCTAssertEqual(try architectures(path: frameworkPath).count, 1)
|
||||
}
|
||||
|
||||
func test_build_tvOS() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let frameworksPath = try temporaryFixture("Frameworks")
|
||||
let projectPath = frameworksPath.appending(component: "Frameworks.xcodeproj")
|
||||
let target = Target.test(name: "tvOS", platform: .tvOS, product: .framework, productName: "tvOS")
|
||||
|
||||
// When
|
||||
try subject.build(projectPath: projectPath, target: target, into: temporaryPath)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(FileHandler.shared.glob(temporaryPath, glob: "*.framework").count, 1)
|
||||
XCTAssertEqual(FileHandler.shared.glob(temporaryPath, glob: "*.dSYM").count, 1)
|
||||
let frameworkPath = try XCTUnwrap(FileHandler.shared.glob(temporaryPath, glob: "*.framework").first)
|
||||
XCTAssertEqual(try binaryLinking(path: frameworkPath), .dynamic)
|
||||
XCTAssertTrue((try architectures(path: frameworkPath)).onlySimulator)
|
||||
XCTAssertEqual(try architectures(path: frameworkPath).count, 1)
|
||||
}
|
||||
|
||||
func test_build_watchOS() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let frameworksPath = try temporaryFixture("Frameworks")
|
||||
let projectPath = frameworksPath.appending(component: "Frameworks.xcodeproj")
|
||||
let target = Target.test(name: "watchOS", platform: .watchOS, product: .framework, productName: "watchOS")
|
||||
|
||||
// When
|
||||
try subject.build(projectPath: projectPath, target: target, into: temporaryPath)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(FileHandler.shared.glob(temporaryPath, glob: "*.framework").count, 1)
|
||||
XCTAssertEqual(FileHandler.shared.glob(temporaryPath, glob: "*.dSYM").count, 1)
|
||||
let frameworkPath = try XCTUnwrap(FileHandler.shared.glob(temporaryPath, glob: "*.framework").first)
|
||||
XCTAssertEqual(try binaryLinking(path: frameworkPath), .dynamic)
|
||||
XCTAssertTrue((try architectures(path: frameworkPath)).onlySimulator)
|
||||
XCTAssertEqual(try architectures(path: frameworkPath).count, 1)
|
||||
}
|
||||
|
||||
fileprivate func binaryLinking(path: AbsolutePath) throws -> BinaryLinking {
|
||||
let binaryPath = FrameworkNode.binaryPath(frameworkPath: path)
|
||||
return try frameworkMetadataProvider.linking(binaryPath: binaryPath)
|
||||
}
|
||||
|
||||
fileprivate func architectures(path: AbsolutePath) throws -> [BinaryArchitecture] {
|
||||
let binaryPath = FrameworkNode.binaryPath(frameworkPath: path)
|
||||
return try frameworkMetadataProvider.architectures(binaryPath: binaryPath)
|
||||
}
|
||||
}
|
|
@ -9,14 +9,14 @@ import XCTest
|
|||
@testable import TuistCoreTesting
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
|
||||
var subject: XCFrameworkBuilder!
|
||||
final class CacheXCFrameworkBuilderIntegrationTests: TuistTestCase {
|
||||
var subject: CacheXCFrameworkBuilder!
|
||||
var plistDecoder: PropertyListDecoder!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
plistDecoder = PropertyListDecoder()
|
||||
subject = XCFrameworkBuilder(xcodeBuildController: XcodeBuildController())
|
||||
subject = CacheXCFrameworkBuilder(xcodeBuildController: XcodeBuildController())
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
|
@ -27,16 +27,18 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
|
|||
|
||||
func test_build_when_iOS_framework() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let frameworksPath = try temporaryFixture("Frameworks")
|
||||
let projectPath = frameworksPath.appending(component: "Frameworks.xcodeproj")
|
||||
let target = Target.test(name: "iOS", platform: .iOS, product: .framework, productName: "iOS")
|
||||
|
||||
// When
|
||||
let xcframeworkPath = try subject.build(projectPath: projectPath, target: target, includeDeviceArch: true).toBlocking().single()
|
||||
let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath)
|
||||
try subject.build(projectPath: projectPath, target: target, into: temporaryPath)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(FileHandler.shared.exists(xcframeworkPath))
|
||||
XCTAssertEqual(FileHandler.shared.glob(temporaryPath, glob: "*.xcframework").count, 1)
|
||||
let xcframeworkPath = try XCTUnwrap(FileHandler.shared.glob(temporaryPath, glob: "*.xcframework").first)
|
||||
let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath)
|
||||
XCTAssertNotNil(infoPlist.availableLibraries.first(where: { $0.supportedArchitectures.contains("arm64") }))
|
||||
XCTAssertNotNil(infoPlist.availableLibraries.first(where: { $0.supportedArchitectures.contains("x86_64") }))
|
||||
XCTAssertTrue(infoPlist.availableLibraries.allSatisfy { $0.supportedPlatform == "ios" })
|
||||
|
@ -45,16 +47,18 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
|
|||
|
||||
func test_build_when_macOS_framework() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let frameworksPath = try temporaryFixture("Frameworks")
|
||||
let projectPath = frameworksPath.appending(component: "Frameworks.xcodeproj")
|
||||
let target = Target.test(name: "macOS", platform: .macOS, product: .framework, productName: "macOS")
|
||||
|
||||
// When
|
||||
let xcframeworkPath = try subject.build(projectPath: projectPath, target: target, includeDeviceArch: true).toBlocking().single()
|
||||
let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath)
|
||||
try subject.build(projectPath: projectPath, target: target, into: temporaryPath)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(FileHandler.shared.exists(xcframeworkPath))
|
||||
XCTAssertEqual(FileHandler.shared.glob(temporaryPath, glob: "*.xcframework").count, 1)
|
||||
let xcframeworkPath = try XCTUnwrap(FileHandler.shared.glob(temporaryPath, glob: "*.xcframework").first)
|
||||
let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath)
|
||||
XCTAssertNotNil(infoPlist.availableLibraries.first(where: { $0.supportedArchitectures.contains("x86_64") }))
|
||||
XCTAssertTrue(infoPlist.availableLibraries.allSatisfy { $0.supportedPlatform == "macos" })
|
||||
try FileHandler.shared.delete(xcframeworkPath)
|
||||
|
@ -62,16 +66,18 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
|
|||
|
||||
func test_build_when_tvOS_framework() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let frameworksPath = try temporaryFixture("Frameworks")
|
||||
let projectPath = frameworksPath.appending(component: "Frameworks.xcodeproj")
|
||||
let target = Target.test(name: "tvOS", platform: .tvOS, product: .framework, productName: "tvOS")
|
||||
|
||||
// When
|
||||
let xcframeworkPath = try subject.build(projectPath: projectPath, target: target, includeDeviceArch: true).toBlocking().single()
|
||||
let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath)
|
||||
try subject.build(projectPath: projectPath, target: target, into: temporaryPath)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(FileHandler.shared.exists(xcframeworkPath))
|
||||
XCTAssertEqual(FileHandler.shared.glob(temporaryPath, glob: "*.xcframework").count, 1)
|
||||
let xcframeworkPath = try XCTUnwrap(FileHandler.shared.glob(temporaryPath, glob: "*.xcframework").first)
|
||||
let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath)
|
||||
XCTAssertNotNil(infoPlist.availableLibraries.first(where: { $0.supportedArchitectures.contains("x86_64") }))
|
||||
XCTAssertNotNil(infoPlist.availableLibraries.first(where: { $0.supportedArchitectures.contains("arm64") }))
|
||||
XCTAssertTrue(infoPlist.availableLibraries.allSatisfy { $0.supportedPlatform == "tvos" })
|
||||
|
@ -80,16 +86,18 @@ final class XCFrameworkBuilderIntegrationTests: TuistTestCase {
|
|||
|
||||
func test_build_when_watchOS_framework() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let frameworksPath = try temporaryFixture("Frameworks")
|
||||
let projectPath = frameworksPath.appending(component: "Frameworks.xcodeproj")
|
||||
let target = Target.test(name: "watchOS", platform: .watchOS, product: .framework, productName: "watchOS")
|
||||
|
||||
// When
|
||||
let xcframeworkPath = try subject.build(projectPath: projectPath, target: target, includeDeviceArch: true).toBlocking().single()
|
||||
let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath)
|
||||
try subject.build(projectPath: projectPath, target: target, into: temporaryPath)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(FileHandler.shared.exists(xcframeworkPath))
|
||||
XCTAssertEqual(FileHandler.shared.glob(temporaryPath, glob: "*.xcframework").count, 1)
|
||||
let xcframeworkPath = try XCTUnwrap(FileHandler.shared.glob(temporaryPath, glob: "*.xcframework").first)
|
||||
let infoPlist = try self.infoPlist(xcframeworkPath: xcframeworkPath)
|
||||
XCTAssertNotNil(infoPlist.availableLibraries.first(where: { $0.supportedArchitectures.contains("i386") }))
|
||||
XCTAssertNotNil(infoPlist.availableLibraries.first(where: { $0.supportedArchitectures.contains("armv7k") }))
|
||||
XCTAssertNotNil(infoPlist.availableLibraries.first(where: { $0.supportedArchitectures.contains("arm64_32") }))
|
|
@ -14,7 +14,7 @@ import XCTest
|
|||
final class CacheControllerTests: TuistUnitTestCase {
|
||||
var generator: MockProjectGenerator!
|
||||
var graphContentHasher: MockGraphContentHasher!
|
||||
var xcframeworkBuilder: MockXCFrameworkBuilder!
|
||||
var artifactBuilder: MockCacheArtifactBuilder!
|
||||
var manifestLoader: MockManifestLoader!
|
||||
var cache: MockCacheStorage!
|
||||
var subject: CacheController!
|
||||
|
@ -22,14 +22,14 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
|
||||
override func setUp() {
|
||||
generator = MockProjectGenerator()
|
||||
xcframeworkBuilder = MockXCFrameworkBuilder()
|
||||
artifactBuilder = MockCacheArtifactBuilder()
|
||||
cache = MockCacheStorage()
|
||||
manifestLoader = MockManifestLoader()
|
||||
graphContentHasher = MockGraphContentHasher()
|
||||
config = .test()
|
||||
subject = CacheController(cache: cache,
|
||||
artifactBuilder: artifactBuilder,
|
||||
generator: generator,
|
||||
xcframeworkBuilder: xcframeworkBuilder,
|
||||
graphContentHasher: graphContentHasher)
|
||||
|
||||
super.setUp()
|
||||
|
@ -38,7 +38,7 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
override func tearDown() {
|
||||
super.tearDown()
|
||||
generator = nil
|
||||
xcframeworkBuilder = nil
|
||||
artifactBuilder = nil
|
||||
graphContentHasher = nil
|
||||
manifestLoader = nil
|
||||
cache = nil
|
||||
|
@ -53,10 +53,10 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
let project = Project.test(path: path, name: "Cache")
|
||||
let aTarget = Target.test(name: "A")
|
||||
let bTarget = Target.test(name: "B")
|
||||
let axcframeworkPath = path.appending(component: "A.xcframework")
|
||||
let bxcframeworkPath = path.appending(component: "B.xcframework")
|
||||
try FileHandler.shared.createFolder(axcframeworkPath)
|
||||
try FileHandler.shared.createFolder(bxcframeworkPath)
|
||||
let aFrameworkPath = path.appending(component: "A.framework")
|
||||
let bFrameworkPath = path.appending(component: "B.framework")
|
||||
try FileHandler.shared.createFolder(aFrameworkPath)
|
||||
try FileHandler.shared.createFolder(bFrameworkPath)
|
||||
|
||||
let nodeWithHashes = [
|
||||
TargetNode.test(project: project, target: aTarget): "A_HASH",
|
||||
|
@ -73,24 +73,18 @@ final class CacheControllerTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(loadPath, path)
|
||||
return (xcworkspacePath, graph)
|
||||
}
|
||||
graphContentHasher.contentHashesStub = nodeWithHashes
|
||||
graphContentHasher.stubbedContentHashesResult = nodeWithHashes
|
||||
artifactBuilder.stubbedCacheOutputType = .xcframework
|
||||
|
||||
xcframeworkBuilder.buildWorkspaceStub = { _xcworkspacePath, target in
|
||||
switch (_xcworkspacePath, target) {
|
||||
case (xcworkspacePath, aTarget): return .success(axcframeworkPath)
|
||||
case (xcworkspacePath, bTarget): return .success(bxcframeworkPath)
|
||||
default: return .failure(TestError("Received invalid Xcode project path or target"))
|
||||
}
|
||||
}
|
||||
|
||||
try subject.cache(path: path, includeDeviceArch: true)
|
||||
try subject.cache(path: path)
|
||||
|
||||
// Then
|
||||
XCTAssertPrinterOutputContains("""
|
||||
Hashing cacheable frameworks
|
||||
All cacheable frameworks have been cached successfully
|
||||
Building cacheable frameworks as xcframeworks
|
||||
All cacheable frameworks have been cached successfully as xcframeworks
|
||||
""")
|
||||
XCTAssertFalse(FileHandler.shared.exists(axcframeworkPath))
|
||||
XCTAssertFalse(FileHandler.shared.exists(bxcframeworkPath))
|
||||
XCTAssertEqual(artifactBuilder.invokedBuildWorkspacePathParametersList.first?.target, aTarget)
|
||||
XCTAssertEqual(artifactBuilder.invokedBuildWorkspacePathParametersList.last?.target, bTarget)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import TuistGenerator
|
|||
final class MockGraphVizGenerator: GraphVizGenerating {
|
||||
var generateProjectArgs: [AbsolutePath] = []
|
||||
var generateWorkspaceArgs: [AbsolutePath] = []
|
||||
var generateProjectStub: GraphViz.Graph = GraphViz.Graph()
|
||||
var generateWorkspaceStub: GraphViz.Graph = GraphViz.Graph()
|
||||
var generateProjectStub = GraphViz.Graph()
|
||||
var generateWorkspaceStub = GraphViz.Graph()
|
||||
|
||||
func generateProject(at path: AbsolutePath, skipTestTargets _: Bool, skipExternalDependencies _: Bool) throws -> GraphViz.Graph {
|
||||
generateProjectArgs.append(path)
|
||||
|
|
|
@ -16,7 +16,7 @@ final class GraphMapperProviderTests: TuistUnitTestCase {
|
|||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
subject = GraphMapperProvider(cache: false)
|
||||
subject = GraphMapperProvider()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
|
@ -26,7 +26,7 @@ final class GraphMapperProviderTests: TuistUnitTestCase {
|
|||
|
||||
func test_mappers_returns_theCacheMapper_when_useCache_is_true() {
|
||||
// Given
|
||||
subject = GraphMapperProvider(cache: true)
|
||||
subject = GraphMapperProvider(cacheConfig: CacheConfig.withCaching(cacheOutputType: .framework))
|
||||
|
||||
// when
|
||||
let got = subject.mappers(config: Config.test())
|
||||
|
@ -37,7 +37,7 @@ final class GraphMapperProviderTests: TuistUnitTestCase {
|
|||
|
||||
func test_mappers_doesnt_return_theCacheMapper_when_useCache_is_false() {
|
||||
// Given
|
||||
subject = GraphMapperProvider(cache: false)
|
||||
subject = GraphMapperProvider()
|
||||
|
||||
// when
|
||||
let got = subject.mappers(config: Config.test())
|
||||
|
|
|
@ -43,7 +43,7 @@ final class CachePrintHashesServiceTests: TuistUnitTestCase {
|
|||
clock: clock)
|
||||
|
||||
// When
|
||||
_ = try subject.run(path: path)
|
||||
_ = try subject.run(path: path, xcframeworks: false)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(projectGenerator.invokedLoadParameterPath, path)
|
||||
|
@ -54,11 +54,11 @@ final class CachePrintHashesServiceTests: TuistUnitTestCase {
|
|||
subject = CachePrintHashesService(projectGenerator: projectGenerator,
|
||||
graphContentHasher: graphContentHasher,
|
||||
clock: clock)
|
||||
let graph: Graph = Graph.test()
|
||||
let graph = Graph.test()
|
||||
projectGenerator.loadStub = { _ in graph }
|
||||
|
||||
// When
|
||||
_ = try subject.run(path: path)
|
||||
_ = try subject.run(path: path, xcframeworks: false)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(graphContentHasher.invokedContentHashesParameters?.graph, graph)
|
||||
|
@ -68,17 +68,31 @@ final class CachePrintHashesServiceTests: TuistUnitTestCase {
|
|||
// Given
|
||||
let target1 = TargetNode.test(target: .test(name: "ShakiOne"))
|
||||
let target2 = TargetNode.test(target: .test(name: "ShakiTwo"))
|
||||
graphContentHasher.contentHashesStub = [target1: "hash1", target2: "hash2"]
|
||||
graphContentHasher.stubbedContentHashesResult = [target1: "hash1", target2: "hash2"]
|
||||
|
||||
subject = CachePrintHashesService(projectGenerator: projectGenerator,
|
||||
graphContentHasher: graphContentHasher,
|
||||
clock: clock)
|
||||
|
||||
// When
|
||||
_ = try subject.run(path: path)
|
||||
_ = try subject.run(path: path, xcframeworks: false)
|
||||
|
||||
// Then
|
||||
XCTAssertPrinterOutputContains("ShakiOne - hash1")
|
||||
XCTAssertPrinterOutputContains("ShakiTwo - hash2")
|
||||
}
|
||||
|
||||
func test_run_gives_correct_artifact_type_to_hasher() throws {
|
||||
// When
|
||||
_ = try subject.run(path: path, xcframeworks: true)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(graphContentHasher.invokedContentHashesParameters?.cacheOutputType, .xcframework)
|
||||
|
||||
// When
|
||||
_ = try subject.run(path: path, xcframeworks: false)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(graphContentHasher.invokedContentHashesParameters?.cacheOutputType, .framework)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,15 +12,15 @@ import XCTest
|
|||
final class MockFocusServiceProjectGeneratorFactory: FocusServiceProjectGeneratorFactorying {
|
||||
var invokedGenerator = false
|
||||
var invokedGeneratorCount = 0
|
||||
var invokedGeneratorParameters: (sources: Set<String>, Void)?
|
||||
var invokedGeneratorParametersList = [(sources: Set<String>, Void)]()
|
||||
var invokedGeneratorParameters: (sources: Set<String>, xcframeworks: Bool)?
|
||||
var invokedGeneratorParametersList = [(sources: Set<String>, xcframeworks: Bool)]()
|
||||
var stubbedGeneratorResult: ProjectGenerating!
|
||||
|
||||
func generator(sources: Set<String>) -> ProjectGenerating {
|
||||
func generator(sources: Set<String>, xcframeworks: Bool) -> ProjectGenerating {
|
||||
invokedGenerator = true
|
||||
invokedGeneratorCount += 1
|
||||
invokedGeneratorParameters = (sources, ())
|
||||
invokedGeneratorParametersList.append((sources, ()))
|
||||
invokedGeneratorParameters = (sources, xcframeworks)
|
||||
invokedGeneratorParametersList.append((sources, xcframeworks))
|
||||
return stubbedGeneratorResult
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ final class FocusServiceTests: TuistUnitTestCase {
|
|||
throw error
|
||||
}
|
||||
|
||||
XCTAssertThrowsError(try subject.run(path: nil, sources: Set(), noOpen: true)) {
|
||||
XCTAssertThrowsError(try subject.run(path: nil, sources: Set(), noOpen: true, xcframeworks: false)) {
|
||||
XCTAssertEqual($0 as NSError?, error)
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ final class FocusServiceTests: TuistUnitTestCase {
|
|||
workspacePath
|
||||
}
|
||||
|
||||
try subject.run(path: nil, sources: Set(), noOpen: false)
|
||||
try subject.run(path: nil, sources: Set(), noOpen: false, xcframeworks: false)
|
||||
|
||||
XCTAssertEqual(opener.openArgs.last?.0, workspacePath.pathString)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import XCTest
|
|||
final class UpMintErrorTests: TuistUnitTestCase {
|
||||
func test_type_when_mintFileNotFound() throws {
|
||||
// Given
|
||||
let upHomebrew: MockUp = MockUp()
|
||||
let upHomebrew = MockUp()
|
||||
let subject = UpMint(linkPackagesGlobally: false, upHomebrew: upHomebrew)
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Feature: Focuses projects with pre-compiled cached xcframeworks
|
||||
|
||||
Scenario: The project is an application with templates (ios_app_with_templates)
|
||||
Given that tuist is available
|
|
@ -1,11 +1,11 @@
|
|||
Feature: Focuses projects with pre-compiled cached dependencies
|
||||
Feature: Focuses projects with pre-compiled cached xcframeworks
|
||||
|
||||
Scenario: The project is an application with templates (ios_app_with_templates)
|
||||
Given that tuist is available
|
||||
And I have a working directory
|
||||
And I initialize a ios application named MyApp
|
||||
And tuist warms the cache
|
||||
When tuist focuses the target MyApp
|
||||
And tuist warms the cache with xcframeworks
|
||||
When tuist focuses the target MyApp using xcframeworks
|
||||
Then MyApp links the xcframework MyAppKit
|
||||
Then MyApp embeds the xcframework MyAppKit
|
||||
Then MyApp embeds the xcframework MyAppUI
|
||||
|
@ -17,7 +17,7 @@ Scenario: The project is an application (ios_workspace_with_microfeature_archite
|
|||
And I have a working directory
|
||||
Then I copy the fixture ios_workspace_with_microfeature_architecture into the working directory
|
||||
And tuist warms the cache
|
||||
When tuist focuses the target App at App
|
||||
When tuist focuses the target App at App using xcframeworks
|
||||
Then App embeds the xcframework Core
|
||||
Then App embeds the xcframework Data
|
||||
Then App embeds the xcframework FeatureContracts
|
||||
|
@ -31,7 +31,7 @@ Scenario: The project is an application and a target is modified after being cac
|
|||
Then I copy the fixture ios_workspace_with_microfeature_architecture into the working directory
|
||||
And tuist warms the cache
|
||||
And I add an empty line at the end of the file Frameworks/FeatureAFramework/Sources/FrameworkA.swift
|
||||
When tuist focuses the target App at App
|
||||
When tuist focuses the target App at App using xcframeworks
|
||||
Then App embeds the xcframework Core
|
||||
Then App embeds the xcframework Data
|
||||
Then App embeds the framework FrameworkA
|
||||
|
@ -46,7 +46,7 @@ Scenario: The project is an application and a target is generated as sources (io
|
|||
And I have a working directory
|
||||
Then I copy the fixture ios_workspace_with_microfeature_architecture into the working directory
|
||||
And tuist warms the cache
|
||||
When tuist focuses the targets App,FrameworkA at App
|
||||
When tuist focuses the targets App,FrameworkA at App using xcframeworks
|
||||
Then App embeds the xcframework Core
|
||||
Then App embeds the xcframework Data
|
||||
Then App embeds the framework FrameworkA
|
|
@ -4,6 +4,10 @@ Then(/^tuist warms the cache$/) do
|
|||
system("swift", "run", "tuist", "cache", "warm", "--path", @dir)
|
||||
end
|
||||
|
||||
Then(/^tuist warms the cache with xcframeworks$/) do
|
||||
system("swift", "run", "tuist", "cache", "warm", "--path", @dir, "--xcframeworks")
|
||||
end
|
||||
|
||||
Then(/^([a-zA-Z]+) links the xcframework ([a-zA-Z]+)$/) do |target_name, xcframework|
|
||||
projects = Xcode.projects(@workspace_path)
|
||||
target = projects.flat_map { |p| p.targets }.detect { |t| t.name == target_name }
|
||||
|
|
|
@ -18,7 +18,7 @@ Then(/^tuist generates the project with environment variable (.+) and value (.+)
|
|||
ENV[variable] = nil
|
||||
end
|
||||
|
||||
Then(/^tuist generates the project at (.+)$/) do |path|
|
||||
Then(/^tuist generates the project at ([a-zA-Z]\/+)$/) do |path|
|
||||
system("swift", "run", "tuist", "generate", "--path", File.join(@dir, path))
|
||||
@workspace_path = Dir.glob(File.join(@dir, path, "*.xcworkspace")).first
|
||||
@xcodeproj_path = Dir.glob(File.join(@dir, path, "*.xcodeproj")).first
|
||||
|
@ -30,18 +30,36 @@ Then(/^tuist focuses the target ([a-zA-Z]+)$/) do |target|
|
|||
@xcodeproj_path = Dir.glob(File.join(@dir, "*.xcodeproj")).first
|
||||
end
|
||||
|
||||
Then(/^tuist focuses the target ([a-zA-Z]+) at (.+)$/) do |target, path|
|
||||
Then(/^tuist focuses the target ([a-zA-Z]+) at ([a-zA-Z]\/+)$/) do |target, path|
|
||||
system("swift", "run", "tuist", "focus", "--no-open", "--path", File.join(@dir, path), target)
|
||||
@workspace_path = Dir.glob(File.join(@dir, path, "*.xcworkspace")).first
|
||||
@xcodeproj_path = Dir.glob(File.join(@dir, path, "*.xcodeproj")).first
|
||||
end
|
||||
|
||||
Then(/^tuist focuses the targets ([a-zA-Z,]+) at (.+)$/) do |targets, path|
|
||||
Then(/^tuist focuses the targets ([a-zA-Z,]+) at ([a-zA-Z]\/+)$/) do |targets, path|
|
||||
system("swift", "run", "tuist", "focus", "--no-open", "--path", File.join(@dir, path), *targets.split(","))
|
||||
@workspace_path = Dir.glob(File.join(@dir, path, "*.xcworkspace")).first
|
||||
@xcodeproj_path = Dir.glob(File.join(@dir, path, "*.xcodeproj")).first
|
||||
end
|
||||
|
||||
Then(/^tuist focuses the target ([a-zA-Z]+) using xcframeworks$/) do |target|
|
||||
system("swift", "run", "tuist", "focus", "--no-open", "--path", @dir, target, "--xcframeworks")
|
||||
@workspace_path = Dir.glob(File.join(@dir, "*.xcworkspace")).first
|
||||
@xcodeproj_path = Dir.glob(File.join(@dir, "*.xcodeproj")).first
|
||||
end
|
||||
|
||||
Then(/^tuist focuses the target ([a-zA-Z]+) at ([a-zA-Z]\/+) using xcframeworks$/) do |target, path|
|
||||
system("swift", "run", "tuist", "focus", "--no-open", "--path", File.join(@dir, path), target, "--xcframeworks")
|
||||
@workspace_path = Dir.glob(File.join(@dir, path, "*.xcworkspace")).first
|
||||
@xcodeproj_path = Dir.glob(File.join(@dir, path, "*.xcodeproj")).first
|
||||
end
|
||||
|
||||
Then(/^tuist focuses the targets ([a-zA-Z,]+) at ([a-zA-Z]\/+) using xcframeworks$/) do |targets, path|
|
||||
system("swift", "run", "tuist", "focus", "--no-open", "--path", File.join(@dir, path), *targets.split(","), "--xcframeworks")
|
||||
@workspace_path = Dir.glob(File.join(@dir, path, "*.xcworkspace")).first
|
||||
@xcodeproj_path = Dir.glob(File.join(@dir, path, "*.xcodeproj")).first
|
||||
end
|
||||
|
||||
Then(/tuist edits the project/) do
|
||||
system("swift", "run", "tuist", "edit", "--path", @dir, "--permanent")
|
||||
@xcodeproj_path = Dir.glob(File.join(@dir, "*.xcodeproj")).first
|
||||
|
|
|
@ -6,8 +6,8 @@ let project = Project(name: "App",
|
|||
platform: .iOS,
|
||||
product: .app,
|
||||
bundleId: "io.tuist.App",
|
||||
infoPlist: "Info.plist",
|
||||
sources: ["Sources/**"],
|
||||
infoPlist: "App/Info.plist",
|
||||
sources: ["App/Sources/**"],
|
||||
resources: [
|
||||
/* Path to resources can be defined here */
|
||||
// "Resources/**"
|
||||
|
@ -15,14 +15,14 @@ let project = Project(name: "App",
|
|||
dependencies: [
|
||||
/* Target dependencies can be defined here */
|
||||
// .framework(path: "Frameworks/MyFramework.framework")
|
||||
.project(target: "FrameworkA", path: "../Frameworks/FeatureAFramework")
|
||||
.project(target: "FrameworkA", path: "Frameworks/FeatureAFramework")
|
||||
]),
|
||||
Target(name: "AppTests",
|
||||
platform: .iOS,
|
||||
product: .unitTests,
|
||||
bundleId: "io.tuist.AppTests",
|
||||
infoPlist: "Tests.plist",
|
||||
sources: "Tests/**",
|
||||
infoPlist: "App/Tests.plist",
|
||||
sources: "App/Tests/**",
|
||||
dependencies: [
|
||||
.target(name: "App")
|
||||
])
|
|
@ -1,7 +0,0 @@
|
|||
import ProjectDescription
|
||||
|
||||
let workspace = Workspace(name: "Workspace",
|
||||
projects: [
|
||||
"App",
|
||||
"Frameworks/**",
|
||||
])
|
Loading…
Reference in New Issue