Merge branch 'main' into cli_tool

This commit is contained in:
Jakub Olejnik 2020-11-05 05:29:27 +01:00
commit 9916aa3d40
100 changed files with 1887 additions and 344 deletions

View File

@ -35,3 +35,4 @@ jobs:
uses: fortmarek/tapestry-action@0.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}

View File

@ -1 +1,8 @@
version = 1
version = 1
[update]
always = true # default: false
require_automerge_label = false # default: true
[approve]
auto_approve_usernames = ["dependabot"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
import Foundation
public extension Dependency {
enum Manager: String, Codable, Equatable {
case carthage
// case spm
// case cocoapods
}
}

View File

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

View File

@ -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<T: AsyncQueueEvent>(event: T) -> Completable
/// Deletes the given event from disk.
/// - Parameter event: Event to be deleted.
func delete<T: AsyncQueueEvent>(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<T: AsyncQueueEvent>(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<T: AsyncQueueEvent>(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<T: AsyncQueueEvent>(event: T) -> String {
"\(Int(event.date.timeIntervalSince1970)).\(event.dispatcherId).\(event.id.uuidString).json"
}
}

View File

@ -0,0 +1,2 @@
import TuistSupport
let logger = Logger(label: "io.tuist.async-queue")

View File

@ -0,0 +1,10 @@
import Foundation
import Queuer
public protocol Queuing {
func addOperation(_ operation: Operation)
func resume()
func waitUntilAllOperationsAreFinished()
}
extension Queuer: Queuing {}

View File

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

View File

@ -0,0 +1,64 @@
import Foundation
import RxSwift
import TuistAsyncQueue
import TuistCore
public final class MockAsyncQueuePersistor<U: AsyncQueueEvent>: 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<T: AsyncQueueEvent>(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<T: AsyncQueueEvent>(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
}
}

View File

@ -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<T: AsyncQueueEvent>(event: T) {
invokedDispatch = true
invokedDispatchCount += 1
invokedDispatchParameters = (event, ())
invokedDispatchParametersList.append((event, ()))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,11 +34,12 @@ public class CacheMapper: GraphMapping {
public convenience init(config: Config,
cacheStorageProvider: CacheStorageProviding,
sources: Set<String>,
cacheOutputType: CacheOutputType)
cacheOutputType: CacheOutputType,
contentHasher: ContentHashing)
{
self.init(config: config,
cache: Cache(storageProvider: cacheStorageProvider),
graphContentHasher: GraphContentHasher(),
graphContentHasher: GraphContentHasher(contentHasher: contentHasher),
sources: sources,
cacheOutputType: cacheOutputType)
}

View File

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

View File

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

View File

@ -0,0 +1,6 @@
import Foundation
import TSCBasic
public protocol FileContentHashing {
func hash(path: AbsolutePath) throws -> String
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -152,7 +152,7 @@ final class BuildPhaseGenerator: BuildPhaseGenerating {
}
}
func generateSourcesBuildPhase(files: [Target.SourceFile],
func generateSourcesBuildPhase(files: [SourceFile],
coreDataModels: [CoreDataModel],
pbxTarget: PBXTarget,
fileElements: ProjectFileElements,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,12 +7,15 @@ final class FocusGraphMapperProvider: GraphMapperProviding {
private let cacheSources: Set<String>
private let cache: Bool
private let cacheOutputType: CacheOutputType
private let contentHasher: ContentHashing
init(cache: Bool,
init(contentHasher: ContentHashing,
cache: Bool,
cacheSources: Set<String>,
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())
}

View File

@ -13,11 +13,19 @@ protocol FocusServiceProjectGeneratorFactorying {
}
final class FocusServiceProjectGeneratorFactory: FocusServiceProjectGeneratorFactorying {
init() {}
func generator(sources: Set<String>, 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())
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,4 +8,5 @@ if CommandLine.arguments.contains("--verbose") { try? ProcessEnv.setVar(Constant
LogOutput.bootstrap()
import TuistKit
TuistCommand.main()

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import TuistCacheTesting
import TuistSupport
import XCTest
@testable import TuistCache
@testable import TuistCoreTesting
@testable import TuistSupportTesting
final class CacheContentHasherTests: TuistUnitTestCase {

View File

@ -12,7 +12,7 @@ final class GraphContentHasherTests: TuistUnitTestCase {
override func setUp() {
super.setUp()
subject = GraphContentHasher()
subject = GraphContentHasher(contentHasher: ContentHasher())
}
override func tearDown() {

View File

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

View File

@ -1,7 +1,7 @@
import TSCBasic
import TuistSupport
import XCTest
@testable import TuistCache
@testable import TuistCore
@testable import TuistSupportTesting
final class ContentHasherTests: TuistUnitTestCase {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
Then I should be able to build for macOS the scheme Config
Then I should be able to build for macOS the scheme Dependencies

View File

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

View File

@ -0,0 +1,10 @@
[
{
"dependenciesCount" : 0,
"targetName" : "Core"
},
{
"dependenciesCount" : 2,
"targetName" : "CoreTests"
}
]

View File

@ -0,0 +1,10 @@
[
{
"dependenciesCount" : 1,
"targetName" : "Data"
},
{
"dependenciesCount" : 2,
"targetName" : "DataTests"
}
]

View File

@ -0,0 +1,10 @@
[
{
"dependenciesCount" : 1,
"targetName" : "UIComponents"
},
{
"dependenciesCount" : 2,
"targetName" : "UIComponentsTests"
}
]

View File

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

View File

@ -0,0 +1,5 @@
import ProjectDescription
let dependencies = Dependencies([
.carthage(name: "Alamofire", requirement: .exact("5.3.0")),
])

View File

@ -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
<ArgumentsTable
args={[
{
long: '`--xcodeproj-path`',
short: '`-p`',
description:
'Path to the Xcode project whose build settings will be checked.',
required: true,
}
]}
/>

View File

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

View File

@ -6,12 +6,12 @@
"author": "Pedro Piñera <pedro@ppinera.es>",
"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": [

View File

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