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:
Lucia V 2020-10-01 17:55:22 +02:00 committed by GitHub
parent 03fa92321f
commit 59ce5d0aff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 1938 additions and 992 deletions

View File

@ -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

View File

@ -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",

View File

@ -1,9 +0,0 @@
import Foundation
struct SimulatorDeviceAndRuntime: Hashable {
/// Device
let device: SimulatorDevice
/// Device's runtime.
let runtime: SimulatorRuntime
}

View File

@ -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)
}
}
}

View File

@ -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) })
}
}

View File

@ -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 {

View File

@ -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 {}
}
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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"
}
}
}

View File

@ -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()
}
}

View File

@ -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"))
}
}

View File

@ -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"))
}
}

View File

@ -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
}
}
}

View File

@ -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()
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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"
}
}
}

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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: ", "))]"
}
}

View File

@ -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)
}
}

View File

@ -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:

View File

@ -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()

View File

@ -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) }
}
}

View File

@ -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)
}

View File

@ -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) }

View File

@ -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
}

View File

@ -0,0 +1,9 @@
import Foundation
public struct SimulatorDeviceAndRuntime: Hashable {
/// Device
public let device: SimulatorDevice
/// Device's runtime.
public let runtime: SimulatorRuntime
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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)]) }
}

View File

@ -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,

View File

@ -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,

View File

@ -2,7 +2,7 @@ import Foundation
import RxSwift
import TuistSupport
@testable import TuistAutomation
@testable import TuistCore
@testable import TuistSupportTesting
public final class MockSimulatorController: SimulatorControlling {

View File

@ -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",

View File

@ -1,6 +1,6 @@
import Foundation
import TSCBasic
@testable import TuistAutomation
@testable import TuistCore
extension SimulatorDeviceAndRuntime {
static func test(device: SimulatorDevice = .test(),

View File

@ -1,6 +1,6 @@
import Foundation
import TSCBasic
@testable import TuistAutomation
@testable import TuistCore
extension SimulatorRuntime {
// swiftlint:disable:next line_length

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -9,7 +9,7 @@ protocol GenerateServiceProjectGeneratorFactorying {
final class GenerateServiceProjectGeneratorFactory: GenerateServiceProjectGeneratorFactorying {
func generator() -> ProjectGenerating {
ProjectGenerator(graphMapperProvider: GraphMapperProvider(cache: false, sources: Set()))
ProjectGenerator(graphMapperProvider: GraphMapperProvider())
}
}

View File

@ -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()) {

View File

@ -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)"
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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.

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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 files 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 files 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 files 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 {

View File

@ -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)
}
}

View File

@ -49,8 +49,6 @@ public final class TemporaryDirectory {
deinit {
if shouldRemoveTreeOnDeinit {
_ = try? FileManager.default.removeItem(atPath: path.pathString)
} else {
rmdir(path.pathString)
}
}
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -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"))))

View File

@ -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])
}

View File

@ -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])

View File

@ -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()

View File

@ -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 })

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -1,7 +1,7 @@
import Foundation
import XCTest
@testable import TuistAutomation
@testable import TuistCore
@testable import TuistSupportTesting
final class SimulatorRuntimeVersionTests: TuistUnitTestCase {

View File

@ -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)

View File

@ -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")

View File

@ -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] {
[]

View File

@ -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

View File

@ -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)
}
}

View File

@ -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") }))

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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())

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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")
])

View File

@ -1,7 +0,0 @@
import ProjectDescription
let workspace = Workspace(name: "Workspace",
projects: [
"App",
"Frameworks/**",
])