diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 086679213..663bc6661 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,3 +35,4 @@ jobs: uses: fortmarek/tapestry-action@0.1.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} diff --git a/.kodiak.toml b/.kodiak.toml index 2ebc2a05d..97bbbecfd 100644 --- a/.kodiak.toml +++ b/.kodiak.toml @@ -1 +1,8 @@ -version = 1 \ No newline at end of file +version = 1 + +[update] +always = true # default: false +require_automerge_label = false # default: true + +[approve] +auto_approve_usernames = ["dependabot"] \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eea331d5..8a53973a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/ ## Next +- Add `migration list-targets` command to show all targets sorted by number of dependencies [#1732](https://github.com/tuist/tuist/pull/1732) of a given project by [@andreacipriani](https://github.com/andreacipriani). + +## 1.23.0 + ### Added - Allow specifying Development Region via new `developmentRegion` parameter in `Config`s GenerationOption. [#1062](https://github.com/tuist/tuist/pull/1867) by [@svastven](https://github.com/svastven). @@ -27,6 +31,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/ - Some renames in the generation logic to make the generation logic easier to reason about [#1942](https://github.com/tuist/tuist/pull/1942) by [@pepibumur](https://github.com/pepibumur). - Update some Swift dependencies [#1971](https://github.com/tuist/tuist/pull/1971) by [@pepibumur](https://github.com/pepibumur). +- Improve hashing logic to account for files generated by mappers [#1977](https://github.com/tuist/tuist/pull/1977) by [@pepibumur](https://github.com/pepibumur). ## 1.22.0 - Heimat diff --git a/Gemfile b/Gemfile index f3651adda..9a165759c 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ gem "rake", "~> 13.0" gem "byebug", "~> 11.1" gem "minitest", "~> 5.14" gem "simctl", "~> 1.6" -gem "rubocop", "~> 1.0.0" +gem "rubocop", "~> 1.1.0" gem "encrypted-environment", "~> 0.2.0" gem "google-cloud-storage", "~> 1.29" gem "colorize", "~> 0.8.1" diff --git a/Gemfile.lock b/Gemfile.lock index 701984532..df31132d3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -171,16 +171,16 @@ GEM uber (< 0.2.0) retriable (3.1.2) rexml (3.2.4) - rubocop (1.0.0) + rubocop (1.1.0) parallel (~> 1.10) parser (>= 2.7.1.5) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8) rexml - rubocop-ast (>= 0.6.0) + rubocop-ast (>= 1.0.1) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (1.0.1) + rubocop-ast (1.1.0) parser (>= 2.7.1.5) ruby-macho (1.4.0) ruby-progressbar (1.10.1) @@ -223,7 +223,7 @@ DEPENDENCIES highline (~> 2.0) minitest (~> 5.14) rake (~> 13.0) - rubocop (~> 1.0.0) + rubocop (~> 1.1.0) rubyzip (~> 2.3.0) simctl (~> 1.6) xcodeproj (~> 1.19) diff --git a/Package.resolved b/Package.resolved index e2af31b5b..a90ba7bf1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -100,6 +100,15 @@ "version": "1.0.0" } }, + { + "package": "Queuer", + "repositoryURL": "https://github.com/FabrizioBrancati/Queuer.git", + "state": { + "branch": null, + "revision": "52515108d0ac4616d9e15ffcc7ad986e300d31ff", + "version": "2.1.1" + } + }, { "package": "RxSwift", "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", @@ -114,8 +123,8 @@ "repositoryURL": "https://github.com/kylef/Spectre.git", "state": { "branch": null, - "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", - "version": "0.9.0" + "revision": "f717bbce0e19f0129fc001b2b6bed43b70fd8b87", + "version": "0.9.1" } }, { @@ -123,7 +132,7 @@ "repositoryURL": "https://github.com/stencilproject/Stencil", "state": { "branch": "master", - "revision": "124df01d3c5defdce07872fe1828c764bb969b38", + "revision": "22440c53690c84603cea018a5204c0f1e770461d", "version": null } }, diff --git a/Package.swift b/Package.swift index e70783b30..e0cebff0f 100644 --- a/Package.swift +++ b/Package.swift @@ -43,11 +43,12 @@ let package = Package( .package(url: "https://github.com/tuist/GraphViz.git", .branch("tuist")), .package(url: "https://github.com/fortmarek/SwiftGen", .branch("stable")), .package(url: "https://github.com/fortmarek/StencilSwiftKit.git", .branch("stable")), + .package(url: "https://github.com/FabrizioBrancati/Queuer.git", .upToNextMajor(from: "2.0.0")), ], targets: [ .target( name: "TuistCore", - dependencies: ["SwiftToolsSupport-auto", "TuistSupport", "XcodeProj"] + dependencies: ["SwiftToolsSupport-auto", "TuistSupport", "XcodeProj", "Checksum"] ), .target( name: "TuistCoreTesting", @@ -75,11 +76,11 @@ let package = Package( ), .target( name: "TuistKit", - dependencies: ["XcodeProj", "SwiftToolsSupport-auto", "ArgumentParser", "TuistSupport", "TuistGenerator", "TuistCache", "TuistAutomation", "ProjectDescription", "Signals", "RxSwift", "RxBlocking", "Checksum", "TuistLoader", "TuistInsights", "TuistScaffold", "TuistSigning", "TuistDependencies", "TuistCloud", "TuistDoc", "GraphViz", "TuistMigration"] + dependencies: ["XcodeProj", "SwiftToolsSupport-auto", "ArgumentParser", "TuistSupport", "TuistGenerator", "TuistCache", "TuistAutomation", "ProjectDescription", "Signals", "RxSwift", "RxBlocking", "TuistLoader", "TuistInsights", "TuistScaffold", "TuistSigning", "TuistDependencies", "TuistCloud", "TuistDoc", "GraphViz", "TuistMigration", "TuistAsyncQueue"] ), .testTarget( name: "TuistKitTests", - dependencies: ["TuistKit", "TuistAutomation", "TuistSupportTesting", "TuistCoreTesting", "ProjectDescription", "RxBlocking", "TuistLoaderTesting", "TuistCacheTesting", "TuistGeneratorTesting", "TuistScaffoldTesting", "TuistCloudTesting", "TuistAutomationTesting", "TuistSigningTesting", "TuistDependenciesTesting", "TuistMigrationTesting", "TuistDocTesting"] + dependencies: ["TuistKit", "TuistAutomation", "TuistSupportTesting", "TuistCoreTesting", "ProjectDescription", "RxBlocking", "TuistLoaderTesting", "TuistCacheTesting", "TuistGeneratorTesting", "TuistScaffoldTesting", "TuistCloudTesting", "TuistAutomationTesting", "TuistSigningTesting", "TuistDependenciesTesting", "TuistMigrationTesting", "TuistDocTesting", "TuistAsyncQueueTesting"] ), .testTarget( name: "TuistKitIntegrationTests", @@ -143,7 +144,7 @@ let package = Package( ), .target( name: "TuistCache", - dependencies: ["XcodeProj", "SwiftToolsSupport-auto", "TuistCore", "TuistSupport", "Checksum", "RxSwift"] + dependencies: ["XcodeProj", "SwiftToolsSupport-auto", "TuistCore", "TuistSupport", "RxSwift"] ), .testTarget( name: "TuistCacheTests", @@ -155,7 +156,7 @@ let package = Package( ), .target( name: "TuistCloud", - dependencies: ["XcodeProj", "SwiftToolsSupport-auto", "TuistCore", "TuistSupport", "Checksum", "RxSwift"] + dependencies: ["XcodeProj", "SwiftToolsSupport-auto", "TuistCore", "TuistSupport", "RxSwift"] ), .testTarget( name: "TuistCloudTests", @@ -261,6 +262,18 @@ let package = Package( name: "TuistMigrationIntegrationTests", dependencies: ["TuistMigration", "TuistSupportTesting", "TuistCoreTesting", "TuistMigrationTesting"] ), + .target( + name: "TuistAsyncQueue", + dependencies: ["TuistCore", "TuistSupport", "XcodeProj", "SwiftToolsSupport-auto", "Queuer"] + ), + .target( + name: "TuistAsyncQueueTesting", + dependencies: ["TuistAsyncQueue"] + ), + .testTarget( + name: "TuistAsyncQueueTests", + dependencies: ["TuistAsyncQueue", "TuistSupportTesting", "TuistCoreTesting", "TuistAsyncQueueTesting", "RxBlocking"] + ), .target( name: "TuistLoader", dependencies: ["XcodeProj", "SwiftToolsSupport-auto", "TuistCore", "TuistSupport", "ProjectDescription"] diff --git a/Rakefile b/Rakefile index 2a1d800d9..42bf2f7c6 100644 --- a/Rakefile +++ b/Rakefile @@ -81,9 +81,9 @@ task :local_package do end desc("Builds, archives, and publishes tuist and tuistenv for release") -task :release do +task :release, [:version] do |task, options| decrypt_secrets - release + release(options[:version]) end desc("Publishes the installation scripts") @@ -249,8 +249,12 @@ def package FileUtils.cp(".build/release/tuistenv.zip", "build/tuistenv.zip") end -def release - version = cli.ask("Introduce the released version:") +def release(version) + if version == nil + version = cli.ask("Introduce the released version:") + end + + puts "Releasing #{version} 🚀" package diff --git a/Sources/ProjectDescription/Dependency.swift b/Sources/ProjectDescription/Dependency.swift index 24786ff12..4d47e113a 100644 --- a/Sources/ProjectDescription/Dependency.swift +++ b/Sources/ProjectDescription/Dependency.swift @@ -8,9 +8,13 @@ public struct Dependency: Codable, Equatable { /// Type of requirement for the given dependency let requirement: Dependency.Requirement - public init(name: String, requirement: Dependency.Requirement) { + /// Dependecy manager used to retrieve the dependecy + public let manager: Dependency.Manager + + public init(name: String, requirement: Dependency.Requirement, manager: Dependency.Manager) { self.name = name self.requirement = requirement + self.manager = manager } /// Carthage dependency initailizer @@ -18,7 +22,7 @@ public struct Dependency: Codable, Equatable { /// - Parameter requirement: Type of requirement for the given dependency /// - Returns Dependency public static func carthage(name: String, requirement: Dependency.Requirement) -> Dependency { - Dependency(name: name, requirement: requirement) + Dependency(name: name, requirement: requirement, manager: .carthage) } public static func == (lhs: Dependency, rhs: Dependency) -> Bool { @@ -27,7 +31,7 @@ public struct Dependency: Codable, Equatable { } public struct Dependencies: Codable, Equatable { - private let dependencies: [Dependency] + public let dependencies: [Dependency] public init(_ dependencies: [Dependency]) { self.dependencies = dependencies diff --git a/Sources/ProjectDescription/DependencyManager.swift b/Sources/ProjectDescription/DependencyManager.swift new file mode 100644 index 000000000..899e16b7c --- /dev/null +++ b/Sources/ProjectDescription/DependencyManager.swift @@ -0,0 +1,9 @@ +import Foundation + +public extension Dependency { + enum Manager: String, Codable, Equatable { + case carthage +// case spm +// case cocoapods + } +} diff --git a/Sources/TuistAsyncQueue/AsyncQueue.swift b/Sources/TuistAsyncQueue/AsyncQueue.swift new file mode 100644 index 000000000..937c848a4 --- /dev/null +++ b/Sources/TuistAsyncQueue/AsyncQueue.swift @@ -0,0 +1,162 @@ +import Foundation +import Queuer +import RxSwift +import TuistCore +import TuistSupport + +public protocol AsyncQueuing { + /// It dispatches the given event. + /// - Parameter event: Event to be dispatched. + func dispatch(event: T) +} + +public class AsyncQueue: AsyncQueuing { + // MARK: - Attributes + + public static var shared: AsyncQueuing! + private let disposeBag: DisposeBag = DisposeBag() + private let queue: Queuing + private let ciChecker: CIChecking + private let persistor: AsyncQueuePersisting + private let dispatchers: [String: AsyncQueueDispatching] + private let executionBlock: () throws -> Void + private let persistedEventsSchedulerType: SchedulerType + + // MARK: - Init + + public convenience init(dispatchers: [AsyncQueueDispatching], + executionBlock: @escaping () throws -> Void) throws + { + try self.init(queue: Queuer.shared, + executionBlock: executionBlock, + ciChecker: CIChecker(), + persistor: AsyncQueuePersistor(), + dispatchers: dispatchers) + } + + init(queue: Queuing, + executionBlock: @escaping () throws -> Void, + ciChecker: CIChecking, + persistor: AsyncQueuePersisting, + dispatchers: [AsyncQueueDispatching], + persistedEventsSchedulerType: SchedulerType = AsyncQueue.schedulerType()) throws + { + self.queue = queue + self.executionBlock = executionBlock + self.ciChecker = ciChecker + self.persistor = persistor + self.dispatchers = dispatchers.reduce(into: [String: AsyncQueueDispatching]()) { $0[$1.identifier] = $1 } + self.persistedEventsSchedulerType = persistedEventsSchedulerType + try run() + } + + // MARK: - AsyncQueuing + + public func dispatch(event: T) { + guard let dispatcher = dispatchers[event.dispatcherId] else { + logger.error("Couldn't find dispatcher with id: \(event.dispatcherId)") + return + } + + // We persist the event in case the dispatching is halted because Tuist's + // process exits. In that case we want to retry again the next time there's + // opportunity for that. + _ = persistor.write(event: event) + + // Queue event to send + let operation = liveDispatchOperation(event: event, dispatcher: dispatcher) + queue.addOperation(operation) + } + + // MARK: - Private + + private func liveDispatchOperation(event: T, dispatcher: AsyncQueueDispatching) -> Operation { + ConcurrentOperation(name: event.id.uuidString) { operation in + logger.debug("Dispatching event with ID '\(event.id.uuidString)' to '\(dispatcher.identifier)'") + + do { + try dispatcher.dispatch(event: event) + operation.success = true + + /// After the dispatching operation finishes, we delete the event locally. + _ = self.persistor.delete(event: event) + } catch { + operation.success = false + } + } + } + + private func dispatchPersisted(eventTuple: AsyncQueueEventTuple) { + guard let dispatcher = dispatchers.first(where: { $0.key == eventTuple.dispatcherId })?.value else { + deletePersistedEvent(filename: eventTuple.filename) + logger.error("Couldn't find dispatcher for persisted event with id: \(eventTuple.dispatcherId)") + return + } + + let operation = persistedDispatchOperation(event: eventTuple, dispatcher: dispatcher) + queue.addOperation(operation) + } + + private func persistedDispatchOperation(event: AsyncQueueEventTuple, + dispatcher: AsyncQueueDispatching) -> Operation + { + ConcurrentOperation(name: event.id.uuidString) { _ in + /// After the dispatching operation finishes, we delete the event locally. + defer { self.deletePersistedEvent(filename: event.filename) } + + do { + logger.debug("Dispatching persisted event with ID '\(event.id.uuidString)' to '\(dispatcher.identifier)'") + try dispatcher.dispatchPersisted(data: event.data) + } catch { + logger.debug("Failed to dispatch persisted event with ID '\(event.id.uuidString)' to '\(dispatcher.identifier)'") + } + } + } + + private func run() throws { + start() + do { + try executionBlock() + waitIfCI() + } catch { + waitIfCI() + throw error + } + } + + private func start() { + loadEvents() + queue.resume() + } + + private func waitIfCI() { + if !ciChecker.isCI() { return } + queue.waitUntilAllOperationsAreFinished() + } + + private func loadEvents() { + persistor + .readAll() + .subscribeOn(persistedEventsSchedulerType) + .subscribe(onSuccess: { events in + events.forEach(self.dispatchPersisted) + }, onError: { error in + logger.debug("Error loading persisted events: \(error)") + }) + .disposed(by: disposeBag) + } + + private func deletePersistedEvent(filename: String) { + persistor.delete(filename: filename).subscribe().disposed(by: disposeBag) + } + + // MARK: Private & Static + + private static func dispatchQueue() -> DispatchQueue { + DispatchQueue(label: "io.tuist.async-queue", qos: .background) + } + + private static func schedulerType() -> SchedulerType { + ConcurrentDispatchQueueScheduler(queue: dispatchQueue()) + } +} diff --git a/Sources/TuistAsyncQueue/AsyncQueuePersistor.swift b/Sources/TuistAsyncQueue/AsyncQueuePersistor.swift new file mode 100644 index 000000000..81c197dd6 --- /dev/null +++ b/Sources/TuistAsyncQueue/AsyncQueuePersistor.swift @@ -0,0 +1,108 @@ +import Foundation +import RxSwift +import TSCBasic +import TuistCore +import TuistSupport + +public typealias AsyncQueueEventTuple = (dispatcherId: String, id: UUID, date: Date, data: Data, filename: String) + +public protocol AsyncQueuePersisting { + /// Reads all the persisted events and returns them. + func readAll() -> Single<[AsyncQueueEventTuple]> + + /// Persiss a given event. + /// - Parameter event: Event to be persisted. + func write(event: T) -> Completable + + /// Deletes the given event from disk. + /// - Parameter event: Event to be deleted. + func delete(event: T) -> Completable + + /// Deletes the given file name from disk. + /// - Parameter filename: Name of the file to be deleted. + func delete(filename: String) -> Completable +} + +final class AsyncQueuePersistor: AsyncQueuePersisting { + // MARK: - Attributes + + let directory: AbsolutePath + let jsonEncoder: JSONEncoder = JSONEncoder() + + // MARK: - Init + + init(directory: AbsolutePath = Environment.shared.queueDirectory) { + self.directory = directory + } + + func write(event: T) -> Completable { + Completable.create { (observer) -> Disposable in + let path = self.directory.appending(component: self.filename(event: event)) + do { + let data = try self.jsonEncoder.encode(event) + try data.write(to: path.url) + observer(.completed) + } catch { + observer(.error(error)) + } + return Disposables.create() + } + } + + func delete(event: T) -> Completable { + delete(filename: filename(event: event)) + } + + func delete(filename: String) -> Completable { + Completable.create { (observer) -> Disposable in + let path = self.directory.appending(component: filename) + guard FileHandler.shared.exists(path) else { return Disposables.create() } + do { + try FileHandler.shared.delete(path) + observer(.completed) + } catch { + observer(.error(error)) + } + return Disposables.create() + } + } + + func readAll() -> Single<[AsyncQueueEventTuple]> { + Single.create { (observer) -> Disposable in + let paths = FileHandler.shared.glob(self.directory, glob: "*.json") + var events: [AsyncQueueEventTuple] = [] + paths.forEach { eventPath in + let fileName = eventPath.basenameWithoutExt + let components = fileName.split(separator: ".") + guard components.count == 3, + let timestamp = Double(components[0]), + let id = UUID(uuidString: String(components[2])) + else { + /// Changing the naming convention is a breaking change. When detected + /// we delete the event. + try? FileHandler.shared.delete(eventPath) + return + } + do { + let data = try Data(contentsOf: eventPath.url) + let event = (dispatcherId: String(components[1]), + id: id, + date: Date(timeIntervalSince1970: timestamp), + data: data, + filename: eventPath.basename) + events.append(event) + } catch { + try? FileHandler.shared.delete(eventPath) + } + } + observer(.success(events)) + return Disposables.create() + } + } + + // MARK: - Private + + private func filename(event: T) -> String { + "\(Int(event.date.timeIntervalSince1970)).\(event.dispatcherId).\(event.id.uuidString).json" + } +} diff --git a/Sources/TuistAsyncQueue/Log/Logger.swift b/Sources/TuistAsyncQueue/Log/Logger.swift new file mode 100644 index 000000000..bf424f36c --- /dev/null +++ b/Sources/TuistAsyncQueue/Log/Logger.swift @@ -0,0 +1,2 @@ +import TuistSupport +let logger = Logger(label: "io.tuist.async-queue") diff --git a/Sources/TuistAsyncQueue/Queuing.swift b/Sources/TuistAsyncQueue/Queuing.swift new file mode 100644 index 000000000..194d210b2 --- /dev/null +++ b/Sources/TuistAsyncQueue/Queuing.swift @@ -0,0 +1,10 @@ +import Foundation +import Queuer + +public protocol Queuing { + func addOperation(_ operation: Operation) + func resume() + func waitUntilAllOperationsAreFinished() +} + +extension Queuer: Queuing {} diff --git a/Sources/TuistAsyncQueueTesting/MockAsyncQueueDispatcher.swift b/Sources/TuistAsyncQueueTesting/MockAsyncQueueDispatcher.swift new file mode 100644 index 000000000..aabc3d0d9 --- /dev/null +++ b/Sources/TuistAsyncQueueTesting/MockAsyncQueueDispatcher.swift @@ -0,0 +1,59 @@ +import Foundation +import TuistAsyncQueue +import TuistCore + +public enum MockAsyncQueueDispatcherError: Error { + case dispatchError +} + +public class MockAsyncQueueDispatcher: AsyncQueueDispatching { + public init() {} + + public var invokedIdentifierGetter = false + public var invokedIdentifierGetterCount = 0 + public var stubbedIdentifier: String! = "" + + public var identifier: String { + invokedIdentifierGetter = true + invokedIdentifierGetterCount += 1 + return stubbedIdentifier + } + + public var invokedDispatch = false + public var invokedDispatchCallBack: () -> Void = {} + public var invokedDispatchCount = 0 + public var invokedDispatchParameterEvent: AsyncQueueEvent? + public var invokedDispatchParametersEventsList = [AsyncQueueEvent]() + public var stubbedDispatchError: Error? + + public func dispatch(event: AsyncQueueEvent) throws { + invokedDispatch = true + invokedDispatchCount += 1 + invokedDispatchParameterEvent = event + invokedDispatchParametersEventsList.append(event) + if let error = stubbedDispatchError { + invokedDispatchCallBack() + throw error + } + invokedDispatchCallBack() + } + + public var invokedDispatchPersisted = false + public var invokedDispatchPersistedCount = 0 + public var invokedDispatchPersistedCallBack: () -> Void = {} + public var invokedDispatchPersistedDataParameter: Data? + public var invokedDispatchPersistedParametersDataList = [Data]() + public var stubbedDispatchPersistedError: Error? + + public func dispatchPersisted(data: Data) throws { + invokedDispatchPersisted = true + invokedDispatchPersistedCount += 1 + invokedDispatchPersistedDataParameter = data + invokedDispatchPersistedParametersDataList.append(data) + if let error = stubbedDispatchPersistedError { + invokedDispatchPersistedCallBack() + throw error + } + invokedDispatchPersistedCallBack() + } +} diff --git a/Sources/TuistAsyncQueueTesting/MockAsyncQueuePersistor.swift b/Sources/TuistAsyncQueueTesting/MockAsyncQueuePersistor.swift new file mode 100644 index 000000000..0cfe69113 --- /dev/null +++ b/Sources/TuistAsyncQueueTesting/MockAsyncQueuePersistor.swift @@ -0,0 +1,64 @@ +import Foundation +import RxSwift +import TuistAsyncQueue +import TuistCore + +public final class MockAsyncQueuePersistor: AsyncQueuePersisting { + public init() {} + + public var invokedReadAll = false + public var invokedReadAllCount = 0 + public var stubbedReadAllResult: Single<[AsyncQueueEventTuple]> = Single.just([]) + + public func readAll() -> Single<[AsyncQueueEventTuple]> { + invokedReadAll = true + invokedReadAllCount += 1 + return stubbedReadAllResult + } + + public var invokedWrite = false + public var invokedWriteCount = 0 + public var invokedWriteEvent: U? + public var invokedWriteEvents = [U]() + public var stubbedWriteResult: Completable = .empty() + + public func write(event: T) -> Completable { + invokedWrite = true + invokedWriteCount += 1 + if let event = event as? U { + invokedWriteEvent = event + invokedWriteEvents.append(event) + } + return stubbedWriteResult + } + + public var invokedDeleteEventCount = 0 + public var invokedDeleteCallBack: () -> Void = {} + public var invokedDeleteEvent: U? + public var invokedDeleteEvents = [U]() + public var stubbedDeleteEventResult: Completable = .empty() + + public func delete(event: T) -> Completable { + invokedDeleteEventCount += 1 + if let event = event as? U { + invokedDeleteEvent = event + invokedDeleteEvents.append(event) + } + invokedDeleteCallBack() + return stubbedDeleteEventResult + } + + public var invokedDeleteFilename = false + public var invokedDeleteFilenameCount = 0 + public var invokedDeleteFilenameParameter: String? + public var invokedDeleteFilenameParametersList = [String]() + public var stubbedDeleteFilenameResult: Completable = .empty() + + public func delete(filename: String) -> Completable { + invokedDeleteFilename = true + invokedDeleteFilenameCount += 1 + invokedDeleteFilenameParameter = filename + invokedDeleteFilenameParametersList.append(filename) + return stubbedDeleteFilenameResult + } +} diff --git a/Sources/TuistAsyncQueueTesting/MockAsyncQueuer.swift b/Sources/TuistAsyncQueueTesting/MockAsyncQueuer.swift new file mode 100644 index 000000000..6bc615ffd --- /dev/null +++ b/Sources/TuistAsyncQueueTesting/MockAsyncQueuer.swift @@ -0,0 +1,19 @@ +import Foundation +import TuistAsyncQueue +import TuistCore + +public class MockAsyncQueuer: AsyncQueuing { + public init() {} + + public var invokedDispatch = false + public var invokedDispatchCount = 0 + public var invokedDispatchParameters: (event: Any, Void)? + public var invokedDispatchParametersList = [(event: Any, Void)]() + + public func dispatch(event: T) { + invokedDispatch = true + invokedDispatchCount += 1 + invokedDispatchParameters = (event, ()) + invokedDispatchParametersList.append((event, ())) + } +} diff --git a/Sources/TuistAsyncQueueTesting/MockQueuer.swift b/Sources/TuistAsyncQueueTesting/MockQueuer.swift new file mode 100644 index 000000000..da24730d2 --- /dev/null +++ b/Sources/TuistAsyncQueueTesting/MockQueuer.swift @@ -0,0 +1,34 @@ +import Foundation +import TuistAsyncQueue + +public final class MockQueuer: Queuing { + public init() {} + + public var invokedAddOperation = false + public var invokedAddOperationCount = 0 + public var invokedAddOperationParameterOperation: Operation? + public var invokedAddOperationParametersOperationsList = [Operation]() + + public func addOperation(_ operation: Operation) { + invokedAddOperation = true + invokedAddOperationCount += 1 + invokedAddOperationParameterOperation = operation + invokedAddOperationParametersOperationsList.append(operation) + } + + public var invokedResume = false + public var invokedResumeCount = 0 + + public func resume() { + invokedResume = true + invokedResumeCount += 1 + } + + public var invokedWaitUntilAllOperationsAreFinished = false + public var invokedWaitUntilAllOperationsAreFinishedCount = 0 + + public func waitUntilAllOperationsAreFinished() { + invokedWaitUntilAllOperationsAreFinished = true + invokedWaitUntilAllOperationsAreFinishedCount += 1 + } +} diff --git a/Sources/TuistCache/ContentHashing/CacheContentHasher.swift b/Sources/TuistCache/ContentHashing/CacheContentHasher.swift index b5a90cdf6..a4e92cfd1 100644 --- a/Sources/TuistCache/ContentHashing/CacheContentHasher.swift +++ b/Sources/TuistCache/ContentHashing/CacheContentHasher.swift @@ -1,5 +1,6 @@ import Foundation import TSCBasic +import TuistCore /// `CacheContentHasher` /// is a wrapper on top of `ContentHasher` that adds an in-memory cache to avoid re-computing the same hashes @@ -13,6 +14,10 @@ public final class CacheContentHasher: ContentHashing { self.contentHasher = contentHasher } + public func hash(_ data: Data) throws -> String { + try contentHasher.hash(data) + } + public func hash(_ string: String) throws -> String { try contentHasher.hash(string) } diff --git a/Sources/TuistCache/ContentHashing/GraphContentHasher.swift b/Sources/TuistCache/ContentHashing/GraphContentHasher.swift index ff31d62fb..caccb5253 100644 --- a/Sources/TuistCache/ContentHashing/GraphContentHasher.swift +++ b/Sources/TuistCache/ContentHashing/GraphContentHasher.swift @@ -1,4 +1,3 @@ -import Checksum import Foundation import TSCBasic import TuistCore @@ -16,9 +15,12 @@ public final class GraphContentHasher: GraphContentHashing { // MARK: - Init - public init( - targetContentHasher: TargetContentHashing = TargetContentHasher() - ) { + public convenience init(contentHasher: ContentHashing) { + let targetContentHasher = TargetContentHasher(contentHasher: contentHasher) + self.init(contentHasher: contentHasher, targetContentHasher: targetContentHasher) + } + + public init(contentHasher _: ContentHashing, targetContentHasher: TargetContentHashing) { self.targetContentHasher = targetContentHasher } diff --git a/Sources/TuistCache/ContentHashing/SourceFilesContentHasher.swift b/Sources/TuistCache/ContentHashing/SourceFilesContentHasher.swift index df8a0003f..0464dce49 100644 --- a/Sources/TuistCache/ContentHashing/SourceFilesContentHasher.swift +++ b/Sources/TuistCache/ContentHashing/SourceFilesContentHasher.swift @@ -2,7 +2,7 @@ import Foundation import TuistCore public protocol SourceFilesContentHashing { - func hash(sources: [Target.SourceFile]) throws -> String + func hash(sources: [SourceFile]) throws -> String } /// `SourceFilesContentHasher` @@ -21,14 +21,18 @@ public final class SourceFilesContentHasher: SourceFilesContentHashing { /// Returns a unique hash that identifies an arry of sourceFiles /// First it hashes the content of every file and append to every hash the compiler flags of the file. It assumes the files are always sorted the same way. /// Then it hashes again all partial hashes to get a unique identifier that represents a group of source files together with their compiler flags - public func hash(sources: [Target.SourceFile]) throws -> String { + public func hash(sources: [SourceFile]) throws -> String { var stringsToHash: [String] = [] for source in sources.sorted(by: { $0.path < $1.path }) { - var sourceHash = try contentHasher.hash(path: source.path) - if let compilerFlags = source.compilerFlags { - sourceHash += try contentHasher.hash(compilerFlags) + if let hash = source.contentHash { + stringsToHash.append(hash) + } else { + var sourceHash = try contentHasher.hash(path: source.path) + if let compilerFlags = source.compilerFlags { + sourceHash += try contentHasher.hash(compilerFlags) + } + stringsToHash.append(sourceHash) } - stringsToHash.append(sourceHash) } return try contentHasher.hash(stringsToHash) } diff --git a/Sources/TuistCache/ContentHashing/TargetContentHasher.swift b/Sources/TuistCache/ContentHashing/TargetContentHasher.swift index 8d5f46bb1..c83a7cc46 100644 --- a/Sources/TuistCache/ContentHashing/TargetContentHasher.swift +++ b/Sources/TuistCache/ContentHashing/TargetContentHasher.swift @@ -24,7 +24,7 @@ public final class TargetContentHasher: TargetContentHashing { // MARK: - Init - public convenience init(contentHasher: ContentHashing = CacheContentHasher()) { + public convenience init(contentHasher: ContentHashing) { self.init( contentHasher: contentHasher, sourceFilesContentHasher: SourceFilesContentHasher(contentHasher: contentHasher), diff --git a/Sources/TuistCache/GraphMappers/CacheMapper.swift b/Sources/TuistCache/GraphMappers/CacheMapper.swift index 064910f44..7d71838d2 100644 --- a/Sources/TuistCache/GraphMappers/CacheMapper.swift +++ b/Sources/TuistCache/GraphMappers/CacheMapper.swift @@ -34,11 +34,12 @@ public class CacheMapper: GraphMapping { public convenience init(config: Config, cacheStorageProvider: CacheStorageProviding, sources: Set, - cacheOutputType: CacheOutputType) + cacheOutputType: CacheOutputType, + contentHasher: ContentHashing) { self.init(config: config, cache: Cache(storageProvider: cacheStorageProvider), - graphContentHasher: GraphContentHasher(), + graphContentHasher: GraphContentHasher(contentHasher: contentHasher), sources: sources, cacheOutputType: cacheOutputType) } diff --git a/Sources/TuistCore/AsyncQueue/AsyncQueueDispatching.swift b/Sources/TuistCore/AsyncQueue/AsyncQueueDispatching.swift new file mode 100644 index 000000000..4bee3af34 --- /dev/null +++ b/Sources/TuistCore/AsyncQueue/AsyncQueueDispatching.swift @@ -0,0 +1,16 @@ +import Foundation +import RxSwift + +/// Async queue dispatcher. +public protocol AsyncQueueDispatching { + /// Identifier. + var identifier: String { get } + + /// Dispatches a given event. + /// - Parameter event: Event to be dispatched. + func dispatch(event: AsyncQueueEvent) throws + + /// Dispatch a persisted event. + /// - Parameter data: Serialized data of the event. + func dispatchPersisted(data: Data) throws +} diff --git a/Sources/TuistCore/AsyncQueue/AsyncQueueEvent.swift b/Sources/TuistCore/AsyncQueue/AsyncQueueEvent.swift new file mode 100644 index 000000000..1de6f5fa8 --- /dev/null +++ b/Sources/TuistCore/AsyncQueue/AsyncQueueEvent.swift @@ -0,0 +1,27 @@ +import Foundation + +public protocol AsyncQueueEvent: Codable { + /// Unique identifier. + var id: UUID { get } + + /// The identifier of the dispatcher that should process this event. + var dispatcherId: String { get } + + /// Event date. + var date: Date { get } +} + +public struct AnyAsyncQueueEvent: AsyncQueueEvent { + public let id: UUID + public let dispatcherId: String + public let date: Date + + public init(id: UUID = UUID(), + dispatcherId: String, + date: Date = Date()) + { + self.id = id + self.dispatcherId = dispatcherId + self.date = date + } +} diff --git a/Sources/TuistCore/Cache/FileContentHashing.swift b/Sources/TuistCore/Cache/FileContentHashing.swift new file mode 100644 index 000000000..8795493f4 --- /dev/null +++ b/Sources/TuistCore/Cache/FileContentHashing.swift @@ -0,0 +1,6 @@ +import Foundation +import TSCBasic + +public protocol FileContentHashing { + func hash(path: AbsolutePath) throws -> String +} diff --git a/Sources/TuistCache/ContentHashing/ContentHasher.swift b/Sources/TuistCore/ContentHashing/ContentHasher.swift similarity index 86% rename from Sources/TuistCache/ContentHashing/ContentHasher.swift rename to Sources/TuistCore/ContentHashing/ContentHasher.swift index 3b2717e7b..08ee487af 100644 --- a/Sources/TuistCache/ContentHashing/ContentHasher.swift +++ b/Sources/TuistCore/ContentHashing/ContentHasher.swift @@ -1,18 +1,8 @@ +import Checksum import Foundation import TSCBasic -import TuistCore import TuistSupport -public protocol FileContentHashing { - func hash(path: AbsolutePath) throws -> String -} - -public protocol ContentHashing: FileContentHashing { - func hash(_ string: String) throws -> String - func hash(_ strings: [String]) throws -> String - func hash(_ dictionary: [String: String]) throws -> String -} - /// `ContentHasher` /// is the single source of truth for hashing content. /// It uses md5 checksum to uniquely hash strings and data @@ -26,6 +16,13 @@ public final class ContentHasher: ContentHashing { // MARK: - ContentHashing + public func hash(_ data: Data) throws -> String { + guard let hash = data.checksum(algorithm: .md5) else { + throw ContentHashingError.dataHashingFailed + } + return hash + } + public func hash(_ string: String) throws -> String { guard let hash = string.checksum(algorithm: .md5) else { throw ContentHashingError.stringHashingFailed(string) diff --git a/Sources/TuistCore/ContentHashing/ContentHashing.swift b/Sources/TuistCore/ContentHashing/ContentHashing.swift new file mode 100644 index 000000000..8aae45e16 --- /dev/null +++ b/Sources/TuistCore/ContentHashing/ContentHashing.swift @@ -0,0 +1,8 @@ +import Foundation + +public protocol ContentHashing: FileContentHashing { + func hash(_ data: Data) throws -> String + func hash(_ string: String) throws -> String + func hash(_ strings: [String]) throws -> String + func hash(_ dictionary: [String: String]) throws -> String +} diff --git a/Sources/TuistCache/ContentHashing/ContentHashingError.swift b/Sources/TuistCore/ContentHashing/ContentHashingError.swift similarity index 73% rename from Sources/TuistCache/ContentHashing/ContentHashingError.swift rename to Sources/TuistCore/ContentHashing/ContentHashingError.swift index 2e42a1155..e70e40db6 100644 --- a/Sources/TuistCache/ContentHashing/ContentHashingError.swift +++ b/Sources/TuistCore/ContentHashing/ContentHashingError.swift @@ -1,20 +1,20 @@ import Foundation import TSCBasic -import TuistCore import TuistSupport /// `ContentHashingError` /// defines all the errors that can happen while cashing the content of a target -enum ContentHashingError: FatalError, Equatable { +public enum ContentHashingError: FatalError, Equatable { case failedToReadFile(AbsolutePath) case fileHashingFailed(AbsolutePath) case stringHashingFailed(String) + case dataHashingFailed - var type: ErrorType { + public var type: ErrorType { .abort } - var description: String { + public var description: String { switch self { case let .failedToReadFile(path): return "Couldn't find file to calculate hash at path \(path.pathString)" @@ -22,10 +22,12 @@ enum ContentHashingError: FatalError, Equatable { return "Couldn't calculate hash of file at path \(path.pathString)" case let .stringHashingFailed(string): return "Couldn't calculate hash of string \(string) for caching." + case .dataHashingFailed: + return "Couldn't get the hash of a data object." } } - static func == (lhs: ContentHashingError, rhs: ContentHashingError) -> Bool { + public static func == (lhs: ContentHashingError, rhs: ContentHashingError) -> Bool { switch (lhs, rhs) { case let (.failedToReadFile(lhsPath), .failedToReadFile(rhsPath)): return lhsPath == rhsPath @@ -33,6 +35,8 @@ enum ContentHashingError: FatalError, Equatable { return lhsPath == rhsPath case let (.stringHashingFailed(lhsPath), .stringHashingFailed(rhsPath)): return lhsPath == rhsPath + case (.dataHashingFailed, .dataHashingFailed): + return true default: return false } diff --git a/Sources/TuistCore/Models/SourceFile.swift b/Sources/TuistCore/Models/SourceFile.swift new file mode 100644 index 000000000..b4bd1538b --- /dev/null +++ b/Sources/TuistCore/Models/SourceFile.swift @@ -0,0 +1,34 @@ +import Foundation +import TSCBasic + +/// A type that represents a source file. +public struct SourceFile: ExpressibleByStringLiteral, Equatable { + /// Source file path. + public var path: AbsolutePath + + /// Compiler flags + /// When source files are added to a target, they can contain compiler flags that Xcode's build system + /// passes to the compiler when compiling those files. By default none is passed. + public var compilerFlags: String? + + /// This is intended to be used by the mappers that generate files through side effects. + /// This attribute is used by the content hasher used by the caching functionality. + public var contentHash: String? + + public init(path: AbsolutePath, + compilerFlags: String? = nil, + contentHash: String? = nil) + { + self.path = path + self.compilerFlags = compilerFlags + self.contentHash = contentHash + } + + // MARK: - ExpressibleByStringLiteral + + public init(stringLiteral value: String) { + path = AbsolutePath(value) + compilerFlags = nil + contentHash = nil + } +} diff --git a/Sources/TuistCore/Models/SourceFileGlob.swift b/Sources/TuistCore/Models/SourceFileGlob.swift new file mode 100644 index 000000000..2c50e51bb --- /dev/null +++ b/Sources/TuistCore/Models/SourceFileGlob.swift @@ -0,0 +1,27 @@ +import Foundation + +/// A type that represents a list of source files defined by a glob. +public struct SourceFileGlob: Equatable { + /// Glob pattern to unfold all the source files. + public var glob: String + + /// Glob pattern used for filtering out files + public var excluding: [String] + + /// Compiler flags. + public var compilerFlags: String? + + /// Initializes the source file glob. + /// - Parameters: + /// - glob: Glob pattern to unfold all the source files. + /// - excluding: Glob pattern used for filtering out files. + /// - compilerFlags: Compiler flags. + public init(glob: String, + excluding: [String] = [], + compilerFlags: String? = nil) + { + self.glob = glob + self.excluding = excluding + self.compilerFlags = compilerFlags + } +} diff --git a/Sources/TuistCore/Models/Target.swift b/Sources/TuistCore/Models/Target.swift index ad1bd3b43..b25039cc5 100644 --- a/Sources/TuistCore/Models/Target.swift +++ b/Sources/TuistCore/Models/Target.swift @@ -16,9 +16,6 @@ public enum TargetError: FatalError, Equatable { } public struct Target: Equatable, Hashable, Comparable { - public typealias SourceFile = (path: AbsolutePath, compilerFlags: String?) - public typealias SourceFileGlob = (glob: String, excluding: [String], compilerFlags: String?) - // MARK: - Static public static let validSourceExtensions: [String] = ["m", "swift", "mm", "cpp", "c", "d", "intentdefinition", "xcmappingmodel", "metal"] @@ -197,8 +194,8 @@ public struct Target: Equatable, Hashable, Comparable { /// This method unfolds the source file globs subtracting the paths that are excluded and ignoring /// the files that don't have a supported source extension. /// - Parameter sources: List of source file glob to be unfolded. - public static func sources(targetName: String, sources: [SourceFileGlob]) throws -> [TuistCore.Target.SourceFile] { - var sourceFiles: [AbsolutePath: TuistCore.Target.SourceFile] = [:] + public static func sources(targetName: String, sources: [SourceFileGlob]) throws -> [TuistCore.SourceFile] { + var sourceFiles: [AbsolutePath: TuistCore.SourceFile] = [:] var invalidGlobs: [InvalidGlob] = [] try sources.forEach { source in @@ -229,7 +226,7 @@ public struct Target: Equatable, Hashable, Comparable { return true } return false - }.forEach { sourceFiles[$0] = (path: $0, compilerFlags: source.compilerFlags) } + }.forEach { sourceFiles[$0] = SourceFile(path: $0, compilerFlags: source.compilerFlags) } } if !invalidGlobs.isEmpty { diff --git a/Sources/TuistCoreTesting/AsyncQueue/MockAsyncQueueDispatcher.swift b/Sources/TuistCoreTesting/AsyncQueue/MockAsyncQueueDispatcher.swift new file mode 100644 index 000000000..fd0a5119a --- /dev/null +++ b/Sources/TuistCoreTesting/AsyncQueue/MockAsyncQueueDispatcher.swift @@ -0,0 +1,48 @@ +import Foundation +import TuistCore + +public final class MockAsyncQueueDispatcher: AsyncQueueDispatching { + init() {} + + public var invokedIdentifierGetter = false + public var invokedIdentifierGetterCount = 0 + public var stubbedIdentifier: String! = "" + + public var identifier: String { + invokedIdentifierGetter = true + invokedIdentifierGetterCount += 1 + return stubbedIdentifier + } + + public var invokedDispatch = false + public var invokedDispatchCount = 0 + public var invokedDispatchParameters: (event: AsyncQueueEvent, Void)? + public var invokedDispatchParametersList = [(event: AsyncQueueEvent, Void)]() + public var stubbedDispatchError: Error? + + public func dispatch(event: AsyncQueueEvent) throws { + invokedDispatch = true + invokedDispatchCount += 1 + invokedDispatchParameters = (event, ()) + invokedDispatchParametersList.append((event, ())) + if let error = stubbedDispatchError { + throw error + } + } + + public var invokedDispatchPersisted = false + public var invokedDispatchPersistedCount = 0 + public var invokedDispatchPersistedParameters: (data: Data, Void)? + public var invokedDispatchPersistedParametersList = [(data: Data, Void)]() + public var stubbedDispatchPersistedError: Error? + + public func dispatchPersisted(data: Data) throws { + invokedDispatchPersisted = true + invokedDispatchPersistedCount += 1 + invokedDispatchPersistedParameters = (data, ()) + invokedDispatchPersistedParametersList.append((data, ())) + if let error = stubbedDispatchPersistedError { + throw error + } + } +} diff --git a/Sources/TuistCacheTesting/ContentHashing/Mocks/MockContentHashing.swift b/Sources/TuistCoreTesting/Cache/MockContentHashing.swift similarity index 80% rename from Sources/TuistCacheTesting/ContentHashing/Mocks/MockContentHashing.swift rename to Sources/TuistCoreTesting/Cache/MockContentHashing.swift index 84a49d2e3..0505826dd 100644 --- a/Sources/TuistCacheTesting/ContentHashing/Mocks/MockContentHashing.swift +++ b/Sources/TuistCoreTesting/Cache/MockContentHashing.swift @@ -1,10 +1,18 @@ import Foundation import TSCBasic -import TuistCache +import TuistCore public class MockContentHashing: ContentHashing { public init() {} + public var hashDataSpy: Data? + public var hashDataCallCount = 0 + public func hash(_ data: Data) throws -> String { + hashDataSpy = data + hashDataCallCount += 1 + return "\(String(describing: hashDataSpy?.base64EncodedString()))-hash" + } + public var hashStringSpy: String? public var hashStringCallCount = 0 public func hash(_ string: String) throws -> String { diff --git a/Sources/TuistCoreTesting/Models/Target+TestData.swift b/Sources/TuistCoreTesting/Models/Target+TestData.swift index fef3bc402..34df70ec8 100644 --- a/Sources/TuistCoreTesting/Models/Target+TestData.swift +++ b/Sources/TuistCoreTesting/Models/Target+TestData.swift @@ -14,7 +14,7 @@ public extension Target { infoPlist: InfoPlist? = nil, entitlements: AbsolutePath? = nil, settings: Settings? = Settings.test(), - sources: [Target.SourceFile] = [], + sources: [SourceFile] = [], resources: [FileElement] = [], coreDataModels: [CoreDataModel] = [], headers: Headers? = nil, @@ -54,7 +54,7 @@ public extension Target { infoPlist: InfoPlist? = nil, entitlements: AbsolutePath? = nil, settings: Settings? = nil, - sources: [Target.SourceFile] = [], + sources: [SourceFile] = [], resources: [FileElement] = [], coreDataModels: [CoreDataModel] = [], headers: Headers? = nil, diff --git a/Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift b/Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift index 63b4070d7..5c098033f 100644 --- a/Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift +++ b/Sources/TuistGenerator/Generator/BuildPhaseGenerator.swift @@ -152,7 +152,7 @@ final class BuildPhaseGenerator: BuildPhaseGenerating { } } - func generateSourcesBuildPhase(files: [Target.SourceFile], + func generateSourcesBuildPhase(files: [SourceFile], coreDataModels: [CoreDataModel], pbxTarget: PBXTarget, fileElements: ProjectFileElements, diff --git a/Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift b/Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift index c06b6b147..1f7345e9b 100644 --- a/Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/ResourcesProjectMapper.swift @@ -44,7 +44,7 @@ public class ResourcesProjectMapper: ProjectMapping { if target.supportsSources { let (filePath, fileDescriptors) = synthesizedFile(bundleName: bundleName, target: target, project: project) - modifiedTarget.sources.append((path: filePath, compilerFlags: nil)) + modifiedTarget.sources.append(SourceFile(path: filePath, compilerFlags: nil)) sideEffects.append(contentsOf: fileDescriptors) } diff --git a/Sources/TuistGenerator/Mappers/SynthesizedResourceInterfaceProjectMapper.swift b/Sources/TuistGenerator/Mappers/SynthesizedResourceInterfaceProjectMapper.swift index 8a5e05c13..8632ea219 100644 --- a/Sources/TuistGenerator/Mappers/SynthesizedResourceInterfaceProjectMapper.swift +++ b/Sources/TuistGenerator/Mappers/SynthesizedResourceInterfaceProjectMapper.swift @@ -6,15 +6,19 @@ import TuistSupport /// A project mapper that synthezies resource interfaces public final class SynthesizedResourceInterfaceProjectMapper: ProjectMapping { private let synthesizedResourceInterfacesGenerator: SynthesizedResourceInterfacesGenerating + private let contentHasher: ContentHashing - public convenience init() { - self.init(synthesizedResourceInterfacesGenerator: SynthesizedResourceInterfacesGenerator()) + public convenience init(contentHasher: ContentHashing) { + self.init(synthesizedResourceInterfacesGenerator: SynthesizedResourceInterfacesGenerator(), + contentHasher: contentHasher) } init( - synthesizedResourceInterfacesGenerator: SynthesizedResourceInterfacesGenerating + synthesizedResourceInterfacesGenerator: SynthesizedResourceInterfacesGenerating, + contentHasher: ContentHashing ) { self.synthesizedResourceInterfacesGenerator = synthesizedResourceInterfacesGenerator + self.contentHasher = contentHasher } public func map(project: Project) throws -> (Project, [SideEffectDescriptor]) { @@ -145,9 +149,11 @@ public final class SynthesizedResourceInterfaceProjectMapper: ProjectMapping { var target = target - target.sources += renderedResources - .map(\.path) - .map { (path: $0, compilerFlags: nil) } + target.sources += try renderedResources + .map { resource in + let hash = try resource.contents.map(contentHasher.hash) + return SourceFile(path: resource.path, contentHash: hash) + } let sideEffects = renderedResources .map { FileDescriptor(path: $0.path, contents: $0.contents) } diff --git a/Sources/TuistKit/AsyncQueue/AsyncQueue+Tuist.swift b/Sources/TuistKit/AsyncQueue/AsyncQueue+Tuist.swift new file mode 100644 index 000000000..04d55d4f8 --- /dev/null +++ b/Sources/TuistKit/AsyncQueue/AsyncQueue+Tuist.swift @@ -0,0 +1,8 @@ +import Foundation +import TuistAsyncQueue + +public extension AsyncQueue { + class func run(executionBlock: @escaping () throws -> Void) throws { + try AsyncQueue.shared = AsyncQueue(dispatchers: [], executionBlock: executionBlock) + } +} diff --git a/Sources/TuistKit/Cache/CacheController.swift b/Sources/TuistKit/Cache/CacheController.swift index 59b547819..c7237a0d1 100644 --- a/Sources/TuistKit/Cache/CacheController.swift +++ b/Sources/TuistKit/Cache/CacheController.swift @@ -13,8 +13,13 @@ import TuistSupport /// A provider that concatenates the default mappers, to the mapper that adds the build phase /// to locate the built products directory. class CacheControllerProjectMapperProvider: ProjectMapperProviding { + fileprivate let contentHasher: ContentHashing + init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + func mapper(config: Config) -> ProjectMapping { - let defaultProjectMapperProvider = ProjectMapperProvider() + let defaultProjectMapperProvider = ProjectMapperProvider(contentHasher: contentHasher) let defaultMapper = defaultProjectMapperProvider.mapper(config: config) return SequentialProjectMapper(mappers: [defaultMapper, CacheBuildPhaseProjectMapper()]) } @@ -28,10 +33,18 @@ protocol CacheControllerProjectGeneratorProviding { /// A provider that returns the project generator that should be used by the cache controller. class CacheControllerProjectGeneratorProvider: CacheControllerProjectGeneratorProviding { + fileprivate let contentHasher: ContentHashing + init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + func generator() -> Generating { - let projectMapperProvider = CacheControllerProjectMapperProvider() + let contentHasher = CacheContentHasher() + let projectMapperProvider = CacheControllerProjectMapperProvider(contentHasher: contentHasher) return Generator(projectMapperProvider: projectMapperProvider, - workspaceMapperProvider: WorkspaceMapperProvider(projectMapperProvider: projectMapperProvider)) + graphMapperProvider: GraphMapperProvider(), + workspaceMapperProvider: WorkspaceMapperProvider(contentHasher: contentHasher), + manifestLoaderFactory: ManifestLoaderFactory()) } } @@ -55,11 +68,14 @@ final class CacheController: CacheControlling { /// Cache. private let cache: CacheStoring - convenience init(cache: CacheStoring, artifactBuilder: CacheArtifactBuilding) { + convenience init(cache: CacheStoring, + artifactBuilder: CacheArtifactBuilding, + contentHasher: ContentHashing) + { self.init(cache: cache, artifactBuilder: artifactBuilder, - projectGeneratorProvider: CacheControllerProjectGeneratorProvider(), - graphContentHasher: GraphContentHasher()) + projectGeneratorProvider: CacheControllerProjectGeneratorProvider(contentHasher: contentHasher), + graphContentHasher: GraphContentHasher(contentHasher: contentHasher)) } init(cache: CacheStoring, diff --git a/Sources/TuistKit/Cache/CacheControllerFactory.swift b/Sources/TuistKit/Cache/CacheControllerFactory.swift index 9d9d004ce..a92b30f9d 100644 --- a/Sources/TuistKit/Cache/CacheControllerFactory.swift +++ b/Sources/TuistKit/Cache/CacheControllerFactory.swift @@ -1,5 +1,6 @@ import TuistAutomation import TuistCache +import TuistCore /// A factory that returns cache controllers for different type of pre-built artifacts. final class CacheControllerFactory { @@ -13,16 +14,18 @@ final class CacheControllerFactory { } /// Returns a cache controller that uses frameworks built for the simulator architecture. + /// - Parameter contentHasher: Content hasher. /// - Returns: A cache controller instance. - func makeForSimulatorFramework() -> CacheControlling { + func makeForSimulatorFramework(contentHasher: ContentHashing) -> CacheControlling { let frameworkBuilder = CacheFrameworkBuilder(xcodeBuildController: XcodeBuildController()) - return CacheController(cache: cache, artifactBuilder: frameworkBuilder) + return CacheController(cache: cache, artifactBuilder: frameworkBuilder, contentHasher: contentHasher) } /// Returns a cache controller that uses xcframeworks built for the simulator and device architectures. - /// - Returns: A cache controller instance. - func makeForXCFramework() -> CacheControlling { + /// - Parameter contentHasher: Content hasher. + /// - Returns: Instance of the cache controller. + func makeForXCFramework(contentHasher: ContentHashing) -> CacheControlling { let frameworkBuilder = CacheXCFrameworkBuilder(xcodeBuildController: XcodeBuildController()) - return CacheController(cache: cache, artifactBuilder: frameworkBuilder) + return CacheController(cache: cache, artifactBuilder: frameworkBuilder, contentHasher: contentHasher) } } diff --git a/Sources/TuistKit/Commands/Migration/MigrationTargetsByDependenciesCommand.swift b/Sources/TuistKit/Commands/Migration/MigrationTargetsByDependenciesCommand.swift new file mode 100644 index 000000000..89589be32 --- /dev/null +++ b/Sources/TuistKit/Commands/Migration/MigrationTargetsByDependenciesCommand.swift @@ -0,0 +1,22 @@ +import ArgumentParser +import Foundation +import TSCBasic +import TuistSupport + +struct MigrationTargetsByDependenciesCommand: ParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration(commandName: "list-targets", + abstract: "It lists the targets of a project sorted by number of dependencies.") + } + + @Option( + name: [.customShort("p"), .long], + help: "The path to the Xcode project", + completion: .directory + ) + var xcodeprojPath: String + + func run() throws { + try MigrationTargetsByDependenciesService().run(xcodeprojPath: AbsolutePath(xcodeprojPath, relativeTo: FileHandler.shared.currentPath)) + } +} diff --git a/Sources/TuistKit/Commands/MigrationCommand.swift b/Sources/TuistKit/Commands/MigrationCommand.swift index 6b7996300..190a9bb5b 100644 --- a/Sources/TuistKit/Commands/MigrationCommand.swift +++ b/Sources/TuistKit/Commands/MigrationCommand.swift @@ -8,6 +8,7 @@ struct MigrationCommand: ParsableCommand { abstract: "A set of utilities to assist on the migration of Xcode projects to Tuist.", subcommands: [ MigrationSettingsToXCConfigCommand.self, MigrationCheckEmptyBuildSettingsCommand.self, + MigrationTargetsByDependenciesCommand.self, ]) } } diff --git a/Sources/TuistKit/Generator/Generator.swift b/Sources/TuistKit/Generator/Generator.swift index 2eab9f031..01c2cfda1 100644 --- a/Sources/TuistKit/Generator/Generator.swift +++ b/Sources/TuistKit/Generator/Generator.swift @@ -34,10 +34,17 @@ class Generator: Generating { private let workspaceMapperProvider: WorkspaceMapperProviding private let manifestLoader: ManifestLoading - init(graphMapperProvider: GraphMapperProviding = GraphMapperProvider(), - projectMapperProvider: ProjectMapperProviding = ProjectMapperProvider(), - workspaceMapperProvider: WorkspaceMapperProviding = WorkspaceMapperProvider(), - manifestLoaderFactory: ManifestLoaderFactory = ManifestLoaderFactory()) + convenience init(contentHasher: ContentHashing) { + self.init(projectMapperProvider: ProjectMapperProvider(contentHasher: contentHasher), + graphMapperProvider: GraphMapperProvider(), + workspaceMapperProvider: WorkspaceMapperProvider(contentHasher: contentHasher), + manifestLoaderFactory: ManifestLoaderFactory()) + } + + init(projectMapperProvider: ProjectMapperProviding, + graphMapperProvider: GraphMapperProviding, + workspaceMapperProvider: WorkspaceMapperProviding, + manifestLoaderFactory: ManifestLoaderFactory) { let manifestLoader = manifestLoaderFactory.createManifestLoader() recursiveManifestLoader = RecursiveManifestLoader(manifestLoader: manifestLoader) diff --git a/Sources/TuistKit/GraphMappers/ProjectMapperProvider.swift b/Sources/TuistKit/GraphMappers/ProjectMapperProvider.swift index 99d88d4dc..25b20eb28 100644 --- a/Sources/TuistKit/GraphMappers/ProjectMapperProvider.swift +++ b/Sources/TuistKit/GraphMappers/ProjectMapperProvider.swift @@ -13,6 +13,15 @@ protocol ProjectMapperProviding { } final class ProjectMapperProvider: ProjectMapperProviding { + /// Content hasher. + private let contentHasher: ContentHashing + + /// Initializes the project mapper provider. + /// - Parameter contentHasher: Content hasher. + init(contentHasher: ContentHashing) { + self.contentHasher = contentHasher + } + func mapper(config: Config) -> ProjectMapping { var mappers: [ProjectMapping] = [] @@ -26,7 +35,7 @@ final class ProjectMapperProvider: ProjectMapperProviding { // Namespace generator if !config.generationOptions.contains(.disableSynthesizedResourceAccessors) { - mappers.append(SynthesizedResourceInterfaceProjectMapper()) + mappers.append(SynthesizedResourceInterfaceProjectMapper(contentHasher: contentHasher)) } // Logfile noise suppression diff --git a/Sources/TuistKit/GraphMappers/WorkspaceMapperProvider.swift b/Sources/TuistKit/GraphMappers/WorkspaceMapperProvider.swift index 118ee3d95..3fcc6d1b4 100644 --- a/Sources/TuistKit/GraphMappers/WorkspaceMapperProvider.swift +++ b/Sources/TuistKit/GraphMappers/WorkspaceMapperProvider.swift @@ -8,7 +8,12 @@ protocol WorkspaceMapperProviding { final class WorkspaceMapperProvider: WorkspaceMapperProviding { private let projectMapperProvider: ProjectMapperProviding - init(projectMapperProvider: ProjectMapperProviding = ProjectMapperProvider()) { + + convenience init(contentHasher: ContentHashing) { + self.init(projectMapperProvider: ProjectMapperProvider(contentHasher: contentHasher)) + } + + init(projectMapperProvider: ProjectMapperProviding) { self.projectMapperProvider = projectMapperProvider } diff --git a/Sources/TuistKit/ProjectEditor/ProjectEditor.swift b/Sources/TuistKit/ProjectEditor/ProjectEditor.swift index 9b0427931..23a84cf23 100644 --- a/Sources/TuistKit/ProjectEditor/ProjectEditor.swift +++ b/Sources/TuistKit/ProjectEditor/ProjectEditor.swift @@ -27,10 +27,10 @@ enum ProjectEditorError: FatalError, Equatable { protocol ProjectEditing: AnyObject { /// Generates an Xcode project to edit the Project defined in the given directory. /// - Parameters: - /// - at: Directory whose project will be edited. + /// - editingPath: Directory whose project will be edited. /// - destinationDirectory: Directory in which the Xcode project will be generated. /// - Returns: The path to the generated Xcode project. - func edit(at: AbsolutePath, in destinationDirectory: AbsolutePath) throws -> AbsolutePath + func edit(at editingPath: AbsolutePath, in destinationDirectory: AbsolutePath) throws -> AbsolutePath } final class ProjectEditor: ProjectEditing { @@ -87,36 +87,38 @@ final class ProjectEditor: ProjectEditing { self.sideEffectDescriptorExecutor = sideEffectDescriptorExecutor } - func edit(at: AbsolutePath, in dstDirectory: AbsolutePath) throws -> AbsolutePath { + func edit(at editingPath: AbsolutePath, in dstDirectory: AbsolutePath) throws -> AbsolutePath { let xcodeprojPath = dstDirectory.appending(component: "Manifests.xcodeproj") let projectDesciptionPath = try resourceLocator.projectDescription() - let manifests = manifestFilesLocator.locateAllProjectManifests(at: at) - let configPath = manifestFilesLocator.locateConfig(at: at) - let setupPath = manifestFilesLocator.locateSetup(at: at) + let manifests = manifestFilesLocator.locateAllProjectManifests(at: editingPath) + let configPath = manifestFilesLocator.locateConfig(at: editingPath) + let dependenciesPath = manifestFilesLocator.locateDependencies(at: editingPath) + let setupPath = manifestFilesLocator.locateSetup(at: editingPath) var helpers: [AbsolutePath] = [] - if let helpersDirectory = helpersDirectoryLocator.locate(at: at) { + if let helpersDirectory = helpersDirectoryLocator.locate(at: editingPath) { helpers = FileHandler.shared.glob(helpersDirectory, glob: "**/*.swift") } var templates: [AbsolutePath] = [] - if let templatesDirectory = templatesDirectoryLocator.locateUserTemplates(at: at) { + if let templatesDirectory = templatesDirectoryLocator.locateUserTemplates(at: editingPath) { templates = FileHandler.shared.glob(templatesDirectory, glob: "**/*.swift") + FileHandler.shared.glob(templatesDirectory, glob: "**/*.stencil") } /// We error if the user tries to edit a project in a directory where there are no editable files. if manifests.isEmpty, helpers.isEmpty, templates.isEmpty { - throw ProjectEditorError.noEditableFiles(at) + throw ProjectEditorError.noEditableFiles(editingPath) } // To be sure that we are using the same binary of Tuist that invoked `edit` let tuistPath = AbsolutePath(TuistCommand.processArguments()!.first!) let (project, graph) = try projectEditorMapper.map(tuistPath: tuistPath, - sourceRootPath: at, + sourceRootPath: editingPath, xcodeProjPath: xcodeprojPath, setupPath: setupPath, configPath: configPath, + dependenciesPath: dependenciesPath, manifests: manifests.map { $0.1 }, helpers: helpers, templates: templates, diff --git a/Sources/TuistKit/ProjectEditor/ProjectEditorMapper.swift b/Sources/TuistKit/ProjectEditor/ProjectEditorMapper.swift index 3cbd45b80..317ec6dfa 100644 --- a/Sources/TuistKit/ProjectEditor/ProjectEditorMapper.swift +++ b/Sources/TuistKit/ProjectEditor/ProjectEditorMapper.swift @@ -9,6 +9,7 @@ protocol ProjectEditorMapping: AnyObject { xcodeProjPath: AbsolutePath, setupPath: AbsolutePath?, configPath: AbsolutePath?, + dependenciesPath: AbsolutePath?, manifests: [AbsolutePath], helpers: [AbsolutePath], templates: [AbsolutePath], @@ -22,6 +23,7 @@ final class ProjectEditorMapper: ProjectEditorMapping { xcodeProjPath: AbsolutePath, setupPath: AbsolutePath?, configPath: AbsolutePath?, + dependenciesPath: AbsolutePath?, manifests: [AbsolutePath], helpers: [AbsolutePath], templates: [AbsolutePath], @@ -43,41 +45,42 @@ final class ProjectEditorMapper: ProjectEditorMapping { manifestsDependencies = [.target(name: "ProjectDescriptionHelpers")] } - let manifestsTargets = named(manifests: manifests).map { name, manifest in - Target(name: name, - platform: .macOS, - product: .staticFramework, - productName: name, - bundleId: "io.tuist.${PRODUCT_NAME:rfc1034identifier}", - settings: targetSettings, - sources: [(path: manifest, compilerFlags: nil)], - filesGroup: .group(name: "Manifests"), - dependencies: manifestsDependencies) + let manifestsTargets = named(manifests: manifests).map { name, manifestSourcePath in + editorHelperTarget(name: name, + targetSettings: targetSettings, + sourcePaths: [manifestSourcePath], + dependencies: manifestsDependencies) } var helpersTarget: Target? if !helpers.isEmpty { - helpersTarget = Target.editorHelperTarget(name: "ProjectDescriptionHelpers", - targetSettings: targetSettings, - sourcePaths: helpers) + helpersTarget = editorHelperTarget(name: "ProjectDescriptionHelpers", + targetSettings: targetSettings, + sourcePaths: helpers) } var templatesTarget: Target? if !templates.isEmpty { - templatesTarget = Target.editorHelperTarget(name: "Templates", - targetSettings: targetSettings, - sourcePaths: templates) + templatesTarget = editorHelperTarget(name: "Templates", + targetSettings: targetSettings, + sourcePaths: templates) } var setupTarget: Target? if let setupPath = setupPath { - setupTarget = Target.editorHelperTarget(name: "Setup", - targetSettings: targetSettings, - sourcePaths: [setupPath]) + setupTarget = editorHelperTarget(name: "Setup", + targetSettings: targetSettings, + sourcePaths: [setupPath]) } var configTarget: Target? if let configPath = configPath { - configTarget = Target.editorHelperTarget(name: "Config", - targetSettings: targetSettings, - sourcePaths: [configPath]) + configTarget = editorHelperTarget(name: "Config", + targetSettings: targetSettings, + sourcePaths: [configPath]) + } + var dependenciesTarget: Target? + if let dependenciesPath = dependenciesPath { + dependenciesTarget = editorHelperTarget(name: "Dependencies", + targetSettings: targetSettings, + sourcePaths: [dependenciesPath]) } var targets: [Target] = [] @@ -86,6 +89,7 @@ final class ProjectEditorMapper: ProjectEditorMapping { if let templatesTarget = templatesTarget { targets.append(templatesTarget) } if let setupTarget = setupTarget { targets.append(setupTarget) } if let configTarget = configTarget { targets.append(configTarget) } + if let dependenciesTarget = dependenciesTarget { targets.append(dependenciesTarget) } // Run Scheme let buildAction = BuildAction(targets: targets.map { TargetReference(projectPath: sourceRootPath, name: $0.name) }) @@ -126,6 +130,10 @@ final class ProjectEditorMapper: ProjectEditorMapping { let configNode = TargetNode(project: project, target: configTarget, dependencies: []) dependencies.append(configNode) } + if let dependenciesTarget = dependenciesTarget { + let dependenciesNode = TargetNode(project: project, target: dependenciesTarget, dependencies: []) + dependencies.append(dependenciesNode) + } let manifestTargetNodes = manifestsTargets.map { TargetNode(project: project, target: $0, dependencies: dependencies) } let workspace = Workspace(path: project.path, name: "Manifests", projects: [project.path]) @@ -149,7 +157,7 @@ final class ProjectEditorMapper: ProjectEditorMapping { /// It returns the build settings that should be used in the manifests target. /// - Parameter projectDescriptionPath: Path to the ProjectDescription framework. /// - Parameter swiftVersion: The system's Swift version. - fileprivate func settings(projectDescriptionPath: AbsolutePath, swiftVersion: String) -> SettingsDictionary { + private func settings(projectDescriptionPath: AbsolutePath, swiftVersion: String) -> SettingsDictionary { let frameworkParentDirectory = projectDescriptionPath.parentDirectory var buildSettings = SettingsDictionary() buildSettings["FRAMEWORK_SEARCH_PATHS"] = .string(frameworkParentDirectory.pathString) @@ -162,7 +170,7 @@ final class ProjectEditorMapper: ProjectEditorMapping { /// It returns a dictionary with unique name as key for each Manifest file /// - Parameter manifests: Manifest files to assign an unique name /// - Returns: Dictionary composed by unique name as key and Manifest file as value. - fileprivate func named(manifests: [AbsolutePath]) -> [String: AbsolutePath] { + private func named(manifests: [AbsolutePath]) -> [String: AbsolutePath] { manifests.reduce(into: [String: AbsolutePath]()) { result, manifest in var name = "\(manifest.parentDirectory.basename)Manifests" while result[name] != nil { @@ -171,21 +179,23 @@ final class ProjectEditorMapper: ProjectEditorMapping { result[name] = manifest } } -} -private extension Target { - /// Target for edit project - static func editorHelperTarget(name: String, - targetSettings: Settings, - sourcePaths: [AbsolutePath]) -> Target - { + /// It returns a target for edit project. + /// - Parameters: + /// - name: Name for the target. + /// - targetSettings: Target's settings. + /// - sourcePaths: Target's sources. + /// - dependencies: Target's dependencies. + /// - Returns: Target for edit project. + private func editorHelperTarget(name: String, targetSettings: Settings, sourcePaths: [AbsolutePath], dependencies: [Dependency] = []) -> Target { Target(name: name, platform: .macOS, product: .staticFramework, productName: name, bundleId: "io.tuist.${PRODUCT_NAME:rfc1034identifier}", settings: targetSettings, - sources: sourcePaths.map { (path: $0, compilerFlags: nil) }, - filesGroup: .group(name: "Manifests")) + sources: sourcePaths.map { SourceFile(path: $0, compilerFlags: nil) }, + filesGroup: .group(name: "Manifests"), + dependencies: dependencies) } } diff --git a/Sources/TuistKit/Services/BuildService.swift b/Sources/TuistKit/Services/BuildService.swift index 587fa0d5a..be2e93458 100644 --- a/Sources/TuistKit/Services/BuildService.swift +++ b/Sources/TuistKit/Services/BuildService.swift @@ -2,6 +2,7 @@ import Foundation import RxBlocking import TSCBasic import TuistAutomation +import TuistCache import TuistCore import TuistSupport @@ -40,7 +41,7 @@ final class BuildService { /// Build graph inspector. let buildGraphInspector: BuildGraphInspecting - init(generator: Generating = Generator(), + init(generator: Generating = Generator(contentHasher: CacheContentHasher()), xcodebuildController: XcodeBuildControlling = XcodeBuildController(), buildGraphInspector: BuildGraphInspecting = BuildGraphInspector()) { diff --git a/Sources/TuistKit/Services/Cache/CachePrintHashesService.swift b/Sources/TuistKit/Services/Cache/CachePrintHashesService.swift index 5585aea57..47357c864 100644 --- a/Sources/TuistKit/Services/Cache/CachePrintHashesService.swift +++ b/Sources/TuistKit/Services/Cache/CachePrintHashesService.swift @@ -12,10 +12,13 @@ final class CachePrintHashesService { let graphContentHasher: GraphContentHashing private let clock: Clock - init(generator: Generating = Generator(), - graphContentHasher: GraphContentHashing = GraphContentHasher(), - clock: Clock = WallClock()) - { + convenience init(contentHasher: ContentHashing = CacheContentHasher()) { + self.init(generator: Generator(contentHasher: contentHasher), + graphContentHasher: GraphContentHasher(contentHasher: contentHasher), + clock: WallClock()) + } + + init(generator: Generating, graphContentHasher: GraphContentHashing, clock: Clock) { self.generator = generator self.graphContentHasher = graphContentHasher self.clock = clock diff --git a/Sources/TuistKit/Services/Cache/CacheWarmService.swift b/Sources/TuistKit/Services/Cache/CacheWarmService.swift index c0d50fca5..b16a28951 100644 --- a/Sources/TuistKit/Services/Cache/CacheWarmService.swift +++ b/Sources/TuistKit/Services/Cache/CacheWarmService.swift @@ -21,9 +21,9 @@ final class CacheWarmService { let config = try generatorModelLoader.loadConfig(at: currentPath) let cache = Cache(storageProvider: CacheStorageProvider(config: config)) let cacheControllerFactory = CacheControllerFactory(cache: cache) - - let cacheController = xcframeworks ? cacheControllerFactory.makeForXCFramework() - : cacheControllerFactory.makeForSimulatorFramework() + let contentHasher = CacheContentHasher() + let cacheController = xcframeworks ? cacheControllerFactory.makeForXCFramework(contentHasher: contentHasher) + : cacheControllerFactory.makeForSimulatorFramework(contentHasher: contentHasher) try cacheController.cache(path: path) } diff --git a/Sources/TuistKit/Services/DocService.swift b/Sources/TuistKit/Services/DocService.swift index 8c5f5d9f6..139bacbd8 100644 --- a/Sources/TuistKit/Services/DocService.swift +++ b/Sources/TuistKit/Services/DocService.swift @@ -2,6 +2,7 @@ import Foundation import RxBlocking import Signals import TSCBasic +import TuistCache import TuistCore import TuistDoc import TuistSupport @@ -59,7 +60,7 @@ final class DocService { /// Semaphore to block the execution private let semaphore: Semaphoring - init(generator: Generating = Generator(), + init(generator: Generating = Generator(contentHasher: CacheContentHasher()), swiftDocController: SwiftDocControlling = SwiftDocController(), swiftDocServer: SwiftDocServing = SwiftDocServer(), fileHandler: FileHandling = FileHandler.shared, diff --git a/Sources/TuistKit/Services/Focus/FocusGraphMapperProvider.swift b/Sources/TuistKit/Services/Focus/FocusGraphMapperProvider.swift index ddc1d8b61..4a3bc5ad4 100644 --- a/Sources/TuistKit/Services/Focus/FocusGraphMapperProvider.swift +++ b/Sources/TuistKit/Services/Focus/FocusGraphMapperProvider.swift @@ -7,12 +7,15 @@ final class FocusGraphMapperProvider: GraphMapperProviding { private let cacheSources: Set private let cache: Bool private let cacheOutputType: CacheOutputType + private let contentHasher: ContentHashing - init(cache: Bool, + init(contentHasher: ContentHashing, + cache: Bool, cacheSources: Set, cacheOutputType: CacheOutputType, defaultProvider: GraphMapperProviding = GraphMapperProvider()) { + self.contentHasher = contentHasher self.cacheSources = cacheSources self.cache = cache self.defaultProvider = defaultProvider @@ -28,7 +31,8 @@ final class FocusGraphMapperProvider: GraphMapperProviding { let cacheMapper = CacheMapper(config: config, cacheStorageProvider: CacheStorageProvider(config: config), sources: cacheSources, - cacheOutputType: cacheOutputType) + cacheOutputType: cacheOutputType, + contentHasher: contentHasher) mappers.append(cacheMapper) mappers.append(CacheTreeShakingGraphMapper()) } diff --git a/Sources/TuistKit/Services/FocusService.swift b/Sources/TuistKit/Services/FocusService.swift index 8c0090c26..5047b1fe5 100644 --- a/Sources/TuistKit/Services/FocusService.swift +++ b/Sources/TuistKit/Services/FocusService.swift @@ -13,11 +13,19 @@ protocol FocusServiceProjectGeneratorFactorying { } final class FocusServiceProjectGeneratorFactory: FocusServiceProjectGeneratorFactorying { + init() {} + func generator(sources: Set, xcframeworks: Bool, ignoreCache: Bool) -> Generating { - let graphMapperProvider = FocusGraphMapperProvider(cache: !ignoreCache, + let contentHasher = CacheContentHasher() + let graphMapperProvider = FocusGraphMapperProvider(contentHasher: contentHasher, + cache: !ignoreCache, cacheSources: sources, cacheOutputType: xcframeworks ? .xcframework : .framework) - return Generator(graphMapperProvider: graphMapperProvider) + let projectMapperProvider = ProjectMapperProvider(contentHasher: contentHasher) + return Generator(projectMapperProvider: projectMapperProvider, + graphMapperProvider: graphMapperProvider, + workspaceMapperProvider: WorkspaceMapperProvider(contentHasher: contentHasher), + manifestLoaderFactory: ManifestLoaderFactory()) } } diff --git a/Sources/TuistKit/Services/GenerateService.swift b/Sources/TuistKit/Services/GenerateService.swift index 9327dc747..80a57064f 100644 --- a/Sources/TuistKit/Services/GenerateService.swift +++ b/Sources/TuistKit/Services/GenerateService.swift @@ -1,4 +1,6 @@ import TSCBasic +import TuistCache +import TuistCore import TuistGenerator import TuistLoader import TuistSupport @@ -9,7 +11,12 @@ protocol GenerateServiceProjectGeneratorFactorying { final class GenerateServiceProjectGeneratorFactory: GenerateServiceProjectGeneratorFactorying { func generator() -> Generating { - Generator(graphMapperProvider: GraphMapperProvider()) + let contentHasher = CacheContentHasher() + let projectMapperProvider = ProjectMapperProvider(contentHasher: contentHasher) + return Generator(projectMapperProvider: projectMapperProvider, + graphMapperProvider: GraphMapperProvider(), + workspaceMapperProvider: WorkspaceMapperProvider(contentHasher: contentHasher), + manifestLoaderFactory: ManifestLoaderFactory()) } } diff --git a/Sources/TuistKit/Services/Migration/MigrationTargetsByDependenciesService.swift b/Sources/TuistKit/Services/Migration/MigrationTargetsByDependenciesService.swift new file mode 100644 index 000000000..3a1b34472 --- /dev/null +++ b/Sources/TuistKit/Services/Migration/MigrationTargetsByDependenciesService.swift @@ -0,0 +1,34 @@ +import Foundation +import TSCBasic +import TuistMigration +import TuistSupport + +final class MigrationTargetsByDependenciesService { + // MARK: - Attributes + + private let targetsExtractor: TargetsExtracting + + // MARK: - Init + + init(targetsExtractor: TargetsExtracting = TargetsExtractor()) { + self.targetsExtractor = targetsExtractor + } + + // MARK: - Internal + + func run(xcodeprojPath: AbsolutePath) throws { + let sortedTargets = try targetsExtractor.targetsSortedByDependencies(xcodeprojPath: xcodeprojPath) + let sortedTargetsJson = try makeJson(from: sortedTargets) + logger.info("\(sortedTargetsJson)") + } + + private func makeJson(from sortedTargets: [TargetDependencyCount]) throws -> String { + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = .prettyPrinted + let targetsData = try jsonEncoder.encode(sortedTargets) + guard let jsonString = String(data: targetsData, encoding: .utf8) else { + throw TargetsExtractorError.failedToEncode + } + return jsonString + } +} diff --git a/Sources/TuistKit/Services/TestService.swift b/Sources/TuistKit/Services/TestService.swift index b9bb4ebab..d5f2d9035 100644 --- a/Sources/TuistKit/Services/TestService.swift +++ b/Sources/TuistKit/Services/TestService.swift @@ -3,6 +3,7 @@ import RxBlocking import TSCBasic import struct TSCUtility.Version import TuistAutomation +import TuistCache import TuistCore import TuistSupport @@ -45,7 +46,7 @@ final class TestService { let simulatorController: SimulatorControlling init( - generator: Generating = Generator(), + generator: Generating = Generator(contentHasher: ContentHasher()), xcodebuildController: XcodeBuildControlling = XcodeBuildController(), buildGraphInspector: BuildGraphInspecting = BuildGraphInspector(), simulatorController: SimulatorControlling = SimulatorController() diff --git a/Sources/TuistLoader/Models+ManifestMappers/Target+ManifestMapper.swift b/Sources/TuistLoader/Models+ManifestMappers/Target+ManifestMapper.swift index 37a7e4188..9f4c83738 100644 --- a/Sources/TuistLoader/Models+ManifestMappers/Target+ManifestMapper.swift +++ b/Sources/TuistLoader/Models+ManifestMappers/Target+ManifestMapper.swift @@ -41,7 +41,7 @@ extension TuistCore.Target { let sources = try TuistCore.Target.sources(targetName: name, sources: manifest.sources?.globs.map { (glob: ProjectDescription.SourceFileGlob) in let globPath = try generatorPaths.resolve(path: glob.glob).pathString let excluding: [String] = try glob.excluding.compactMap { try generatorPaths.resolve(path: $0).pathString } - return (glob: globPath, excluding: excluding, compilerFlags: glob.compilerFlags) + return TuistCore.SourceFileGlob(glob: globPath, excluding: excluding, compilerFlags: glob.compilerFlags) } ?? []) let resourceFilter = { (path: AbsolutePath) -> Bool in diff --git a/Sources/TuistLoader/Utils/ManifestFilesLocator.swift b/Sources/TuistLoader/Utils/ManifestFilesLocator.swift index 85fab3dac..319429604 100644 --- a/Sources/TuistLoader/Utils/ManifestFilesLocator.swift +++ b/Sources/TuistLoader/Utils/ManifestFilesLocator.swift @@ -6,21 +6,25 @@ import TuistSupport public protocol ManifestFilesLocating: AnyObject { /// It locates the manifest files that are have a connection with the /// definitions in the current directory. - /// - Parameter at: Directory for which the manifest files will be obtained. - func locateProjectManifests(at: AbsolutePath) -> [(Manifest, AbsolutePath)] + /// - Parameter locatingPath: Directory for which the manifest files will be obtained. + func locateProjectManifests(at locatingPath: AbsolutePath) -> [(Manifest, AbsolutePath)] /// It locates all manifest files under the root project folder - /// - Parameter at: Directory for which the **project** manifest files will + /// - Parameter locatingPath: Directory for which the **project** manifest files will /// be obtained - func locateAllProjectManifests(at: AbsolutePath) -> [(Manifest, AbsolutePath)] + func locateAllProjectManifests(at locatingPath: AbsolutePath) -> [(Manifest, AbsolutePath)] /// It traverses up the directory hierarchy until it finds a `Config.swift` file. - /// - Parameter at: Path from where to do the lookup. - func locateConfig(at: AbsolutePath) -> AbsolutePath? + /// - Parameter locatingPath: Path from where to do the lookup. + func locateConfig(at locatingPath: AbsolutePath) -> AbsolutePath? + + /// It traverses up the directory hierarchy until it finds a `Dependencies.swift` file. + /// - Parameter locatingPath: Path from where to do the lookup. + func locateDependencies(at locatingPath: AbsolutePath) -> AbsolutePath? /// It traverses up the directory hierarchy until it finds a `Setup.swift` file. - /// - Parameter at: Path from where to do the lookup. - func locateSetup(at: AbsolutePath) -> AbsolutePath? + /// - Parameter locatingPath: Path from where to do the lookup. + func locateSetup(at locatingPath: AbsolutePath) -> AbsolutePath? } public final class ManifestFilesLocator: ManifestFilesLocating { @@ -39,32 +43,37 @@ public final class ManifestFilesLocator: ManifestFilesLocating { } } - public func locateAllProjectManifests(at: AbsolutePath) -> [(Manifest, AbsolutePath)] { - guard let rootPath = rootDirectoryLocator.locate(from: at) else { return locateProjectManifests(at: at) } - let projectsPaths = FileHandler.shared.glob(rootPath, glob: "**/\(Manifest.project.fileName(at))").map { (Manifest.project, $0) } - let workspacesPaths = FileHandler.shared.glob(rootPath, glob: "**/\(Manifest.workspace.fileName(at))").map { (Manifest.workspace, $0) } + public func locateAllProjectManifests(at locatingPath: AbsolutePath) -> [(Manifest, AbsolutePath)] { + guard let rootPath = rootDirectoryLocator.locate(from: locatingPath) else { return locateProjectManifests(at: locatingPath) } + let projectsPaths = FileHandler.shared.glob(rootPath, glob: "**/\(Manifest.project.fileName(locatingPath))").map { (Manifest.project, $0) } + let workspacesPaths = FileHandler.shared.glob(rootPath, glob: "**/\(Manifest.workspace.fileName(locatingPath))").map { (Manifest.workspace, $0) } return projectsPaths + workspacesPaths } - public func locateConfig(at: AbsolutePath) -> AbsolutePath? { - let subPath = RelativePath("\(Constants.tuistDirectoryName)/\(Manifest.config.fileName(at))") - return traverseAndLocate(at: at, appending: subPath) + public func locateConfig(at locatingPath: AbsolutePath) -> AbsolutePath? { + let subPath = RelativePath("\(Constants.tuistDirectoryName)/\(Manifest.config.fileName(locatingPath))") + return traverseAndLocate(at: locatingPath, appending: subPath) } - public func locateSetup(at: AbsolutePath) -> AbsolutePath? { - let subPath = RelativePath(Manifest.setup.fileName(at)) - return traverseAndLocate(at: at, appending: subPath) + public func locateDependencies(at locatingPath: AbsolutePath) -> AbsolutePath? { + let subPath = RelativePath("\(Constants.tuistDirectoryName)/\(Manifest.dependencies.fileName(locatingPath))") + return traverseAndLocate(at: locatingPath, appending: subPath) + } + + public func locateSetup(at locatingPath: AbsolutePath) -> AbsolutePath? { + let subPath = RelativePath(Manifest.setup.fileName(locatingPath)) + return traverseAndLocate(at: locatingPath, appending: subPath) } // MARK: - Helpers - private func traverseAndLocate(at: AbsolutePath, appending subpath: RelativePath) -> AbsolutePath? { - let manifestPath = at.appending(subpath) + private func traverseAndLocate(at locatingPath: AbsolutePath, appending subpath: RelativePath) -> AbsolutePath? { + let manifestPath = locatingPath.appending(subpath) if FileHandler.shared.exists(manifestPath) { return manifestPath - } else if at != .root { - return traverseAndLocate(at: at.parentDirectory, appending: subpath) + } else if locatingPath != .root { + return traverseAndLocate(at: locatingPath.parentDirectory, appending: subpath) } else { return nil } diff --git a/Sources/TuistLoaderTesting/Loaders/TestData/ProjectDescription+TestData.swift b/Sources/TuistLoaderTesting/Loaders/TestData/ProjectDescription+TestData.swift index 253eaf918..47cc63198 100644 --- a/Sources/TuistLoaderTesting/Loaders/TestData/ProjectDescription+TestData.swift +++ b/Sources/TuistLoaderTesting/Loaders/TestData/ProjectDescription+TestData.swift @@ -167,6 +167,6 @@ extension Dependencies { public static func test(name: String = "Any Dependency", requirement: Dependency.Requirement = .exact("1.4.0")) -> Dependencies { - Dependencies([Dependency(name: name, requirement: requirement)]) + Dependencies([.carthage(name: name, requirement: requirement)]) } } diff --git a/Sources/TuistLoaderTesting/Utils/Mocks/MockManifestFilesLocator.swift b/Sources/TuistLoaderTesting/Utils/Mocks/MockManifestFilesLocator.swift index 5abbfe5b4..2a16280db 100644 --- a/Sources/TuistLoaderTesting/Utils/Mocks/MockManifestFilesLocator.swift +++ b/Sources/TuistLoaderTesting/Utils/Mocks/MockManifestFilesLocator.swift @@ -9,6 +9,8 @@ public final class MockManifestFilesLocator: ManifestFilesLocating { public var locateAllProjectManifestsArgs: [AbsolutePath] = [] public var locateConfigStub: AbsolutePath? public var locateConfigArgs: [AbsolutePath] = [] + public var locateDependenciesStub: AbsolutePath? + public var locateDependenciesArgs: [AbsolutePath] = [] public var locateSetupStub: AbsolutePath? public var locateSetupArgs: [AbsolutePath] = [] @@ -27,6 +29,11 @@ public final class MockManifestFilesLocator: ManifestFilesLocating { return locateConfigStub ?? at.appending(components: "Tuist", "Config.swift") } + public func locateDependencies(at: AbsolutePath) -> AbsolutePath? { + locateDependenciesArgs.append(at) + return locateDependenciesStub ?? at.appending(components: "Tuist", "Dependencies.swift") + } + public func locateSetup(at: AbsolutePath) -> AbsolutePath? { locateSetupArgs.append(at) return locateSetupStub ?? at.appending(component: "Setup.swift") diff --git a/Sources/TuistMigration/Utilities/TargetsExtractor.swift b/Sources/TuistMigration/Utilities/TargetsExtractor.swift new file mode 100644 index 000000000..a034e9398 --- /dev/null +++ b/Sources/TuistMigration/Utilities/TargetsExtractor.swift @@ -0,0 +1,84 @@ +import Foundation +import PathKit +import TSCBasic +import TuistSupport +import XcodeProj + +public enum TargetsExtractorError: FatalError, Equatable { + case missingProject + case noTargets + case failedToExtractTargets(String) + case failedToEncode + + public var description: String { + switch self { + case .missingProject: return "The project's pbxproj file contains no projects." + case .noTargets: return "The project doesn't have any targets." + case .failedToEncode: return "Failed to encode targets into JSON schema" + case let .failedToExtractTargets(reason): return "Failed to extract targets for reason: \(reason)." + } + } + + public var type: ErrorType { + switch self { + case .missingProject: + return .abort + case .noTargets: + return .abort + case .failedToExtractTargets: + return .bug + case .failedToEncode: + return .bug + } + } +} + +/// An interface to extract all targets from an xcode project, sorted by number of dependencies +public protocol TargetsExtracting { + /// - Parameters: + /// - xcodeprojPath: Path to the Xcode project. + func targetsSortedByDependencies(xcodeprojPath: AbsolutePath) throws -> [TargetDependencyCount] +} + +public struct TargetDependencyCount: Encodable { + public let targetName: String + public let dependenciesCount: Int +} + +public final class TargetsExtractor: TargetsExtracting { + // MARK: - Init + + public init() {} + + // MARK: - EmptyBuildSettingsChecking + + public func targetsSortedByDependencies(xcodeprojPath: AbsolutePath) throws -> [TargetDependencyCount] { + guard FileHandler.shared.exists(xcodeprojPath) else { throw TargetsExtractorError.missingProject } + let pbxproj = try XcodeProj(path: Path(xcodeprojPath.pathString)).pbxproj + let targets = pbxproj.nativeTargets + pbxproj.aggregateTargets + pbxproj.legacyTargets + if targets.isEmpty { + throw TargetsExtractorError.noTargets + } + return try sortTargetsByDependenciesCount(targets) + } + + private func sortTargetsByDependenciesCount(_ targets: [PBXTarget]) throws -> [TargetDependencyCount] { + let sortedTargets = try targets.sorted { lTarget, rTarget -> Bool in + let lCount = try countDependencies(of: lTarget) + let rCount = try countDependencies(of: rTarget) + if lCount == rCount { + return lTarget.name < rTarget.name + } + return lCount < rCount + } + return try sortedTargets.map { TargetDependencyCount(targetName: $0.name, dependenciesCount: try countDependencies(of: $0)) } + } + + private func countDependencies(of target: PBXTarget) throws -> Int { + var count = target.dependencies.count + if let frameworkFiles = try target.frameworksBuildPhase()?.files { + count += frameworkFiles.count + } + return count + } +} diff --git a/Sources/TuistSupport/Constants.swift b/Sources/TuistSupport/Constants.swift index a620e6daf..4fd15832c 100644 --- a/Sources/TuistSupport/Constants.swift +++ b/Sources/TuistSupport/Constants.swift @@ -32,11 +32,16 @@ public struct Constants { public static let signingKeychain = "signing.keychain" } + public struct AsyncQueue { + public static let directoryName: String = "Queue" + } + public struct EnvironmentVariables { public static let verbose = "TUIST_VERBOSE" public static let colouredOutput = "TUIST_COLOURED_OUTPUT" public static let versionsDirectory = "TUIST_VERSIONS_DIRECTORY" public static let cacheDirectory = "TUIST_CACHE_DIRECTORY" + public static let queueDirectory = "TUIST_QUEUE_DIRECTORY" public static let cloudToken = "TUIST_CLOUD_TOKEN" public static let cacheManifests = "TUIST_CACHE_MANIFESTS" } diff --git a/Sources/TuistSupport/Utils/Environment.swift b/Sources/TuistSupport/Utils/Environment.swift index bbd94dc5c..3fa99b607 100644 --- a/Sources/TuistSupport/Utils/Environment.swift +++ b/Sources/TuistSupport/Utils/Environment.swift @@ -31,6 +31,9 @@ public protocol Environmenting: AnyObject { /// Returns true if Tuist is running with verbose mode enabled. var isVerbose: Bool { get } + + /// Returns the path to the directory where the async queue events are persisted. + var queueDirectory: AbsolutePath { get } } /// Local environment controller. @@ -124,6 +127,14 @@ public class Environment: Environmenting { } } + public var queueDirectory: AbsolutePath { + if let envVariable = ProcessInfo.processInfo.environment[Constants.EnvironmentVariables.queueDirectory] { + return AbsolutePath(envVariable) + } else { + return directory.appending(component: Constants.AsyncQueue.directoryName) + } + } + /// Returns all the environment variables that are specific to Tuist (prefixed with TUIST_) public var tuistVariables: [String: String] { ProcessInfo.processInfo.environment.filter { $0.key.hasPrefix("TUIST_") } diff --git a/Sources/TuistSupportTesting/Utils/MockEnvironment.swift b/Sources/TuistSupportTesting/Utils/MockEnvironment.swift index 7c5bc9ad1..3aac2cc91 100644 --- a/Sources/TuistSupportTesting/Utils/MockEnvironment.swift +++ b/Sources/TuistSupportTesting/Utils/MockEnvironment.swift @@ -17,7 +17,7 @@ public class MockEnvironment: Environmenting { public var isVerbose: Bool = false public var cacheDirectoryStub: AbsolutePath? - + public var queueDirectoryStub: AbsolutePath? public var shouldOutputBeColoured: Bool = false public var isStandardOutputInteractive: Bool = false public var tuistVariables: [String: String] = [:] @@ -35,6 +35,10 @@ public class MockEnvironment: Environmenting { cacheDirectoryStub ?? directory.path.appending(component: "Cache") } + public var queueDirectory: AbsolutePath { + queueDirectoryStub ?? directory.path.appending(component: Constants.AsyncQueue.directoryName) + } + public var projectDescriptionHelpersCacheDirectory: AbsolutePath { cacheDirectory.appending(component: "ProjectDescriptionHelpers") } diff --git a/Sources/tuist/main.swift b/Sources/tuist/main.swift index 2b3dc8cf9..83961be48 100644 --- a/Sources/tuist/main.swift +++ b/Sources/tuist/main.swift @@ -8,4 +8,5 @@ if CommandLine.arguments.contains("--verbose") { try? ProcessEnv.setVar(Constant LogOutput.bootstrap() import TuistKit + TuistCommand.main() diff --git a/TapestryConfig.swift b/TapestryConfig.swift index fb76581ed..c3592fccf 100644 --- a/TapestryConfig.swift +++ b/TapestryConfig.swift @@ -12,7 +12,7 @@ let config = TapestryConfig( // .pre(tool: "bundle", arguments: ["exec", "rake", "features"]), .pre(.docsUpdate), .pre(tool: "sudo", arguments: ["xcode-select", "-s", "/Applications/Xcode_11.5.app"]), - .post(tool: "bundle", arguments: ["exec", "rake", "release"]), + .post(tool: "bundle", arguments: ["exec", "rake", "release[\(Argument.version)]"]), .post(tool: "bundle", arguments: ["exec", "rake", "release_scripts"]), .post( .githubRelease( diff --git a/Tests/TuistAsyncQueueTests/AsyncQueuePersistorTests.swift b/Tests/TuistAsyncQueueTests/AsyncQueuePersistorTests.swift new file mode 100644 index 000000000..37d10ac4a --- /dev/null +++ b/Tests/TuistAsyncQueueTests/AsyncQueuePersistorTests.swift @@ -0,0 +1,55 @@ +import Foundation +import RxBlocking +import RxSwift +import TuistCore +import TuistSupport +import XCTest + +@testable import TuistAsyncQueue +@testable import TuistSupportTesting + +final class AsyncQueuePersistorTests: TuistUnitTestCase { + var subject: AsyncQueuePersistor! + + override func setUp() { + let temporaryDirectory = try! temporaryPath() + subject = AsyncQueuePersistor(directory: temporaryDirectory) + super.setUp() + } + + override func tearDown() { + super.tearDown() + subject = nil + } + + func test_write() throws { + // Given + let event = AnyAsyncQueueEvent(dispatcherId: "dispatcher") + + // When + _ = try subject.write(event: event).toBlocking().last() + + // Then + let got = try subject.readAll().toBlocking().last() + let gotEvent = try XCTUnwrap(got?.first) + XCTAssertEqual(gotEvent.dispatcherId, "dispatcher") + XCTAssertEqual(gotEvent.id, event.id) + let normalizedDate = Date(timeIntervalSince1970: Double(Int(Double(event.date.timeIntervalSince1970)))) + XCTAssertEqual(gotEvent.date, normalizedDate) + } + + func test_delete() throws { + // Given + let event = AnyAsyncQueueEvent(dispatcherId: "dispatcher") + _ = try subject.write(event: event).toBlocking().last() + var persistedEvents = try subject.readAll().toBlocking().last() + XCTAssertEqual(persistedEvents?.count, 1) + + // When + _ = try subject.delete(event: event).toBlocking().last() + + // Then + persistedEvents = try subject.readAll().toBlocking().last() + XCTAssertEqual(persistedEvents?.count, 0) + } +} diff --git a/Tests/TuistAsyncQueueTests/AsyncQueueTests.swift b/Tests/TuistAsyncQueueTests/AsyncQueueTests.swift new file mode 100644 index 000000000..da0dda2a3 --- /dev/null +++ b/Tests/TuistAsyncQueueTests/AsyncQueueTests.swift @@ -0,0 +1,294 @@ +import Foundation +import Queuer +import RxBlocking +import RxSwift +import TuistCore +import TuistSupport +import XCTest + +@testable import TuistAsyncQueue +@testable import TuistAsyncQueueTesting +@testable import TuistSupportTesting + +final class AsyncQueueTests: TuistUnitTestCase { + var subject: AsyncQueue! + + let dispatcher1ID = "Dispatcher1" + let dispatcher2ID = "Dispatcher2" + + var mockAsyncQueueDispatcher1: MockAsyncQueueDispatcher! + var mockAsyncQueueDispatcher2: MockAsyncQueueDispatcher! + + var mockCIChecker: MockCIChecker! + + var mockPersistor: MockAsyncQueuePersistor! + var mockQueuer: MockQueuer! + + let timeout = 3.0 + + override func setUp() { + mockAsyncQueueDispatcher1 = MockAsyncQueueDispatcher() + mockAsyncQueueDispatcher1.stubbedIdentifier = dispatcher1ID + + mockAsyncQueueDispatcher2 = MockAsyncQueueDispatcher() + mockAsyncQueueDispatcher2.stubbedIdentifier = dispatcher2ID + + mockCIChecker = MockCIChecker() + mockPersistor = MockAsyncQueuePersistor() + mockQueuer = MockQueuer() + super.setUp() + } + + override func tearDown() { + super.tearDown() + mockAsyncQueueDispatcher1 = nil + mockAsyncQueueDispatcher2 = nil + mockCIChecker = nil + mockPersistor = nil + mockQueuer = nil + subject = nil + } + + func subjectWithExecutionBlock( + queue: Queuing? = nil, + executionBlock: @escaping () throws -> Void = {}, + ciChecker: CIChecking? = nil, + persistor: AsyncQueuePersisting? = nil, + dispatchers: [AsyncQueueDispatching]? = nil + ) -> AsyncQueue { + guard let asyncQueue = try? AsyncQueue( + queue: queue ?? mockQueuer, + executionBlock: executionBlock, + ciChecker: ciChecker ?? mockCIChecker, + persistor: persistor ?? mockPersistor, + dispatchers: dispatchers ?? [mockAsyncQueueDispatcher1, mockAsyncQueueDispatcher2], + persistedEventsSchedulerType: MainScheduler() + ) else { + XCTFail("Could not create subject") + return try! AsyncQueue(dispatchers: [mockAsyncQueueDispatcher1, mockAsyncQueueDispatcher2], executionBlock: executionBlock) + } + return asyncQueue + } + + func test_dispatch_eventIsPersisted() throws { + // Given + let event = AnyAsyncQueueEvent(dispatcherId: dispatcher1ID) + subject = subjectWithExecutionBlock() + + // When + subject.dispatch(event: event) + + // Then + guard let persistedEvent = mockPersistor.invokedWriteEvent else { + XCTFail("Event not passed to the persistor") + return + } + XCTAssertEqual(event.id, persistedEvent.id) + } + + func test_dispatch_eventIsQueued() throws { + // Given + let event = AnyAsyncQueueEvent(dispatcherId: dispatcher1ID) + subject = subjectWithExecutionBlock() + + // When + subject.dispatch(event: event) + + // Then + guard let queuedOperation = mockQueuer.invokedAddOperationParameterOperation as? ConcurrentOperation else { + XCTFail("Operation not added to the queuer") + return + } + XCTAssertEqual(queuedOperation.name, event.id.uuidString) + } + + func test_dispatch_eventIsDeletedFromThePersistorOnSendSuccess() throws { + // Given + let event = AnyAsyncQueueEvent(dispatcherId: dispatcher1ID) + subject = subjectWithExecutionBlock(queue: Queuer.shared) + let expectation = XCTestExpectation(description: #function) + mockPersistor.invokedDeleteCallBack = { + expectation.fulfill() + } + + // When + subject.dispatch(event: event) + + // Then + wait(for: [expectation], timeout: timeout) + guard let deletedEvent = mockPersistor.invokedDeleteEvent else { + XCTFail("Event was not deleted by the persistor") + return + } + XCTAssertEqual(event.id, deletedEvent.id) + } + + func test_dispatch_eventIsDispatchedByTheRightDispatcher() throws { + // Given + let event = AnyAsyncQueueEvent(dispatcherId: dispatcher1ID) + subject = subjectWithExecutionBlock(queue: Queuer.shared) + let expectation = XCTestExpectation(description: #function) + mockAsyncQueueDispatcher1.invokedDispatchCallBack = { + expectation.fulfill() + } + + // When + subject.dispatch(event: event) + + // Then + wait(for: [expectation], timeout: timeout) + guard let dispatchedEvent = mockAsyncQueueDispatcher1.invokedDispatchParameterEvent else { + XCTFail("Event was not dispatched") + return + } + XCTAssertEqual(event.id, dispatchedEvent.id) + XCTAssertEqual(mockAsyncQueueDispatcher1.invokedDispatchCount, 1) + XCTAssertEqual(mockAsyncQueueDispatcher2.invokedDispatchCount, 0) + XCTAssertNil(mockAsyncQueueDispatcher2.invokedDispatchParameterEvent) + } + + func test_dispatch_queuerTriesThreeTimesToDispatch() throws { + // Given + let event = AnyAsyncQueueEvent(dispatcherId: dispatcher1ID) + subject = subjectWithExecutionBlock(queue: Queuer.shared) + mockAsyncQueueDispatcher1.stubbedDispatchError = MockAsyncQueueDispatcherError.dispatchError + let expectation = XCTestExpectation(description: #function) + + var count = 0 + mockAsyncQueueDispatcher1.invokedDispatchCallBack = { + count += 1 + if count == 3 { + expectation.fulfill() + } + } + + // When + subject.dispatch(event: event) + + // Then + wait(for: [expectation], timeout: timeout) + XCTAssertEqual(count, 3) + } + + func test_dispatch_doesNotDeleteEventOnError() throws { + // Given + let event = AnyAsyncQueueEvent(dispatcherId: dispatcher1ID) + subject = subjectWithExecutionBlock(queue: Queuer.shared) + mockAsyncQueueDispatcher1.stubbedDispatchError = MockAsyncQueueDispatcherError.dispatchError + let expectation = XCTestExpectation(description: #function) + + var count = 0 + mockAsyncQueueDispatcher1.invokedDispatchCallBack = { + count += 1 + if count == 3 { + expectation.fulfill() + } + } + + // When + subject.dispatch(event: event) + + // Then + wait(for: [expectation], timeout: timeout) + XCTAssertEqual(count, 3) + XCTAssertEqual(mockPersistor.invokedDeleteEventCount, 0) + } + + func test_dispatch_readsPersistedEventsInitialization() throws { + // Given + let eventTuple1: AsyncQueueEventTuple = makeEventTuple(id: 1) + let eventTuple2: AsyncQueueEventTuple = makeEventTuple(id: 2) + let eventTuple3: AsyncQueueEventTuple = makeEventTuple(id: 3) + mockPersistor.stubbedReadAllResult = .just([eventTuple1, eventTuple2, eventTuple3]) + + // When + subject = subjectWithExecutionBlock() + + // Then + let numberOfOperationsQueued = mockQueuer.invokedAddOperationCount + XCTAssertEqual(numberOfOperationsQueued, 3) + + guard let queuedOperation1 = mockQueuer.invokedAddOperationParametersOperationsList[0] as? ConcurrentOperation else { + XCTFail("Operation for event tuple 1 not added to the queuer") + return + } + XCTAssertEqual(queuedOperation1.name, eventTuple1.id.uuidString) + + guard let queuedOperation2 = mockQueuer.invokedAddOperationParametersOperationsList[1] as? ConcurrentOperation else { + XCTFail("Operation for event tuple 2 not added to the queuer") + return + } + XCTAssertEqual(queuedOperation2.name, eventTuple2.id.uuidString) + + guard let queuedOperation3 = mockQueuer.invokedAddOperationParametersOperationsList[2] as? ConcurrentOperation else { + XCTFail("Operation for event tuple 3 not added to the queuer") + return + } + XCTAssertEqual(queuedOperation3.name, eventTuple3.id.uuidString) + } + + func test_dispatch_persistedEventIsDispatchedByTheRightDispatcher() throws { + // Given + let eventTuple1: AsyncQueueEventTuple = makeEventTuple(id: 1) + mockPersistor.stubbedReadAllResult = .just([eventTuple1]) + + let expectation = XCTestExpectation(description: #function) + mockAsyncQueueDispatcher1.invokedDispatchPersistedCallBack = { + expectation.fulfill() + } + + // When + subject = subjectWithExecutionBlock(queue: Queuer.shared) + + // Then + wait(for: [expectation], timeout: timeout) + guard let dispatchedEventData = mockAsyncQueueDispatcher1.invokedDispatchPersistedDataParameter else { + XCTFail("Data from persisted event was not dispatched") + return + } + XCTAssertEqual(eventTuple1.data, dispatchedEventData) + XCTAssertEqual(mockAsyncQueueDispatcher1.invokedDispatchPersistedCount, 1) + XCTAssertEqual(mockAsyncQueueDispatcher2.invokedDispatchPersistedCount, 0) + } + + func test_dispatch_sentPersistedEventIsThenDeleted() throws { + // Given + let id: UInt = 1 + let eventTuple1: AsyncQueueEventTuple = makeEventTuple(id: id) + mockPersistor.stubbedReadAllResult = .just([eventTuple1]) + + let expectation = XCTestExpectation(description: #function) + mockAsyncQueueDispatcher1.invokedDispatchPersistedCallBack = { + expectation.fulfill() + } + + // When + subject = subjectWithExecutionBlock(queue: Queuer.shared) + + // Then + wait(for: [expectation], timeout: timeout) + guard let filename = mockPersistor.invokedDeleteFilenameParameter else { + XCTFail("Sent persisted event was then not deleted") + return + } + XCTAssertEqual(filename, self.filename(with: id)) + } + + // MARK: Private + + private func makeEventTuple(id: UInt, dispatcherId: String? = nil) -> AsyncQueueEventTuple { + (dispatcherId: dispatcherId ?? dispatcher1ID, + id: UUID(), + date: Date(), + data: data(with: id), + filename: filename(with: id)) + } + + private func data(with id: UInt) -> Data { + withUnsafeBytes(of: id) { Data($0) } + } + + private func filename(with id: UInt) -> String { + "filename-\(id)" + } +} diff --git a/Tests/TuistCacheIntegrationTests/ContentHashing/ContentHashingIntegrationTests.swift b/Tests/TuistCacheIntegrationTests/ContentHashing/ContentHashingIntegrationTests.swift index 603d9c60f..441de09c9 100644 --- a/Tests/TuistCacheIntegrationTests/ContentHashing/ContentHashingIntegrationTests.swift +++ b/Tests/TuistCacheIntegrationTests/ContentHashing/ContentHashingIntegrationTests.swift @@ -1,5 +1,6 @@ import Foundation import TSCBasic +import TuistCore import TuistCoreTesting import TuistSupport import XCTest @@ -11,10 +12,10 @@ import XCTest final class ContentHashingIntegrationTests: TuistTestCase { var subject: GraphContentHasher! var temporaryDirectoryPath: String! - var source1: Target.SourceFile! - var source2: Target.SourceFile! - var source3: Target.SourceFile! - var source4: Target.SourceFile! + var source1: SourceFile! + var source2: SourceFile! + var source3: SourceFile! + var source4: SourceFile! var resourceFile1: FileElement! var resourceFile2: FileElement! var resourceFolderReference1: FileElement! @@ -42,7 +43,7 @@ final class ContentHashingIntegrationTests: TuistTestCase { } catch { XCTFail("Error while creating files for stub project") } - subject = GraphContentHasher() + subject = GraphContentHasher(contentHasher: CacheContentHasher()) } override func tearDown() { @@ -252,11 +253,11 @@ final class ContentHashingIntegrationTests: TuistTestCase { // MARK: - Private helpers - private func createTemporarySourceFile(on temporaryDirectoryPath: AbsolutePath, name: String, content: String) throws -> Target.SourceFile { + private func createTemporarySourceFile(on temporaryDirectoryPath: AbsolutePath, name: String, content: String) throws -> SourceFile { let filePath = temporaryDirectoryPath.appending(component: name) try FileHandler.shared.touch(filePath) try FileHandler.shared.write(content, path: filePath, atomically: true) - return Target.SourceFile(path: filePath, compilerFlags: nil) + return SourceFile(path: filePath, compilerFlags: nil) } private func createTemporaryResourceFile(on temporaryDirectoryPath: AbsolutePath, name: String, content: String) throws -> FileElement { @@ -276,7 +277,7 @@ final class ContentHashingIntegrationTests: TuistTestCase { private func makeFramework(named: String, platform: Platform = .iOS, productName: String? = nil, - sources: [Target.SourceFile] = [], + sources: [SourceFile] = [], resources: [FileElement] = [], coreDataModels: [CoreDataModel] = [], targetActions: [TargetAction] = []) -> TargetNode diff --git a/Tests/TuistCacheTests/ContentHashing/CacheContentHasherTests.swift b/Tests/TuistCacheTests/ContentHashing/CacheContentHasherTests.swift index bcdcb9e1e..513290138 100644 --- a/Tests/TuistCacheTests/ContentHashing/CacheContentHasherTests.swift +++ b/Tests/TuistCacheTests/ContentHashing/CacheContentHasherTests.swift @@ -3,6 +3,7 @@ import TuistCacheTesting import TuistSupport import XCTest @testable import TuistCache +@testable import TuistCoreTesting @testable import TuistSupportTesting final class CacheContentHasherTests: TuistUnitTestCase { diff --git a/Tests/TuistCacheTests/ContentHashing/GraphContentHasherTests.swift b/Tests/TuistCacheTests/ContentHashing/GraphContentHasherTests.swift index 64fdf379d..830d8fa16 100644 --- a/Tests/TuistCacheTests/ContentHashing/GraphContentHasherTests.swift +++ b/Tests/TuistCacheTests/ContentHashing/GraphContentHasherTests.swift @@ -12,7 +12,7 @@ final class GraphContentHasherTests: TuistUnitTestCase { override func setUp() { super.setUp() - subject = GraphContentHasher() + subject = GraphContentHasher(contentHasher: ContentHasher()) } override func tearDown() { diff --git a/Tests/TuistCacheTests/ContentHashing/SourceFilesContentHasherTests.swift b/Tests/TuistCacheTests/ContentHashing/SourceFilesContentHasherTests.swift index ff487c13a..de1bc3246 100644 --- a/Tests/TuistCacheTests/ContentHashing/SourceFilesContentHasherTests.swift +++ b/Tests/TuistCacheTests/ContentHashing/SourceFilesContentHasherTests.swift @@ -13,15 +13,15 @@ final class SourceFilesContentHasherTests: TuistUnitTestCase { private var mockContentHasher: MockContentHashing! private let sourceFile1Path = AbsolutePath("/file1") private let sourceFile2Path = AbsolutePath("/file2") - private var sourceFile1: Target.SourceFile! - private var sourceFile2: Target.SourceFile! + private var sourceFile1: SourceFile! + private var sourceFile2: SourceFile! override func setUp() { super.setUp() mockContentHasher = MockContentHashing() subject = SourceFilesContentHasher(contentHasher: mockContentHasher) - sourceFile1 = (path: sourceFile1Path, compilerFlags: "-fno-objc-arc") - sourceFile2 = (path: sourceFile2Path, compilerFlags: "-print-objc-runtime-info") + sourceFile1 = SourceFile(path: sourceFile1Path, compilerFlags: "-fno-objc-arc") + sourceFile2 = SourceFile(path: sourceFile2Path, compilerFlags: "-print-objc-runtime-info") } override func tearDown() { @@ -34,6 +34,16 @@ final class SourceFilesContentHasherTests: TuistUnitTestCase { // MARK: - Tests + func test_hash_when_the_files_have_a_hash() throws { + // When + sourceFile1 = SourceFile(path: sourceFile1Path, contentHash: "first") + sourceFile2 = SourceFile(path: sourceFile2Path, contentHash: "second") + let hash = try subject.hash(sources: [sourceFile1, sourceFile2]) + + // Then + XCTAssertEqual(hash, "first;second") + } + func test_hash_returnsSameValue() throws { // When let hash = try subject.hash(sources: [sourceFile1, sourceFile2]) diff --git a/Tests/TuistCacheTests/ContentHashing/ContentHasherTests.swift b/Tests/TuistCoreTests/ContentHashing/ContentHasherTests.swift similarity index 99% rename from Tests/TuistCacheTests/ContentHashing/ContentHasherTests.swift rename to Tests/TuistCoreTests/ContentHashing/ContentHasherTests.swift index 27beae26a..be9eefd56 100644 --- a/Tests/TuistCacheTests/ContentHashing/ContentHasherTests.swift +++ b/Tests/TuistCoreTests/ContentHashing/ContentHasherTests.swift @@ -1,7 +1,7 @@ import TSCBasic import TuistSupport import XCTest -@testable import TuistCache +@testable import TuistCore @testable import TuistSupportTesting final class ContentHasherTests: TuistUnitTestCase { diff --git a/Tests/TuistCoreTests/Models/TargetTests.swift b/Tests/TuistCoreTests/Models/TargetTests.swift index e545cd5a1..87819d48f 100644 --- a/Tests/TuistCoreTests/Models/TargetTests.swift +++ b/Tests/TuistCoreTests/Models/TargetTests.swift @@ -93,8 +93,8 @@ final class TargetTests: TuistUnitTestCase { // When let sources = try Target.sources(targetName: "Target", sources: [ - (glob: temporaryPath.appending(RelativePath("sources/**")).pathString, excluding: [], compilerFlags: nil), - (glob: temporaryPath.appending(RelativePath("sources/**")).pathString, excluding: [], compilerFlags: nil), + SourceFileGlob(glob: temporaryPath.appending(RelativePath("sources/**")).pathString, excluding: [], compilerFlags: nil), + SourceFileGlob(glob: temporaryPath.appending(RelativePath("sources/**")).pathString, excluding: [], compilerFlags: nil), ]) // Then @@ -125,9 +125,9 @@ final class TargetTests: TuistUnitTestCase { // When let sources = try Target.sources(targetName: "Target", sources: [ - (glob: temporaryPath.appending(RelativePath("sources/**")).pathString, - excluding: [temporaryPath.appending(RelativePath("sources/**/*Tests.swift")).pathString], - compilerFlags: nil), + SourceFileGlob(glob: temporaryPath.appending(RelativePath("sources/**")).pathString, + excluding: [temporaryPath.appending(RelativePath("sources/**/*Tests.swift")).pathString], + compilerFlags: nil), ]) // Then @@ -165,9 +165,9 @@ final class TargetTests: TuistUnitTestCase { // When let sources = try Target.sources(targetName: "Target", sources: [ - (glob: temporaryPath.appending(RelativePath("sources/**")).pathString, - excluding: excluding, - compilerFlags: nil), + SourceFileGlob(glob: temporaryPath.appending(RelativePath("sources/**")).pathString, + excluding: excluding, + compilerFlags: nil), ]) // Then @@ -191,9 +191,7 @@ final class TargetTests: TuistUnitTestCase { invalidGlobs: invalidGlobs) // When XCTAssertThrowsSpecific(try Target.sources(targetName: "Target", sources: [ - (glob: temporaryPath.appending(RelativePath("invalid/path/**")).pathString, - excluding: [], - compilerFlags: nil), + SourceFileGlob(glob: temporaryPath.appending(RelativePath("invalid/path/**")).pathString), ]), error) } diff --git a/Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift index 253db6c28..aab5ae71c 100644 --- a/Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/BuildPhaseGeneratorTests.swift @@ -46,9 +46,9 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let pbxproj = PBXProj() pbxproj.add(object: target) - let sources: [Target.SourceFile] = [ - ("/test/file1.swift", "flag"), - ("/test/file2.swift", nil), + let sources: [SourceFile] = [ + SourceFile(path: "/test/file1.swift", compilerFlags: "flag"), + SourceFile(path: "/test/file2.swift"), ] let fileElements = createFileElements(for: sources.map { $0.path }) @@ -111,7 +111,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { pbxproj.add(object: target) let fileElements = ProjectFileElements() - XCTAssertThrowsError(try subject.generateSourcesBuildPhase(files: [(path: path, compilerFlags: nil)], + XCTAssertThrowsError(try subject.generateSourcesBuildPhase(files: [SourceFile(path: path, compilerFlags: nil)], coreDataModels: [], pbxTarget: target, fileElements: fileElements, @@ -126,9 +126,9 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let pbxproj = PBXProj() pbxproj.add(object: target) - let sources: [Target.SourceFile] = [ - ("/path/sources/Base.lproj/OTTSiriExtension.intentdefinition", nil), - ("/path/sources/en.lproj/OTTSiriExtension.intentdefinition", nil), + let sources: [SourceFile] = [ + SourceFile(path: "/path/sources/Base.lproj/OTTSiriExtension.intentdefinition", compilerFlags: nil), + SourceFile(path: "/path/sources/en.lproj/OTTSiriExtension.intentdefinition", compilerFlags: nil), ] let fileElements = createLocalizedResourceFileElements(for: [ @@ -158,7 +158,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { pbxproj.add(object: target) let fileElements = ProjectFileElements() - XCTAssertThrowsError(try subject.generateSourcesBuildPhase(files: [(path: path, compilerFlags: nil)], + XCTAssertThrowsError(try subject.generateSourcesBuildPhase(files: [SourceFile(path: path, compilerFlags: nil)], coreDataModels: [], pbxTarget: target, fileElements: fileElements, @@ -225,7 +225,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { fileElements.elements[headerPath] = headerFileReference let target = Target.test(platform: .iOS, - sources: [(path: "/test/file.swift", compilerFlags: nil)], + sources: [SourceFile(path: "/test/file.swift", compilerFlags: nil)], headers: headers) let graph = ValueGraph.test(path: tmpDir) @@ -261,7 +261,7 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase { let target = Target.test(platform: .iOS, product: .framework, - sources: [(path: "/test/file.swift", compilerFlags: nil)], + sources: [SourceFile(path: "/test/file.swift", compilerFlags: nil)], headers: headers) let graph = ValueGraph.test(path: tmpDir) let graphTraverser = ValueGraphTraverser(graph: graph) diff --git a/Tests/TuistGeneratorTests/Generator/ProjectFileElementsTests.swift b/Tests/TuistGeneratorTests/Generator/ProjectFileElementsTests.swift index 47f5e58ba..91e1c58c3 100644 --- a/Tests/TuistGeneratorTests/Generator/ProjectFileElementsTests.swift +++ b/Tests/TuistGeneratorTests/Generator/ProjectFileElementsTests.swift @@ -291,7 +291,7 @@ final class ProjectFileElementsTests: TuistUnitTestCase { infoPlist: .file(path: AbsolutePath("/project/info.plist")), entitlements: AbsolutePath("/project/app.entitlements"), settings: settings, - sources: [(path: AbsolutePath("/project/file.swift"), compilerFlags: nil)], + sources: [SourceFile(path: AbsolutePath("/project/file.swift"))], resources: [ .file(path: AbsolutePath("/project/image.png")), .folderReference(path: AbsolutePath("/project/reference")), @@ -522,12 +522,12 @@ final class ProjectFileElementsTests: TuistUnitTestCase { let group = PBXGroup() let pbxproj = PBXProj() pbxproj.add(object: group) - _ = subject.addFileElement(from: from, - fileAbsolutePath: fileAbsolutePath, - fileRelativePath: fileRelativePath, - name: nil, - toGroup: group, - pbxproj: pbxproj) + subject.addFileElement(from: from, + fileAbsolutePath: fileAbsolutePath, + fileRelativePath: fileRelativePath, + name: nil, + toGroup: group, + pbxproj: pbxproj) let file: PBXFileReference? = group.children.first as? PBXFileReference XCTAssertEqual(file?.path, "file.swift") XCTAssertEqual(file?.sourceTree, .group) diff --git a/Tests/TuistGeneratorTests/Linter/TargetLinterTests.swift b/Tests/TuistGeneratorTests/Linter/TargetLinterTests.swift index c38b45173..e255d1c60 100644 --- a/Tests/TuistGeneratorTests/Linter/TargetLinterTests.swift +++ b/Tests/TuistGeneratorTests/Linter/TargetLinterTests.swift @@ -136,7 +136,7 @@ final class TargetLinterTests: TuistUnitTestCase { let bundle = Target.empty(platform: .iOS, product: .bundle, sources: [ - (path: "/path/to/some/source.swift", compilerFlags: nil), + SourceFile(path: "/path/to/some/source.swift"), ], resources: []) diff --git a/Tests/TuistGeneratorTests/ProjectMappers/SynthesizedResourceInterfaceProjectMapperTests.swift b/Tests/TuistGeneratorTests/ProjectMappers/SynthesizedResourceInterfaceProjectMapperTests.swift index 200da01b6..25f4080b6 100644 --- a/Tests/TuistGeneratorTests/ProjectMappers/SynthesizedResourceInterfaceProjectMapperTests.swift +++ b/Tests/TuistGeneratorTests/ProjectMappers/SynthesizedResourceInterfaceProjectMapperTests.swift @@ -3,6 +3,7 @@ import TSCBasic import TuistCore import TuistSupport import XCTest + @testable import TuistCoreTesting @testable import TuistGenerator @testable import TuistSupportTesting @@ -10,27 +11,31 @@ import XCTest final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { private var subject: SynthesizedResourceInterfaceProjectMapper! private var synthesizedResourceInterfacesGenerator: MockSynthesizedResourceInterfaceGenerator! + private var contentHasher: ContentHashing! override func setUp() { super.setUp() synthesizedResourceInterfacesGenerator = MockSynthesizedResourceInterfaceGenerator() + contentHasher = ContentHasher() subject = SynthesizedResourceInterfaceProjectMapper( - synthesizedResourceInterfacesGenerator: synthesizedResourceInterfacesGenerator + synthesizedResourceInterfacesGenerator: synthesizedResourceInterfacesGenerator, + contentHasher: contentHasher ) } override func tearDown() { super.tearDown() - + contentHasher = nil synthesizedResourceInterfacesGenerator = nil subject = nil } func test_map() throws { // Given - synthesizedResourceInterfacesGenerator.renderStub = { _, _, _ in - "" + synthesizedResourceInterfacesGenerator.renderStub = { _, _, paths in + let content = paths.last?.basename + return content ?? "" } let projectPath = try temporaryPath() @@ -92,30 +97,29 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { .file( FileDescriptor( path: derivedSourcesPath.appending(component: "Assets+TargetA.swift"), - contents: "".data(using: .utf8) + contents: "a.xcassets".data(using: .utf8) ) ), .file( FileDescriptor( path: derivedSourcesPath.appending(component: "Strings+TargetA.swift"), - contents: "".data(using: .utf8) + contents: "aStrings.strings".data(using: .utf8) ) ), .file( FileDescriptor( path: derivedSourcesPath.appending(component: "Environment.swift"), - contents: "".data(using: .utf8) + contents: "Environment.plist".data(using: .utf8) ) ), .file( FileDescriptor( path: derivedSourcesPath.appending(component: "Fonts+TargetA.swift"), - contents: "".data(using: .utf8) + contents: "ttcFont.ttc".data(using: .utf8) ) ), ] ) - XCTAssertEqual( mappedProject, Project.test( @@ -124,18 +128,22 @@ final class SynthesizedResourceInterfaceProjectMapperTests: TuistUnitTestCase { Target.test( name: targetA.name, sources: [ - (path: derivedSourcesPath + SourceFile(path: derivedSourcesPath .appending(component: "Assets+TargetA.swift"), - compilerFlags: nil), - (path: derivedSourcesPath + compilerFlags: nil, + contentHash: try contentHasher.hash("a.xcassets".data(using: .utf8)!)), + SourceFile(path: derivedSourcesPath .appending(component: "Strings+TargetA.swift"), - compilerFlags: nil), - (path: derivedSourcesPath + compilerFlags: nil, + contentHash: try contentHasher.hash("aStrings.strings".data(using: .utf8)!)), + SourceFile(path: derivedSourcesPath .appending(component: "Environment.swift"), - compilerFlags: nil), - (path: derivedSourcesPath + compilerFlags: nil, + contentHash: try contentHasher.hash("Environment.plist".data(using: .utf8)!)), + SourceFile(path: derivedSourcesPath .appending(component: "Fonts+TargetA.swift"), - compilerFlags: nil), + compilerFlags: nil, + contentHash: try contentHasher.hash("ttcFont.ttc".data(using: .utf8)!)), ], resources: targetA.resources ), diff --git a/Tests/TuistIntegrationTests/Generator/MultipleConfigurationsIntegrationTests.swift b/Tests/TuistIntegrationTests/Generator/MultipleConfigurationsIntegrationTests.swift index 3e9d1503f..ea585bbe9 100644 --- a/Tests/TuistIntegrationTests/Generator/MultipleConfigurationsIntegrationTests.swift +++ b/Tests/TuistIntegrationTests/Generator/MultipleConfigurationsIntegrationTests.swift @@ -364,7 +364,7 @@ final class MultipleConfigurationsIntegrationTests: TuistUnitTestCase { productName: "AppTarget", bundleId: "test.bundle", settings: settings, - sources: [(path: try pathTo("App/Sources/AppDelegate.swift"), compilerFlags: nil)], + sources: [SourceFile(path: try pathTo("App/Sources/AppDelegate.swift"))], filesGroup: .group(name: "ProjectGroup")) } diff --git a/Tests/TuistIntegrationTests/Generator/TestModelGenerator.swift b/Tests/TuistIntegrationTests/Generator/TestModelGenerator.swift index 484704f08..8de70648c 100644 --- a/Tests/TuistIntegrationTests/Generator/TestModelGenerator.swift +++ b/Tests/TuistIntegrationTests/Generator/TestModelGenerator.swift @@ -123,10 +123,10 @@ final class TestModelGenerator { dependencies: dependencies.map { Dependency.target(name: $0) }) } - private func createSources(path: AbsolutePath) -> [Target.SourceFile] { - let sources: [Target.SourceFile] = (0 ..< config.sources) + private func createSources(path: AbsolutePath) -> [SourceFile] { + let sources: [SourceFile] = (0 ..< config.sources) .map { "Sources/SourceFile\($0).swift" } - .map { (path: path.appending(RelativePath($0)), compilerFlags: nil) } + .map { SourceFile(path: path.appending(RelativePath($0))) } .shuffled() return sources } diff --git a/Tests/TuistKitTests/Cache/CacheControllerTests.swift b/Tests/TuistKitTests/Cache/CacheControllerTests.swift index 54ff07f38..9440458cc 100644 --- a/Tests/TuistKitTests/Cache/CacheControllerTests.swift +++ b/Tests/TuistKitTests/Cache/CacheControllerTests.swift @@ -17,7 +17,7 @@ final class CacheControllerProjectMapperProviderTests: TuistUnitTestCase { var subject: CacheControllerProjectMapperProvider! override func setUp() { - subject = CacheControllerProjectMapperProvider() + subject = CacheControllerProjectMapperProvider(contentHasher: ContentHasher()) } func test_mapper_includes_the_cache_build_phase_project_mapper() throws { diff --git a/Tests/TuistKitTests/GraphMappers/ProjectMapperProviderTests.swift b/Tests/TuistKitTests/GraphMappers/ProjectMapperProviderTests.swift index c45c271b2..b374e5c69 100644 --- a/Tests/TuistKitTests/GraphMappers/ProjectMapperProviderTests.swift +++ b/Tests/TuistKitTests/GraphMappers/ProjectMapperProviderTests.swift @@ -16,7 +16,7 @@ final class ProjectMapperProviderTests: TuistUnitTestCase { override func setUp() { super.setUp() - subject = ProjectMapperProvider() + subject = ProjectMapperProvider(contentHasher: ContentHasher()) } override func tearDown() { @@ -26,7 +26,7 @@ final class ProjectMapperProviderTests: TuistUnitTestCase { func test_mapper_returns_a_sequential_mapper_with_the_autogenerated_schemes_project_mapper() throws { // Given - subject = ProjectMapperProvider() + subject = ProjectMapperProvider(contentHasher: ContentHasher()) // When let got = subject.mapper(config: Config.test(cloud: .test(options: []))) @@ -38,7 +38,7 @@ final class ProjectMapperProviderTests: TuistUnitTestCase { func test_mappers_returns_theSigningMapper() throws { // Given - subject = ProjectMapperProvider() + subject = ProjectMapperProvider(contentHasher: ContentHasher()) // When let got = subject.mapper(config: Config.test()) @@ -50,7 +50,7 @@ final class ProjectMapperProviderTests: TuistUnitTestCase { func test_mappers_returns_resources_namespace_project_mapper() throws { // Given - subject = ProjectMapperProvider() + subject = ProjectMapperProvider(contentHasher: ContentHasher()) // When let got = subject.mapper(config: Config.test()) @@ -62,7 +62,7 @@ final class ProjectMapperProviderTests: TuistUnitTestCase { func test_mappers_does_not_returns_resources_namespace_project_mapper_when_disabled_autogenerated_namespace() throws { // Given - subject = ProjectMapperProvider() + subject = ProjectMapperProvider(contentHasher: ContentHasher()) // When let got = subject.mapper( @@ -80,7 +80,7 @@ final class ProjectMapperProviderTests: TuistUnitTestCase { func test_mappers_does_disable_show_environment_vars() throws { // Given - subject = ProjectMapperProvider() + subject = ProjectMapperProvider(contentHasher: ContentHasher()) // When let got = subject.mapper( diff --git a/Tests/TuistKitTests/GraphMappers/WorkspaceMapperProviderTests.swift b/Tests/TuistKitTests/GraphMappers/WorkspaceMapperProviderTests.swift index 8a32faefc..875463a5e 100644 --- a/Tests/TuistKitTests/GraphMappers/WorkspaceMapperProviderTests.swift +++ b/Tests/TuistKitTests/GraphMappers/WorkspaceMapperProviderTests.swift @@ -1,4 +1,5 @@ import Foundation +import TuistCache import TuistCore import TuistGenerator import XCTest @@ -11,7 +12,7 @@ final class WorkspaceMapperProviderTests: TuistUnitTestCase { override func setUp() { super.setUp() - subject = WorkspaceMapperProvider() + subject = WorkspaceMapperProvider(contentHasher: ContentHasher()) } override func tearDown() { @@ -21,7 +22,7 @@ final class WorkspaceMapperProviderTests: TuistUnitTestCase { func test_mapper_does_not_return_autogenerated_project_scheme_mapper_when_autogenerated_schemes_are_disabled() throws { // Given - subject = WorkspaceMapperProvider() + subject = WorkspaceMapperProvider(contentHasher: ContentHasher()) // When let got = subject.mappers( @@ -38,7 +39,7 @@ final class WorkspaceMapperProviderTests: TuistUnitTestCase { func test_mapper_returns_autogenerated_project_scheme_mapper() throws { // Given - subject = WorkspaceMapperProvider() + subject = WorkspaceMapperProvider(contentHasher: ContentHasher()) // When let got = subject.mappers(config: Config.test()) diff --git a/Tests/TuistKitTests/ProjectEditor/Mocks/MockProjectEditorMapper.swift b/Tests/TuistKitTests/ProjectEditor/Mocks/MockProjectEditorMapper.swift index a0779d4aa..8d12d16fc 100644 --- a/Tests/TuistKitTests/ProjectEditor/Mocks/MockProjectEditorMapper.swift +++ b/Tests/TuistKitTests/ProjectEditor/Mocks/MockProjectEditorMapper.swift @@ -13,6 +13,7 @@ final class MockProjectEditorMapper: ProjectEditorMapping { xcodeProjPath: AbsolutePath, setupPath: AbsolutePath?, configPath: AbsolutePath?, + dependenciesPath: AbsolutePath?, manifests: [AbsolutePath], helpers: [AbsolutePath], templates: [AbsolutePath], @@ -24,6 +25,7 @@ final class MockProjectEditorMapper: ProjectEditorMapping { xcodeProjPath: AbsolutePath, setupPath: AbsolutePath?, configPath: AbsolutePath?, + dependenciesPath: AbsolutePath?, manifests: [AbsolutePath], helpers: [AbsolutePath], templates: [AbsolutePath], @@ -34,6 +36,7 @@ final class MockProjectEditorMapper: ProjectEditorMapping { xcodeProjPath: xcodeProjPath, setupPath: setupPath, configPath: configPath, + dependenciesPath: dependenciesPath, manifests: manifests, helpers: helpers, templates: templates, diff --git a/Tests/TuistKitTests/ProjectEditor/ProjectEditorMapperTests.swift b/Tests/TuistKitTests/ProjectEditor/ProjectEditorMapperTests.swift index cf935f917..3f515ab9a 100644 --- a/Tests/TuistKitTests/ProjectEditor/ProjectEditorMapperTests.swift +++ b/Tests/TuistKitTests/ProjectEditor/ProjectEditorMapperTests.swift @@ -21,13 +21,14 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { super.tearDown() } - func test_edit_when_there_are_helpers_and_setup_and_config() throws { + func test_edit_when_there_are_helpers_and_setup_and_config_and_dependencies() throws { // Given let sourceRootPath = try temporaryPath() let xcodeProjPath = sourceRootPath.appending(component: "Project.xcodeproj") let manifestPaths = [sourceRootPath].map { $0.appending(component: "Project.swift") } let setupPath = sourceRootPath.appending(component: "Setup.swift") let configPath = sourceRootPath.appending(components: Constants.tuistDirectoryName, "Config.swift") + let dependenciesPath = sourceRootPath.appending(components: Constants.tuistDirectoryName, "Dependencies.swift") let helperPaths = [sourceRootPath].map { $0.appending(component: "Project+Template.swift") } let templates = [sourceRootPath].map { $0.appending(component: "template") } let projectDescriptionPath = sourceRootPath.appending(component: "ProjectDescription.framework") @@ -39,6 +40,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { xcodeProjPath: xcodeProjPath, setupPath: setupPath, configPath: configPath, + dependenciesPath: dependenciesPath, manifests: manifestPaths, helpers: helperPaths, templates: templates, @@ -47,7 +49,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { // Then let targetNodes = graph.targets.values.lazy.flatMap { targets in targets.compactMap { $0 } }.sorted(by: { $0.target.name < $1.target.name }) - XCTAssertEqual(targetNodes.count, 5) + XCTAssertEqual(targetNodes.count, 6) XCTAssertEqual(Set(targetNodes.last?.dependencies ?? []), Set(targetNodes.dropLast())) // Generated Manifests target @@ -63,7 +65,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { // Generated Helpers target let helpersTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "ProjectDescriptionHelpers" })) - XCTAssertEqual(targetNodes.dropFirst().first?.target, helpersTarget) + XCTAssertEqual(targetNodes.dropFirst().dropFirst().first?.target, helpersTarget) XCTAssertEqual(helpersTarget.name, "ProjectDescriptionHelpers") XCTAssertEqual(helpersTarget.platform, .macOS) @@ -109,6 +111,18 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { XCTAssertEqual(configTarget.filesGroup, .group(name: "Manifests")) XCTAssertEmpty(configTarget.dependencies) + // Generated Dependencies target + let dependenciesTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "Dependencies" })) + XCTAssertEqual(targetNodes.dropFirst().first?.target, dependenciesTarget) + + XCTAssertEqual(dependenciesTarget.name, "Dependencies") + XCTAssertEqual(dependenciesTarget.platform, .macOS) + XCTAssertEqual(dependenciesTarget.product, .staticFramework) + XCTAssertEqual(dependenciesTarget.settings, expectedSettings(sourceRootPath: sourceRootPath)) + XCTAssertEqual(dependenciesTarget.sources.map { $0.path }, [dependenciesPath]) + XCTAssertEqual(dependenciesTarget.filesGroup, .group(name: "Manifests")) + XCTAssertEmpty(dependenciesTarget.dependencies) + // Generated Project XCTAssertEqual(project.path, sourceRootPath) XCTAssertEqual(project.name, "Manifests") @@ -132,7 +146,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { XCTAssertEqual(runAction.arguments, Arguments(launchArguments: [generateArgument: true])) } - func test_edit_when_there_are_no_helpers_and_no_setup_and_no_config() throws { + func test_edit_when_there_are_no_helpers_and_no_setup_and_no_config_and_no_dependencies() throws { // Given let sourceRootPath = try temporaryPath() let xcodeProjPath = sourceRootPath.appending(component: "Project.xcodeproj") @@ -148,6 +162,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { xcodeProjPath: xcodeProjPath, setupPath: nil, configPath: nil, + dependenciesPath: nil, manifests: manifestPaths, helpers: helperPaths, templates: templates, @@ -199,6 +214,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { let xcodeProjPath = sourceRootPath.appending(component: "Project.xcodeproj") let setupPath = sourceRootPath.appending(component: "Setup.swift") let configPath = sourceRootPath.appending(components: Constants.tuistDirectoryName, "Config.swift") + let dependenciesPath = sourceRootPath.appending(components: Constants.tuistDirectoryName, "Dependencies.swift") let otherProjectPath = "Module" let manifestPaths = [ sourceRootPath.appending(component: "Project.swift"), @@ -215,6 +231,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { xcodeProjPath: xcodeProjPath, setupPath: setupPath, configPath: configPath, + dependenciesPath: dependenciesPath, manifests: manifestPaths, helpers: helperPaths, templates: templates, @@ -223,14 +240,13 @@ final class ProjectEditorMapperTests: TuistUnitTestCase { // Then let targetNodes = graph.targets.values.flatMap { targets in targets.compactMap { $0 } }.sorted(by: { $0.target.name < $1.target.name }) - // expecting `targetNodes == [Config, ModuleManifests, Setup, TemporaryDirectory.XXXMainifest]` - XCTAssertEqual(targetNodes.count, 4) + XCTAssertEqual(targetNodes.count, 5) XCTAssertEmpty(targetNodes.first?.dependencies ?? []) - XCTAssertEqual(Set(targetNodes.last?.dependencies ?? []), Set([targetNodes[0], targetNodes[2]])) + XCTAssertEqual(Set(targetNodes.last?.dependencies ?? []), Set([targetNodes[0], targetNodes[1], targetNodes[3]])) // Generated Manifests target let manifestOneTarget = try XCTUnwrap(project.targets.first(where: { $0.name == "ModuleManifests" })) - XCTAssertEqual(targetNodes.dropFirst().first?.target, manifestOneTarget) + XCTAssertEqual(targetNodes.dropFirst().dropFirst().first?.target, manifestOneTarget) XCTAssertEqual(manifestOneTarget.name, "ModuleManifests") XCTAssertEqual(manifestOneTarget.platform, .macOS) diff --git a/Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift b/Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift index 0ad86fe17..c33d71fc0 100644 --- a/Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift +++ b/Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift @@ -81,9 +81,15 @@ final class ProjectEditorTests: TuistUnitTestCase { try helpers.forEach { try FileHandler.shared.touch($0) } let manifests: [(Manifest, AbsolutePath)] = [(.project, directory.appending(component: "Project.swift"))] let tuistPath = AbsolutePath(ProcessInfo.processInfo.arguments.first!) + let setupPath = directory.appending(components: "Setup.swift") + let configPath = directory.appending(components: "Tuist", "Config.swift") + let dependenciesPath = directory.appending(components: "Tuist", "Dependencies.swif") resourceLocator.projectDescriptionStub = { projectDescriptionPath } manifestFilesLocator.locateProjectManifestsStub = manifests + manifestFilesLocator.locateConfigStub = configPath + manifestFilesLocator.locateDependenciesStub = dependenciesPath + manifestFilesLocator.locateSetupStub = setupPath helpersDirectoryLocator.locateStub = helpersDirectory projectEditorMapper.mapStub = (project, graph) var mappedProject: Project? @@ -107,6 +113,9 @@ final class ProjectEditorTests: TuistUnitTestCase { XCTAssertEqual(mapArgs?.helpers, helpers) XCTAssertEqual(mapArgs?.sourceRootPath, directory) XCTAssertEqual(mapArgs?.projectDescriptionPath, projectDescriptionPath) + XCTAssertEqual(mapArgs?.configPath, configPath) + XCTAssertEqual(mapArgs?.setupPath, setupPath) + XCTAssertEqual(mapArgs?.dependenciesPath, dependenciesPath) XCTAssertEqual(project, mappedProject) XCTAssertEqual(generatedProject, project) diff --git a/Tests/TuistKitTests/Services/Lint/LintCodeServiceTests.swift b/Tests/TuistKitTests/Services/Lint/LintCodeServiceTests.swift index c606105c0..fa0b38b30 100644 --- a/Tests/TuistKitTests/Services/Lint/LintCodeServiceTests.swift +++ b/Tests/TuistKitTests/Services/Lint/LintCodeServiceTests.swift @@ -101,16 +101,16 @@ final class LintCodeServiceTests: TuistUnitTestCase { manifestLoader.manifestsAtStub = { _ in Set([.workspace]) } let target01 = Target.test(sources: [ - ("/target01/file1.swift", nil), - ("/target01/file2.swift", nil), + SourceFile(path: "/target01/file1.swift", compilerFlags: nil), + SourceFile(path: "/target01/file2.swift", compilerFlags: nil), ]) let target02 = Target.test(sources: [ - ("/target02/file1.swift", nil), - ("/target02/file2.swift", nil), - ("/target02/file3.swift", nil), + SourceFile(path: "/target02/file1.swift", compilerFlags: nil), + SourceFile(path: "/target02/file2.swift", compilerFlags: nil), + SourceFile(path: "/target02/file3.swift", compilerFlags: nil), ]) let target03 = Target.test(sources: [ - ("/target03/file1.swift", nil), + SourceFile(path: "/target03/file1.swift", compilerFlags: nil), ]) let graph = Graph.test( entryPath: "/rootPath", @@ -150,16 +150,16 @@ final class LintCodeServiceTests: TuistUnitTestCase { manifestLoader.manifestsAtStub = { _ in Set([.project]) } let target01 = Target.test(sources: [ - ("/target01/file1.swift", nil), - ("/target01/file2.swift", nil), + SourceFile(path: "/target01/file1.swift", compilerFlags: nil), + SourceFile(path: "/target01/file2.swift", compilerFlags: nil), ]) let target02 = Target.test(sources: [ - ("/target02/file1.swift", nil), - ("/target02/file2.swift", nil), - ("/target02/file3.swift", nil), + SourceFile(path: "/target02/file1.swift", compilerFlags: nil), + SourceFile(path: "/target02/file2.swift", compilerFlags: nil), + SourceFile(path: "/target02/file3.swift", compilerFlags: nil), ]) let target03 = Target.test(sources: [ - ("/target03/file1.swift", nil), + SourceFile(path: "/target03/file1.swift", compilerFlags: nil), ]) let graph = Graph.test( entryPath: "/rootPath", @@ -199,16 +199,16 @@ final class LintCodeServiceTests: TuistUnitTestCase { manifestLoader.manifestsAtStub = { _ in Set([.workspace]) } let target01 = Target.test(name: "Target1", sources: [ - ("/target01/file1.swift", nil), - ("/target01/file2.swift", nil), + SourceFile(path: "/target01/file1.swift", compilerFlags: nil), + SourceFile(path: "/target01/file2.swift", compilerFlags: nil), ]) let target02 = Target.test(name: "Target2", sources: [ - ("/target02/file1.swift", nil), - ("/target02/file2.swift", nil), - ("/target02/file3.swift", nil), + SourceFile(path: "/target02/file1.swift", compilerFlags: nil), + SourceFile(path: "/target02/file2.swift", compilerFlags: nil), + SourceFile(path: "/target02/file3.swift", compilerFlags: nil), ]) let target03 = Target.test(name: "Target3", sources: [ - ("/target03/file1.swift", nil), + SourceFile(path: "/target03/file1.swift", compilerFlags: nil), ]) let graph = Graph.test(targets: [ "/path1": [.test(target: target01), .test(target: target02), .test(target: target03)], diff --git a/Tests/TuistLoaderTests/Utils/ManifestFileLocatorTests.swift b/Tests/TuistLoaderTests/Utils/ManifestFileLocatorTests.swift index 35457377d..0895ec48f 100644 --- a/Tests/TuistLoaderTests/Utils/ManifestFileLocatorTests.swift +++ b/Tests/TuistLoaderTests/Utils/ManifestFileLocatorTests.swift @@ -7,7 +7,7 @@ import XCTest @testable import TuistLoader @testable import TuistSupportTesting -final class ManifestFileLocatorTests: TuistUnitTestCase { +final class ManifestFilesLocatorTests: TuistUnitTestCase { private var subject: ManifestFilesLocator! override func setUp() { @@ -155,6 +155,104 @@ final class ManifestFileLocatorTests: TuistUnitTestCase { XCTAssertNil(configPath) } + func test_locateDependencies() throws { + // Given + let paths = try createFiles([ + "Module01/File01.swift", + "Module01/File02.swift", + "Module01/File03.swift", + + "Module02/File01.swift", + "Module02/File01.swift", + "Module02/Subdir01/File01.swift", + "Module02/Subdir01/File02.swift", + + "File01.swift", + "File02.swift", + "Tuist/Dependencies.swift", + ]) + + // When + let dependenciesPath = subject.locateDependencies(at: try temporaryPath()) + + // Then + XCTAssertNotNil(dependenciesPath) + XCTAssertEqual(paths.last, dependenciesPath) + } + + func test_locateDependencies_traversing() throws { + // Given + let paths = try createFiles([ + "Module01/File01.swift", + "Module01/File02.swift", + "Module01/File03.swift", + + "Module02/File01.swift", + "Module02/File01.swift", + "Module02/Subdir01/File01.swift", + "Module02/Subdir01/File02.swift", + + "File01.swift", + "File02.swift", + "Tuist/Dependencies.swift", + ]) + let locatingPath = paths[5] // "Module02/Subdir01/File01.swift" + + // When + let dependenciesPath = subject.locateDependencies(at: locatingPath) + + // Then + XCTAssertNotNil(dependenciesPath) + XCTAssertEqual(paths.last, dependenciesPath) + } + + func test_locateDependencies_where_config_not_exist() throws { + // Given + try createFiles([ + "Module01/File01.swift", + "Module01/File02.swift", + "Module01/File03.swift", + + "Module02/File01.swift", + "Module02/File01.swift", + "Module02/Subdir01/File01.swift", + "Module02/Subdir01/File02.swift", + + "File01.swift", + "File02.swift", + ]) + + // When + let dependenciesPath = subject.locateDependencies(at: try temporaryPath()) + + // Then + XCTAssertNil(dependenciesPath) + } + + func test_locateDependencies_traversing_where_config_not_exist() throws { + // Given + let paths = try createFiles([ + "Module01/File01.swift", + "Module01/File02.swift", + "Module01/File03.swift", + + "Module02/File01.swift", + "Module02/File01.swift", + "Module02/Subdir01/File01.swift", + "Module02/Subdir01/File02.swift", + + "File01.swift", + "File02.swift", + ]) + let locatingPath = paths[5] // "Module02/Subdir01/File01.swift" + + // When + let dependenciesPath = subject.locateDependencies(at: locatingPath) + + // Then + XCTAssertNil(dependenciesPath) + } + func test_locateSetup() throws { // Given let paths = try createFiles([ diff --git a/features/edit.feature b/features/edit.feature index 6b584b20c..c14bdf0db 100644 --- a/features/edit.feature +++ b/features/edit.feature @@ -1,6 +1,6 @@ Feature: Edit an existing project using Tuist - Scenario: The project is an application with helpers, sub projects, Config.swift and Project.swift (ios_app_with_helpers) + Scenario: The project is an application with helpers, sub projects, Config.swift, Dependencies.swift and Project.swift (ios_app_with_helpers) Given that tuist is available And I have a working directory Then I copy the fixture ios_app_with_helpers into the working directory @@ -10,4 +10,5 @@ Feature: Edit an existing project using Tuist Then I should be able to build for macOS the scheme AppKitManifests Then I should be able to build for macOS the scheme AppSupportManifests Then I should be able to build for macOS the scheme Setup - Then I should be able to build for macOS the scheme Config \ No newline at end of file + Then I should be able to build for macOS the scheme Config + Then I should be able to build for macOS the scheme Dependencies diff --git a/features/list-targets.feature b/features/list-targets.feature new file mode 100644 index 000000000..6a9128ead --- /dev/null +++ b/features/list-targets.feature @@ -0,0 +1,10 @@ +Feature: List targets sorted by number of dependencies + + Scenario: The project is the microfeature fixture + Given that tuist is available + And I have a working directory + Then I copy the fixture ios_workspace_with_microfeature_architecture into the working directory + Then run tuist migration list-targets for UIComponents in ios_workspace_with_microfeature_architecture matches list-targets-ui-components.json + Then run tuist migration list-targets for Core in ios_workspace_with_microfeature_architecture matches list-targets-core.json + Then run tuist migration list-targets for Data in ios_workspace_with_microfeature_architecture matches list-targets-data.json + diff --git a/features/resources/list-targets-core.json b/features/resources/list-targets-core.json new file mode 100644 index 000000000..a36cd3d4f --- /dev/null +++ b/features/resources/list-targets-core.json @@ -0,0 +1,10 @@ +[ + { + "dependenciesCount" : 0, + "targetName" : "Core" + }, + { + "dependenciesCount" : 2, + "targetName" : "CoreTests" + } +] diff --git a/features/resources/list-targets-data.json b/features/resources/list-targets-data.json new file mode 100644 index 000000000..83c6f9d02 --- /dev/null +++ b/features/resources/list-targets-data.json @@ -0,0 +1,10 @@ +[ + { + "dependenciesCount" : 1, + "targetName" : "Data" + }, + { + "dependenciesCount" : 2, + "targetName" : "DataTests" + } +] diff --git a/features/resources/list-targets-ui-components.json b/features/resources/list-targets-ui-components.json new file mode 100644 index 000000000..9092d3d5b --- /dev/null +++ b/features/resources/list-targets-ui-components.json @@ -0,0 +1,10 @@ +[ + { + "dependenciesCount" : 1, + "targetName" : "UIComponents" + }, + { + "dependenciesCount" : 2, + "targetName" : "UIComponentsTests" + } +] diff --git a/features/step_definitions/migration.rb b/features/step_definitions/migration.rb new file mode 100644 index 000000000..283c6bb97 --- /dev/null +++ b/features/step_definitions/migration.rb @@ -0,0 +1,12 @@ +Then(/run tuist migration list-targets for (.+) in ios_workspace_with_microfeature_architecture matches (.+)$/) do |framework, json_file| + fixtures_path = File.expand_path("../../fixtures", __dir__) + fixture_path = File.join(fixtures_path, "ios_workspace_with_microfeature_architecture/Frameworks/#{framework}Framework/#{framework}.xcodeproj/") + resources_path = File.expand_path("../resources", __dir__) + expected_json = File.read("#{resources_path}/#{json_file}") + + assert(false, "Project #{fixture_path} not found") unless File.exist?(fixture_path) + + out, s = Open3.capture2("swift", "run", "tuist", "migration", "list-targets", "-p", fixture_path) + + assert(out.include?(expected_json)) +end diff --git a/fixtures/ios_app_with_helpers/Tuist/Dependencies.swift b/fixtures/ios_app_with_helpers/Tuist/Dependencies.swift new file mode 100644 index 000000000..14bef5ea1 --- /dev/null +++ b/fixtures/ios_app_with_helpers/Tuist/Dependencies.swift @@ -0,0 +1,5 @@ +import ProjectDescription + +let dependencies = Dependencies([ + .carthage(name: "Alamofire", requirement: .exact("5.3.0")), +]) diff --git a/website/markdown/docs/commands/migration.mdx b/website/markdown/docs/commands/migration.mdx index c04d90c46..2f382487e 100644 --- a/website/markdown/docs/commands/migration.mdx +++ b/website/markdown/docs/commands/migration.mdx @@ -79,3 +79,26 @@ tuist migration check-empty-settings -p Project.xcodeproj -t MyApp }, ]} /> + +## List targets sorted by dependencies + +Migration of big Xcode projects to Tuist can happen iteratively, one target at a time. It makes sense to start from the target with the lowest number of dependencies. +To help with that, Tuist includes a command that lists the targets of a project sorted by number dependencies ascending. The count only includes dependencies that are declared in build phases. + +```bash +tuist migration list-targets -p Project.xcodeproj +``` + +#### Arguments + + diff --git a/website/markdown/docs/usage/adoption-guidelines.mdx b/website/markdown/docs/usage/adoption-guidelines.mdx index 2b9e23938..6ca16bd8c 100644 --- a/website/markdown/docs/usage/adoption-guidelines.mdx +++ b/website/markdown/docs/usage/adoption-guidelines.mdx @@ -53,10 +53,15 @@ After extracting the build settings into `.xcconfig` files, we recommend adding tuist migration check-empty-settings -p Project.xcodeproj -t MyApp ``` -#### Migrate the most independent projects first +#### Migrate the most independent targets first -Those are usually simpler and contain fewer dependencies than the rest. +Those are usually simpler since they contain fewer dependencies than the rest. That makes them good candidates from which we can start the migration. +Use this command to list the targets of a project, sorted by number of dependencies. We recommend starting from the top, with the target that has the lowest number of dependencies + +``` +tuist migration list-targets -p Project.xcodeproj +``` #### Remove broken references diff --git a/website/package.json b/website/package.json index b805e9f43..2c9613454 100644 --- a/website/package.json +++ b/website/package.json @@ -6,12 +6,12 @@ "author": "Pedro Piñera ", "dependencies": { "@brainhubeu/react-carousel": "^1.19.26", - "@emotion/core": "^10.0.35", + "@emotion/core": "^10.1.1", "@fortawesome/fontawesome-svg-core": "^1.2.32", "@fortawesome/free-brands-svg-icons": "^5.15.1", "@fortawesome/free-regular-svg-icons": "^5.15.1", "@fortawesome/free-solid-svg-icons": "^5.15.1", - "@fortawesome/react-fontawesome": "^0.1.11", + "@fortawesome/react-fontawesome": "^0.1.12", "@mdx-js/mdx": "^1.6.19", "@mdx-js/react": "^1.6.19", "@mdx-js/tag": "^0.20.3", @@ -26,26 +26,26 @@ "gatsby": "^2.24.89", "gatsby-image": "^2.4.21", "gatsby-plugin-favicon": "3.1.6", - "gatsby-plugin-feed": "^2.5.19", + "gatsby-plugin-feed": "^2.5.20", "gatsby-plugin-google-analytics": "^2.3.19", - "gatsby-plugin-manifest": "2.4.36", - "gatsby-plugin-mdx": "^1.2.51", + "gatsby-plugin-manifest": "2.4.37", + "gatsby-plugin-mdx": "^1.2.53", "gatsby-plugin-meta-redirect": "^1.1.1", - "gatsby-plugin-netlify": "^2.3.24", + "gatsby-plugin-netlify": "^2.3.25", "gatsby-plugin-next-seo": "^1.6.1", - "gatsby-plugin-offline": "^3.2.37", + "gatsby-plugin-offline": "^3.2.38", "gatsby-plugin-purgecss": "^5.0.0", "gatsby-plugin-react-helmet": "^3.3.14", "gatsby-plugin-react-svg": "^3.0.0", "gatsby-plugin-robots-txt": "^1.5.3", - "gatsby-plugin-sharp": "2.6.43", + "gatsby-plugin-sharp": "2.6.44", "gatsby-plugin-sitemap": "^2.4.17", "gatsby-plugin-theme-ui": "^0.3.0", "gatsby-redirect-from": "^0.2.4", "gatsby-remark-check-links": "^2.1.0", "gatsby-remark-copy-linked-files": "^2.3.19", - "gatsby-remark-embedder": "^4.0.0", - "gatsby-remark-images": "^3.3.39", + "gatsby-remark-embedder": "^4.1.0", + "gatsby-remark-images": "^3.3.40", "gatsby-remark-smartypants": "^2.3.13", "gatsby-source-filesystem": "^2.3.37", "gatsby-theme-tailwindcss": "^1.1.0", @@ -77,9 +77,9 @@ "react-use-visibility": "^0.3.0", "remark-slug": "^6.0.0", "semantic-ui-react": "^2.0.1", - "slug": "^4.0.1", + "slug": "^4.0.2", "theme-ui": "^0.3.1", - "twin.macro": "^1.10.0", + "twin.macro": "^1.12.0", "url-join": "^4.0.1" }, "keywords": [ diff --git a/website/yarn.lock b/website/yarn.lock index b0eb5f86f..88bcb6269 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -1073,10 +1073,10 @@ "@emotion/utils" "0.11.3" "@emotion/weak-memoize" "0.2.5" -"@emotion/core@^10.0.0", "@emotion/core@^10.0.14", "@emotion/core@^10.0.27", "@emotion/core@^10.0.35", "@emotion/core@^10.0.9": - version "10.0.35" - resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.35.tgz#513fcf2e22cd4dfe9d3894ed138c9d7a859af9b3" - integrity sha512-sH++vJCdk025fBlRZSAhkRlSUoqSqgCzYf5fMOmqqi3bM6how+sQpg3hkgJonj8GxXM4WbD7dRO+4tegDB9fUw== +"@emotion/core@^10.0.0", "@emotion/core@^10.0.14", "@emotion/core@^10.0.27", "@emotion/core@^10.0.9", "@emotion/core@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.1.1.tgz#c956c1365f2f2481960064bcb8c4732e5fb612c3" + integrity sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA== dependencies: "@babel/runtime" "^7.5.5" "@emotion/cache" "^10.0.27" @@ -1225,10 +1225,10 @@ dependencies: "@fortawesome/fontawesome-common-types" "^0.2.32" -"@fortawesome/react-fontawesome@^0.1.11": - version "0.1.11" - resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.11.tgz#c1a95a2bdb6a18fa97b355a563832e248bf6ef4a" - integrity sha512-sClfojasRifQKI0OPqTy8Ln8iIhnxR/Pv/hukBhWnBz9kQRmqi6JSH3nghlhAY7SUeIIM7B5/D2G8WjX0iepVg== +"@fortawesome/react-fontawesome@^0.1.12": + version "0.1.12" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.12.tgz#fbdea86e8b73032895e6ded1ee1dbb1874902d1a" + integrity sha512-kV6HtqotM3K4YIXlTVvomuIi6QgGCvYm++ImyEx2wwgmSppZ6kbbA29ASwjAUBD63j2OFU0yoxeXpZkjrrX0qQ== dependencies: prop-types "^15.7.2" @@ -7765,16 +7765,16 @@ gatsby-plugin-favicon@3.1.6: html-react-parser "^0.6.4" lodash "^4.17.11" -gatsby-plugin-feed@^2.5.19: - version "2.5.19" - resolved "https://registry.yarnpkg.com/gatsby-plugin-feed/-/gatsby-plugin-feed-2.5.19.tgz#fab25c72f760457cc5e98f6a34309db8950f38fa" - integrity sha512-W79GwBFNdt7U/FiKHKKKOFVlPLqGaI7VchE4yMt4NXj+p2mUNDY+JwRy98/wKaRfDCWyvL4ulmaVaEH7EVqaQw== +gatsby-plugin-feed@^2.5.20: + version "2.5.20" + resolved "https://registry.yarnpkg.com/gatsby-plugin-feed/-/gatsby-plugin-feed-2.5.20.tgz#a4dba4eddb4786028956933853382872a0adc362" + integrity sha512-JNSPLddBuu12Le6/0Es8KcgZgqy40+BmgcFGVPDBdT3oRsp/AtWWNMDvOoXU+avdObzyB3l1IkMbSBSmUxTSMg== dependencies: "@babel/runtime" "^7.11.2" "@hapi/joi" "^15.1.1" common-tags "^1.8.0" fs-extra "^8.1.0" - gatsby-plugin-utils "^0.2.39" + gatsby-plugin-utils "^0.2.40" lodash.merge "^4.6.2" rss "^1.2.2" @@ -7786,21 +7786,21 @@ gatsby-plugin-google-analytics@^2.3.19: "@babel/runtime" "^7.11.2" minimatch "3.0.4" -gatsby-plugin-manifest@2.4.36: - version "2.4.36" - resolved "https://registry.yarnpkg.com/gatsby-plugin-manifest/-/gatsby-plugin-manifest-2.4.36.tgz#d4923df481dd5cb527b51bf78c807b1784777445" - integrity sha512-HedqeBVR9WcZeVMUb07GqRvO04RUbo1eWxY1URdfVaNrRHIz2KjAceVJVghSbHJRkbl4aSrkKc19aW5GlnBNqg== +gatsby-plugin-manifest@2.4.37: + version "2.4.37" + resolved "https://registry.yarnpkg.com/gatsby-plugin-manifest/-/gatsby-plugin-manifest-2.4.37.tgz#36ec84cc09f9ee6f872b82e7397570da294878d5" + integrity sha512-Gub8QanC6lwkF5PLDUhZm6AGYSjjriVFWIF97d9TS9c5ofS/wlwHdnc7i3VozyWwIDjfKSaUyOZyHKVyB9vVMA== dependencies: "@babel/runtime" "^7.11.2" gatsby-core-utils "^1.3.23" - gatsby-plugin-utils "0.2.39" + gatsby-plugin-utils "^0.2.40" semver "^7.3.2" sharp "^0.25.4" -gatsby-plugin-mdx@^1.2.51: - version "1.2.51" - resolved "https://registry.yarnpkg.com/gatsby-plugin-mdx/-/gatsby-plugin-mdx-1.2.51.tgz#d1285505026011bc3d75ae26d32cde9cab03d106" - integrity sha512-wddtB88h4aHhS+tjNWYKT848S7LHuh2/svMg+7qdl0nL4D0tvNua76OUZxGUZDtkf0y8ajcAufps4qGeYA9mVQ== +gatsby-plugin-mdx@^1.2.53: + version "1.2.53" + resolved "https://registry.yarnpkg.com/gatsby-plugin-mdx/-/gatsby-plugin-mdx-1.2.53.tgz#09e3c07f2169a7c01804ed6822e8ded3e2952270" + integrity sha512-hh/+0R0nKjJwUkufRo2Hx4ijRwXzUdV2E0Wk6q7tHj7QUWrUOaHYLbH8x0V31Vi7xqG389tWuTUzPx6Z4gE54w== dependencies: "@babel/core" "^7.11.6" "@babel/generator" "^7.11.6" @@ -7846,10 +7846,10 @@ gatsby-plugin-meta-redirect@^1.1.1: dependencies: fs-extra "^7.0.0" -gatsby-plugin-netlify@^2.3.24: - version "2.3.24" - resolved "https://registry.yarnpkg.com/gatsby-plugin-netlify/-/gatsby-plugin-netlify-2.3.24.tgz#468bda55b28234b99dadd007e2b2b7285db90d44" - integrity sha512-OJgV1XNbffLt7C9zzOviC9oS3Yvfv5pV908H54p0PB37yP4ohfKF66NEyO0svWBYBZdf2QpeUUXJdn0ji4/AAQ== +gatsby-plugin-netlify@^2.3.25: + version "2.3.25" + resolved "https://registry.yarnpkg.com/gatsby-plugin-netlify/-/gatsby-plugin-netlify-2.3.25.tgz#24b297c342922d5f4a247eac493417be9edddd85" + integrity sha512-iPFGzobb8tn13i3nIdF1o79UWrgzz4xarI1DN4mpG87SJQ170dFMVM42MrpO4TXlYG4gFgsgcZHG6PRP6vzvcw== dependencies: "@babel/runtime" "^7.11.2" fs-extra "^8.1.0" @@ -7867,10 +7867,10 @@ gatsby-plugin-next-seo@^1.6.1: schema-dts "0.6.0" type-fest "^0.15.1" -gatsby-plugin-offline@^3.2.37: - version "3.2.37" - resolved "https://registry.yarnpkg.com/gatsby-plugin-offline/-/gatsby-plugin-offline-3.2.37.tgz#2c57f390425264cdc670c7b039cd1ca9857ea264" - integrity sha512-GwutE/t5t1SlPv8tQPboxGdwk2ERMFoVLE/LAMgLKnt+SonDk3kLd2+j7qwyA60qdVjyET1gGa2296gyPnLPsw== +gatsby-plugin-offline@^3.2.38: + version "3.2.38" + resolved "https://registry.yarnpkg.com/gatsby-plugin-offline/-/gatsby-plugin-offline-3.2.38.tgz#17d097f468a5fb3f080dfed5fbd52525ecfa10dd" + integrity sha512-9HbElk+9oPzM1baSx8z0+9EhBt3L0uA7aL9u0VC+xLSBXxJ16mD6+wczFl6C007udCxpHSUGl/neK3tjLunnXA== dependencies: "@babel/runtime" "^7.11.2" cheerio "^1.0.0-rc.3" @@ -7933,10 +7933,10 @@ gatsby-plugin-robots-txt@^1.5.3: "@babel/runtime" "^7.11.2" generate-robotstxt "^8.0.3" -gatsby-plugin-sharp@2.6.43: - version "2.6.43" - resolved "https://registry.yarnpkg.com/gatsby-plugin-sharp/-/gatsby-plugin-sharp-2.6.43.tgz#dd7f4fcf848337e64a014f491318f61d530a38a8" - integrity sha512-/V5T8SzS/GgpQEgp34blLwzwpc+dxJJVPRXGBOZtKdcTDRAfCYuY2IuRf5FJa65lSuPRQmHdG6AoQIka/0xnWA== +gatsby-plugin-sharp@2.6.44: + version "2.6.44" + resolved "https://registry.yarnpkg.com/gatsby-plugin-sharp/-/gatsby-plugin-sharp-2.6.44.tgz#cb30c41b2488639e060e461f819f8f5026bfc28d" + integrity sha512-GhTzNMmdcJtVQirba97P6r6IEQvTSTHnWzbskW8g0TsaZ+Wx20/uFnDZCquT4F8EzaqM626RqK53vUmiXFHMGw== dependencies: "@babel/runtime" "^7.11.2" async "^3.2.0" @@ -7986,10 +7986,10 @@ gatsby-plugin-typescript@^2.4.24: "@babel/runtime" "^7.11.2" babel-plugin-remove-graphql-queries "^2.9.20" -gatsby-plugin-utils@0.2.39, gatsby-plugin-utils@^0.2.39: - version "0.2.39" - resolved "https://registry.yarnpkg.com/gatsby-plugin-utils/-/gatsby-plugin-utils-0.2.39.tgz#aa0c5e9469977b476884d53fe97b28fd771f2a78" - integrity sha512-Ar6m9hjWodd4+AwHQZYBe08XGmYS1t5nghZdyKGaBcdMZRP3GGmoeyU7LH/bdkvJFf0dyoY1Me0oJV4ZNT6Abg== +gatsby-plugin-utils@^0.2.39, gatsby-plugin-utils@^0.2.40: + version "0.2.40" + resolved "https://registry.yarnpkg.com/gatsby-plugin-utils/-/gatsby-plugin-utils-0.2.40.tgz#20e997d10efb9a0368270f79ce2e6001346f6336" + integrity sha512-RKjmpPhmi8TDR9hAKxmD4ZJMje3BLs6nt6mxMWT0F8gf5giCYEywplJikyCvaPfuyaFlq1hMmFaVvzmeZNussg== dependencies: joi "^17.2.1" @@ -8091,20 +8091,20 @@ gatsby-remark-copy-linked-files@^2.3.19: probe-image-size "^5.0.0" unist-util-visit "^1.4.1" -gatsby-remark-embedder@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/gatsby-remark-embedder/-/gatsby-remark-embedder-4.0.0.tgz#ed0e2ddc669d21b4ba8e2cdffbd3f79aa5ee1102" - integrity sha512-nBVxhnuKcZZaibN6ipRKiAQibRcEJRBC6fuF2JW9op+W/uvgpqkaIzvH/JSKzESDQWX/eg+MnR8UAKsfR/6BKA== +gatsby-remark-embedder@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/gatsby-remark-embedder/-/gatsby-remark-embedder-4.1.0.tgz#3c08c33e09625fde3b0fa4c6c1a55e36fd5c9b1c" + integrity sha512-cp0Q1SDBniW57s6FB0prTM8n+yyT+e/zYKcDISHXvRWNGG/T9eRDDl99f4mQjKjWkA23QXihh/+4BfBrIwcwvQ== dependencies: "@babel/runtime" "^7.12.1" fetch-retry "^4.0.1" node-fetch "^2.6.1" unist-util-visit "^2.0.3" -gatsby-remark-images@^3.3.39: - version "3.3.39" - resolved "https://registry.yarnpkg.com/gatsby-remark-images/-/gatsby-remark-images-3.3.39.tgz#a356968fd62cdf2a511cf7624a79b60c70b7ed36" - integrity sha512-nAP/xOo8MTwYmWrA2xesoLyBoYrdpqYuiYd/N5c+IWExD+u6ARG2WdUGG9hJAyT32GACUxgLDaKqSvMSD2u2cg== +gatsby-remark-images@^3.3.40: + version "3.3.40" + resolved "https://registry.yarnpkg.com/gatsby-remark-images/-/gatsby-remark-images-3.3.40.tgz#da605c42de282225a1a133bbfb0aadbecaf47071" + integrity sha512-GVFvGdOOApG5SaNqPgbTqfiXoaf07e3bIa1pZgIyy8lC0EpL6Fq8RDtcVz+Xu+fzxEMbRJGdTF7bXxAkJ6Er0w== dependencies: "@babel/runtime" "^7.11.2" chalk "^2.4.2" @@ -15118,10 +15118,10 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" -slug@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/slug/-/slug-4.0.1.tgz#24775a0fbb7fabae2f94e34bdfab0a32ed3bc1ba" - integrity sha512-g3eofiaQdlNRrjGSW+gtSHmAdE8Oq/tFIrFlP6oQ4AW4M3l3FegO2C4TV6MM4eHdnpllJJdgG892Vt/KqpV9kQ== +slug@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/slug/-/slug-4.0.2.tgz#35a62b4e71582778ac08bb30a1bf439fd0a43ea7" + integrity sha512-c5XbWkwxHU13gAdSvBHQgnGy2sxv/REMz0ugcM0SOSBCO/N4wfU0TDBC3pgdOwVGjZwGnLBTRljXzdVYE+KYNw== slugify@^1.4.4: version "1.4.5" @@ -16010,10 +16010,10 @@ tailwind.macro@^1.0.0-alpha.10: dset "^2.0.1" tailwindcss "^1.0.0-beta.8" -tailwindcss@^1.0.0-beta.8, tailwindcss@^1.2.0, tailwindcss@^1.8.8: - version "1.8.9" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.8.9.tgz#86aa05f019b4975675d1c67a2c9962d9c833ea13" - integrity sha512-VgoBxIyF+Ipf+x3eDz3tPA8A5blGBPkswA2xM6fxCmAwAmhlIr9Zk3exft44BUf3q9mxorvjCRtxBrklfPDSoQ== +tailwindcss@^1.0.0-beta.8, tailwindcss@^1.2.0, tailwindcss@^1.9.6: + version "1.9.6" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.9.6.tgz#0c5089911d24e1e98e592a31bfdb3d8f34ecf1a0" + integrity sha512-nY8WYM/RLPqGsPEGEV2z63riyQPcHYZUJpAwdyBzVpxQHOHqHE+F/fvbCeXhdF1+TA5l72vSkZrtYCB9hRcwkQ== dependencies: "@fullhuman/postcss-purgecss" "^2.1.2" autoprefixer "^9.4.5" @@ -16449,10 +16449,10 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -twin.macro@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/twin.macro/-/twin.macro-1.10.0.tgz#b13d0934457d4cae54f1504a3a2ed744ed603960" - integrity sha512-+K9xvBvlx7iQ+CRatqNO/3VVV2+D+rbxexViKFtkjlICd+7A9hp5/8IOQ3SUPTQp80Ouist3Zcs/89quSLaoZg== +twin.macro@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/twin.macro/-/twin.macro-1.12.0.tgz#3917743cee5e0693df9679ab182aba4de962be45" + integrity sha512-KGRed9d42MkZDIS4wnCvUp5PsPnRx3eaMpvTAOQTz7/K7ArELhg/+SrbJxniQWh6QtRm7/LotlzRfvVW3/qGHQ== dependencies: "@babel/parser" "^7.10.2" babel-plugin-macros "^2.8.0" @@ -16463,7 +16463,7 @@ twin.macro@^1.10.0: dset "^2.0.1" lodash.merge "^4.6.2" string-similarity "^4.0.1" - tailwindcss "^1.8.8" + tailwindcss "^1.9.6" timsort "^0.3.0" type-check@~0.3.2: