Merge remote-tracking branch 'origin/master' into signing

This commit is contained in:
Marek Fořt 2020-03-16 21:22:11 +01:00
commit 74ab15d04d
246 changed files with 5845 additions and 3293 deletions

View File

@ -7,6 +7,13 @@ update_configs:
- 'tuist/core' - 'tuist/core'
default_labels: default_labels:
- 'dependencies' - 'dependencies'
automerged_updates:
- match:
dependency_type: 'development'
update_type: 'semver:minor'
- match:
dependency_type: 'production'
update_type: 'semver:minor'
- package_manager: 'javascript' - package_manager: 'javascript'
directory: '/website' directory: '/website'
update_schedule: 'weekly' update_schedule: 'weekly'
@ -14,3 +21,10 @@ update_configs:
- 'tuist/core' - 'tuist/core'
default_labels: default_labels:
- 'dependencies' - 'dependencies'
automerged_updates:
- match:
dependency_type: 'development'
update_type: 'semver:minor'
- match:
dependency_type: 'production'
update_type: 'semver:minor'

View File

@ -45,12 +45,3 @@ jobs:
uses: norio-nomura/action-swiftlint@3c67ce2e382be797d968883944140ffa0113f737 uses: norio-nomura/action-swiftlint@3c67ce2e382be797d968883944140ffa0113f737
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
changelog:
name: Changelog
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Changelog Reminder
uses: peterjgrainger/action-changelog-reminder@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -4,13 +4,31 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
## Next ## Next
### Changed
- Optimize `TargetNode`'s set operations https://github.com/tuist/tuist/pull/1095 by @kwridan
- Optimize `BuildPhaseGenerator`'s method of detecting assets and localized files https://github.com/tuist/tuist/pull/1094 by @kwridan
## 1.4.0
### Fixed ### Fixed
- Fix `TargetAction` when `PROJECT_DIR` includes a space https://github.com/tuist/tuist/pull/1037 by @fortmarek - Fix `TargetAction` when `PROJECT_DIR` includes a space https://github.com/tuist/tuist/pull/1037 by @fortmarek
- Fix code example compilation issues in "Project description helpers" documentation https://github.com/tuist/tuist/pull/1081 by @chojnac
### Added ### Added
- New `ProjectDescription` models for `scaffold` command https://github.com/tuist/tuist/pull/1082 by @fortmarek
- Allow specifying Project Organization name via new `organizationName` parameter to `Project` initializer or via `Config` new GenerationOption. https://github.com/tuist/tuist/pull/1062 by @c0diq
- `tuist lint` command https://github.com/tuist/tuist/pull/1043 by @pepibumur. - `tuist lint` command https://github.com/tuist/tuist/pull/1043 by @pepibumur.
- Add `--verbose` https://github.com/tuist/tuist/pull/1027 by @ollieatkinson.
- `TuistInsights` target https://github.com/tuist/tuist/pull/1084 by @pepibumur.
- Add `cloudURL` attribute to `Config` https://github.com/tuist/tuist/pull/1085 by @pepibumur.
### Changed
- Rename `TuistConfig.swift` to `Config.swift` https://github.com/tuist/tuist/pull/1083 by @pepibumur.
- Generator update - leveraging intermediate descriptors https://github.com/tuist/tuist/pull/1007 by @kwridan
- Note: `TuistGenerator.Generator` is now deprecated and will be removed in a future version of Tuist.
## 1.3.0 ## 1.3.0

View File

@ -18,10 +18,10 @@ GEM
builder (3.2.3) builder (3.2.3)
byebug (11.1.1) byebug (11.1.1)
claide (1.0.3) claide (1.0.3)
cocoapods (1.9.0) cocoapods (1.9.1)
activesupport (>= 4.0.2, < 5) activesupport (>= 4.0.2, < 5)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.9.0) cocoapods-core (= 1.9.1)
cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.2.2, < 2.0) cocoapods-downloader (>= 1.2.2, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0)
@ -37,7 +37,7 @@ GEM
nap (~> 1.0) nap (~> 1.0)
ruby-macho (~> 1.4) ruby-macho (~> 1.4)
xcodeproj (>= 1.14.0, < 2.0) xcodeproj (>= 1.14.0, < 2.0)
cocoapods-core (1.9.0) cocoapods-core (1.9.1)
activesupport (>= 4.0.2, < 6) activesupport (>= 4.0.2, < 6)
algoliasearch (~> 1.0) algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1) concurrent-ruby (~> 1.1)

View File

@ -100,6 +100,15 @@
"version": "0.2.0" "version": "0.2.0"
} }
}, },
{
"package": "swift-log",
"repositoryURL": "https://github.com/apple/swift-log.git",
"state": {
"branch": null,
"revision": "74d7b91ceebc85daf387ebb206003f78813f71aa",
"version": "1.2.0"
}
},
{ {
"package": "SwiftPM", "package": "SwiftPM",
"repositoryURL": "https://github.com/apple/swift-package-manager", "repositoryURL": "https://github.com/apple/swift-package-manager",

View File

@ -4,7 +4,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "tuist", name: "tuist",
platforms: [.macOS(.v10_11)], platforms: [.macOS(.v10_12)],
products: [ products: [
.executable(name: "tuist", targets: ["tuist"]), .executable(name: "tuist", targets: ["tuist"]),
.executable(name: "tuistenv", targets: ["tuistenv"]), .executable(name: "tuistenv", targets: ["tuistenv"]),
@ -32,6 +32,7 @@ let package = Package(
.package(url: "https://github.com/IBM-Swift/BlueSignals", .upToNextMajor(from: "1.0.21")), .package(url: "https://github.com/IBM-Swift/BlueSignals", .upToNextMajor(from: "1.0.21")),
.package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "5.0.1")), .package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "5.0.1")),
.package(url: "https://github.com/rnine/Checksum.git", .upToNextMajor(from: "1.0.2")), .package(url: "https://github.com/rnine/Checksum.git", .upToNextMajor(from: "1.0.2")),
.package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.2.0")),
.package(url: "https://github.com/thii/xcbeautify.git", .upToNextMajor(from: "0.7.3")), .package(url: "https://github.com/thii/xcbeautify.git", .upToNextMajor(from: "0.7.3")),
.package(url: "https://github.com/krzyzanowskim/CryptoSwift", .upToNextMajor(from: "1.3.0")), .package(url: "https://github.com/krzyzanowskim/CryptoSwift", .upToNextMajor(from: "1.3.0")),
], ],
@ -54,7 +55,7 @@ let package = Package(
), ),
.target( .target(
name: "TuistKit", name: "TuistKit",
dependencies: ["XcodeProj", "SPMUtility", "TuistSupport", "TuistGenerator", "TuistCache", "TuistAutomation", "ProjectDescription", "Signals", "RxSwift", "RxBlocking", "Checksum", "TuistLoader", "TuistSigning"] dependencies: ["XcodeProj", "SPMUtility", "TuistSupport", "TuistGenerator", "TuistCache", "TuistAutomation", "ProjectDescription", "Signals", "RxSwift", "RxBlocking", "Checksum", "TuistLoader", "TuistInsights", "TuistSigning"]
), ),
.testTarget( .testTarget(
name: "TuistKitTests", name: "TuistKitTests",
@ -90,7 +91,7 @@ let package = Package(
), ),
.target( .target(
name: "TuistSupport", name: "TuistSupport",
dependencies: ["SPMUtility", "RxSwift", "RxRelay"] dependencies: ["SPMUtility", "RxSwift", "RxRelay", "Logging"]
), ),
.target( .target(
name: "TuistSupportTesting", name: "TuistSupportTesting",
@ -118,7 +119,7 @@ let package = Package(
), ),
.testTarget( .testTarget(
name: "TuistGeneratorIntegrationTests", name: "TuistGeneratorIntegrationTests",
dependencies: ["TuistGenerator", "TuistSupportTesting", "TuistCoreTesting"] dependencies: ["TuistGenerator", "TuistSupportTesting", "TuistCoreTesting", "TuistGeneratorTesting"]
), ),
.target( .target(
name: "TuistCache", name: "TuistCache",
@ -148,6 +149,18 @@ let package = Package(
name: "TuistAutomationIntegrationTests", name: "TuistAutomationIntegrationTests",
dependencies: ["TuistAutomation", "TuistSupportTesting"] dependencies: ["TuistAutomation", "TuistSupportTesting"]
), ),
.target(
name: "TuistInsights",
dependencies: ["XcodeProj", "SPMUtility", "TuistCore", "TuistSupport", "XcbeautifyLib"]
),
.testTarget(
name: "TuistInsightsTests",
dependencies: ["TuistInsights", "TuistSupportTesting"]
),
.testTarget(
name: "TuistInsightsIntegrationTests",
dependencies: ["TuistInsights", "TuistSupportTesting"]
),
.target( .target(
name: "TuistSigning", name: "TuistSigning",
dependencies: ["TuistCore", "TuistSupport", "CryptoSwift"] dependencies: ["TuistCore", "TuistSupport", "CryptoSwift"]

View File

@ -26,6 +26,7 @@ The example below shows how projects are defined with Tuist:
import ProjectDescription import ProjectDescription
let project = Project(name: "App", let project = Project(name: "App",
organizationName: "tuist",
targets: [ targets: [
Target(name: "App", Target(name: "App",
platform: .iOS, platform: .iOS,

View File

@ -0,0 +1,13 @@
import Foundation
public struct AnalyzeAction: Equatable, Codable {
public let configurationName: String
public init(configurationName: String) {
self.configurationName = configurationName
}
public init(config: PresetBuildConfiguration = .release) {
self.init(configurationName: config.name)
}
}

View File

@ -1,12 +1,16 @@
import Foundation import Foundation
public typealias TuistConfig = Config
/// This model allows to configure Tuist. /// This model allows to configure Tuist.
public struct TuistConfig: Codable, Equatable { public struct Config: Codable, Equatable {
/// Contains options related to the project generation. /// Contains options related to the project generation.
/// ///
/// - xcodeProjectName(TemplateString): When passed, Tuist generates the project with the specific name on disk instead of using the project name. /// - xcodeProjectName(TemplateString): When passed, Tuist generates the project with the specific name on disk instead of using the project name.
/// - organizationName(Strig): When passed, Tuist generates the project with the specific organization name.
public enum GenerationOptions: Encodable, Decodable, Equatable { public enum GenerationOptions: Encodable, Decodable, Equatable {
case xcodeProjectName(TemplateString) case xcodeProjectName(TemplateString)
case organizationName(String)
} }
/// Generation options. /// Generation options.
@ -15,22 +19,28 @@ public struct TuistConfig: Codable, Equatable {
/// List of Xcode versions that the project supports. /// List of Xcode versions that the project supports.
public let compatibleXcodeVersions: CompatibleXcodeVersions public let compatibleXcodeVersions: CompatibleXcodeVersions
/// URL to the server that caching and insights will interact with.
public let cloudURL: String?
/// Initializes the tuist cofiguration. /// Initializes the tuist cofiguration.
/// ///
/// - Parameters: /// - Parameters:
/// - compatibleXcodeVersions: . /// - compatibleXcodeVersions: List of Xcode versions the project is compatible with.
/// - generationOptions: List of Xcode versions that the project supports. An empty list means that /// - cloudURL: URL to the server that caching and insights will interact with.
/// - generationOptions: List of options to use when generating the project.
public init(compatibleXcodeVersions: CompatibleXcodeVersions = .all, public init(compatibleXcodeVersions: CompatibleXcodeVersions = .all,
cloudURL: String? = nil,
generationOptions: [GenerationOptions]) { generationOptions: [GenerationOptions]) {
self.generationOptions = generationOptions
self.compatibleXcodeVersions = compatibleXcodeVersions self.compatibleXcodeVersions = compatibleXcodeVersions
self.generationOptions = generationOptions
self.cloudURL = cloudURL
dumpIfNeeded(self) dumpIfNeeded(self)
} }
} }
extension TuistConfig.GenerationOptions { extension Config.GenerationOptions {
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case xcodeProjectName case xcodeProjectName, organizationName
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -42,6 +52,12 @@ extension TuistConfig.GenerationOptions {
self = .xcodeProjectName(templateProjectName) self = .xcodeProjectName(templateProjectName)
return return
} }
if container.allKeys.contains(.organizationName), try container.decodeNil(forKey: .organizationName) == false {
var associatedValues = try container.nestedUnkeyedContainer(forKey: .organizationName)
let organizationName = try associatedValues.decode(String.self)
self = .organizationName(organizationName)
return
}
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case")) throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case"))
} }
@ -52,6 +68,9 @@ extension TuistConfig.GenerationOptions {
case let .xcodeProjectName(templateProjectName): case let .xcodeProjectName(templateProjectName):
var associatedValues = container.nestedUnkeyedContainer(forKey: .xcodeProjectName) var associatedValues = container.nestedUnkeyedContainer(forKey: .xcodeProjectName)
try associatedValues.encode(templateProjectName) try associatedValues.encode(templateProjectName)
case let .organizationName(name):
var associatedValues = container.nestedUnkeyedContainer(forKey: .organizationName)
try associatedValues.encode(name)
} }
} }
} }
@ -65,5 +84,9 @@ public func == (lhs: TuistConfig.GenerationOptions, rhs: TuistConfig.GenerationO
switch (lhs, rhs) { switch (lhs, rhs) {
case let (.xcodeProjectName(lhs), .xcodeProjectName(rhs)): case let (.xcodeProjectName(lhs), .xcodeProjectName(rhs)):
return lhs.rawString == rhs.rawString return lhs.rawString == rhs.rawString
case let (.organizationName(lhs), .organizationName(rhs)):
return lhs == rhs
default:
return false
} }
} }

View File

@ -66,6 +66,8 @@ extension FileElement: ExpressibleByStringLiteral {
} }
} }
extension FileElement: ExpressibleByStringInterpolation {}
extension Array: ExpressibleByUnicodeScalarLiteral where Element == FileElement { extension Array: ExpressibleByUnicodeScalarLiteral where Element == FileElement {
public typealias UnicodeScalarLiteralType = String public typealias UnicodeScalarLiteralType = String
} }

View File

@ -1,6 +1,6 @@
import Foundation import Foundation
public struct Path: Codable, ExpressibleByStringLiteral, Equatable { public struct Path: Codable, ExpressibleByStringLiteral, ExpressibleByStringInterpolation, Equatable {
public enum PathType: String, Codable { public enum PathType: String, Codable {
case relativeToCurrentFile case relativeToCurrentFile
case relativeToManifest case relativeToManifest

View File

@ -0,0 +1,23 @@
import Foundation
public struct ProfileAction: Equatable, Codable {
public let configurationName: String
public let executable: TargetReference?
public let arguments: Arguments?
public init(configurationName: String,
executable: TargetReference? = nil,
arguments: Arguments? = nil) {
self.configurationName = configurationName
self.executable = executable
self.arguments = arguments
}
public init(config: PresetBuildConfiguration = .release,
executable: TargetReference? = nil,
arguments: Arguments? = nil) {
self.init(configurationName: config.name,
executable: executable,
arguments: arguments)
}
}

View File

@ -4,6 +4,7 @@ import Foundation
public struct Project: Codable, Equatable { public struct Project: Codable, Equatable {
public let name: String public let name: String
public let organizationName: String?
public let packages: [Package] public let packages: [Package]
public let targets: [Target] public let targets: [Target]
public let schemes: [Scheme] public let schemes: [Scheme]
@ -11,12 +12,14 @@ public struct Project: Codable, Equatable {
public let additionalFiles: [FileElement] public let additionalFiles: [FileElement]
public init(name: String, public init(name: String,
organizationName: String? = nil,
packages: [Package] = [], packages: [Package] = [],
settings: Settings? = nil, settings: Settings? = nil,
targets: [Target] = [], targets: [Target] = [],
schemes: [Scheme] = [], schemes: [Scheme] = [],
additionalFiles: [FileElement] = []) { additionalFiles: [FileElement] = []) {
self.name = name self.name = name
self.organizationName = organizationName
self.packages = packages self.packages = packages
self.targets = targets self.targets = targets
self.schemes = schemes self.schemes = schemes

View File

@ -9,18 +9,24 @@ public struct Scheme: Equatable, Codable {
public let testAction: TestAction? public let testAction: TestAction?
public let runAction: RunAction? public let runAction: RunAction?
public let archiveAction: ArchiveAction? public let archiveAction: ArchiveAction?
public let profileAction: ProfileAction?
public let analyzeAction: AnalyzeAction?
public init(name: String, public init(name: String,
shared: Bool = true, shared: Bool = true,
buildAction: BuildAction? = nil, buildAction: BuildAction? = nil,
testAction: TestAction? = nil, testAction: TestAction? = nil,
runAction: RunAction? = nil, runAction: RunAction? = nil,
archiveAction: ArchiveAction? = nil) { archiveAction: ArchiveAction? = nil,
profileAction: ProfileAction? = nil,
analyzeAction: AnalyzeAction? = nil) {
self.name = name self.name = name
self.shared = shared self.shared = shared
self.buildAction = buildAction self.buildAction = buildAction
self.testAction = testAction self.testAction = testAction
self.runAction = runAction self.runAction = runAction
self.archiveAction = archiveAction self.archiveAction = archiveAction
self.profileAction = profileAction
self.analyzeAction = analyzeAction
} }
} }

View File

@ -1,7 +1,7 @@
// MARK: - FileList // MARK: - FileList
/// A model to refer to source files that supports passing compiler flags. /// A model to refer to source files that supports passing compiler flags.
public struct SourceFileGlob: ExpressibleByStringLiteral, Codable, Equatable { public struct SourceFileGlob: ExpressibleByStringLiteral, ExpressibleByStringInterpolation, Codable, Equatable {
/// Relative glob pattern. /// Relative glob pattern.
public let glob: Path public let glob: Path

View File

@ -0,0 +1,146 @@
import Foundation
/// Template manifest - used with `tuist scaffold`
public struct Template: Codable, Equatable {
/// Description of template
public let description: String
/// Attributes to be passed to template
public let attributes: [Attribute]
/// Files to generate
public let files: [File]
/// Directories to generate
public let directories: [String]
public init(description: String,
attributes: [Attribute] = [],
files: [File] = [],
directories: [String] = [],
script _: String? = nil) {
self.description = description
self.attributes = attributes
self.files = files
self.directories = directories
dumpIfNeeded(self)
}
/// Enum containing information about how to generate file
public enum Contents: Codable, Equatable {
/// String Contents is defined in `Template.swift` and contains a simple `String`
/// Can not contain any additional logic apart from plain `String` from `arguments`
case string(String)
/// File content is defined in a different file from `Template.swift`
/// Can contain additional logic and anything that is defined in `ProjectDescriptionHelpers`
case file(String)
private enum CodingKeys: String, CodingKey {
case type
case value
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let value = try container.decode(String.self, forKey: .value)
let type = try container.decode(String.self, forKey: .type)
if type == "string" {
self = .string(value)
} else if type == "file" {
self = .file(value)
} else {
fatalError("Argument '\(type)' not supported")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .string(contents):
try container.encode("string", forKey: .type)
try container.encode(contents, forKey: .value)
case let .file(path):
try container.encode("file", forKey: .type)
try container.encode(path, forKey: .value)
}
}
}
/// File description for generating
public struct File: Codable, Equatable {
public let path: String
public let contents: Contents
public init(path: String, contents: Contents) {
self.path = path
self.contents = contents
}
}
/// Attribute to be passed to `tuist scaffold` for generating with `Template`
public enum Attribute: Codable, Equatable {
/// Required attribute with a given name
case required(String)
/// Optional attribute with a given name and a default value used when attribute not provided by user
case optional(String, default: String)
enum CodingKeys: String, CodingKey {
case type
case name
case `default`
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let name = try container.decode(String.self, forKey: .name)
let type = try container.decode(String.self, forKey: .type)
if type == "required" {
self = .required(name)
} else if type == "optional" {
let defaultValue = try container.decode(String.self, forKey: .default)
self = .optional(name, default: defaultValue)
} else {
fatalError("Argument '\(type)' not supported")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .required(name):
try container.encode("required", forKey: .type)
try container.encode(name, forKey: .name)
case let .optional(name, default: defaultValue):
try container.encode("optional", forKey: .type)
try container.encode(name, forKey: .name)
try container.encode(defaultValue, forKey: .default)
}
}
}
}
public extension Template.File {
/// - Parameters:
/// - path: Path where to generate file
/// - contents: String Contents
/// - Returns: `Template.File` that is `.string`
static func string(path: String, contents: String) -> Template.File {
Template.File(path: path, contents: .string(contents))
}
/// - Parameters:
/// - path: Path where to generate file
/// - templatePath: Path of file where the template is defined
/// - Returns: `Template.File` that is `.file`
static func file(path: String, templatePath: String) -> Template.File {
Template.File(path: path, contents: .file(templatePath))
}
}
public extension String.StringInterpolation {
mutating func appendInterpolation(_ value: Template.Attribute) {
switch value {
case let .required(name), let .optional(name, default: _):
appendInterpolation("{{ \(name) }}")
}
}
}

View File

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

View File

@ -80,7 +80,7 @@ public final class XCFrameworkBuilder: XCFrameworkBuilding {
let outputDirectory = try TemporaryDirectory(removeTreeOnDeinit: false) let outputDirectory = try TemporaryDirectory(removeTreeOnDeinit: false)
let temporaryPath = try TemporaryDirectory(removeTreeOnDeinit: false) let temporaryPath = try TemporaryDirectory(removeTreeOnDeinit: false)
Printer.shared.print(section: "Building .xcframework for \(target.name)") logger.notice("Building .xcframework for \(target.name)", metadata: .section)
// Build for the device // Build for the device
// Without the BUILD_LIBRARY_FOR_DISTRIBUTION argument xcodebuild doesn't generate the .swiftinterface file // Without the BUILD_LIBRARY_FOR_DISTRIBUTION argument xcodebuild doesn't generate the .swiftinterface file
@ -95,7 +95,7 @@ public final class XCFrameworkBuilder: XCFrameworkBuilding {
.buildSetting("SKIP_INSTALL", "NO"), .buildSetting("SKIP_INSTALL", "NO"),
.buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES")) .buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES"))
.do(onSubscribed: { .do(onSubscribed: {
Printer.shared.print(subsection: "Building \(target.name) for device") logger.notice("Building \(target.name) for device", metadata: .subsection)
}) })
// Build for the simulator // Build for the simulator
@ -113,7 +113,7 @@ public final class XCFrameworkBuilder: XCFrameworkBuilding {
.buildSetting("SKIP_INSTALL", "NO"), .buildSetting("SKIP_INSTALL", "NO"),
.buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES")) .buildSetting("BUILD_LIBRARY_FOR_DISTRIBUTION", "YES"))
.do(onSubscribed: { .do(onSubscribed: {
Printer.shared.print(subsection: "Building \(target.name) for simulator") logger.notice("Building \(target.name) for simulator", metadata: .subsection)
}) })
} }
@ -125,7 +125,7 @@ public final class XCFrameworkBuilder: XCFrameworkBuilding {
let xcframeworkPath = outputDirectory.path.appending(component: "\(target.productName).xcframework") let xcframeworkPath = outputDirectory.path.appending(component: "\(target.productName).xcframework")
let xcframeworkObservable = xcodeBuildController.createXCFramework(frameworks: frameworkpaths, output: xcframeworkPath) let xcframeworkObservable = xcodeBuildController.createXCFramework(frameworks: frameworkpaths, output: xcframeworkPath)
.do(onSubscribed: { .do(onSubscribed: {
Printer.shared.print(subsection: "Exporting xcframework for \(target.platform.caseValue)") logger.notice("Exporting xcframework for \(target.platform.caseValue)", metadata: .subsection)
}) })
return deviceArchiveObservable return deviceArchiveObservable

View File

@ -4,7 +4,7 @@ import TuistSupport
public extension Observable where Element == SystemEvent<XcodeBuildOutput> { public extension Observable where Element == SystemEvent<XcodeBuildOutput> {
func printFormattedOutput() -> Observable<SystemEvent<XcodeBuildOutput>> { func printFormattedOutput() -> Observable<SystemEvent<XcodeBuildOutput>> {
self.do(onNext: { event in `do`(onNext: { event in
switch event { switch event {
case let .standardError(error): case let .standardError(error):
let string = error.formatted ?? error.raw let string = error.formatted ?? error.raw

View File

@ -11,12 +11,12 @@ public protocol GraphLoading: AnyObject {
/// - Parameter path: Path to the directory that contains the workspace. /// - Parameter path: Path to the directory that contains the workspace.
func loadWorkspace(path: AbsolutePath) throws -> (Graph, Workspace) func loadWorkspace(path: AbsolutePath) throws -> (Graph, Workspace)
/// Loads the TuistConfig. /// Loads the configuration.
/// ///
/// - Parameter path: Directory from which look up and load the TuistConfig. /// - Parameter path: Directory from which look up and load the Config.
/// - Returns: Loaded TuistConfig object. /// - Returns: Loaded Config object.
/// - Throws: An error if the TuistConfig.swift can't be parsed. /// - Throws: An error if the Config.swift can't be parsed.
func loadTuistConfig(path: AbsolutePath) throws -> TuistConfig func loadConfig(path: AbsolutePath) throws -> Config
} }
public class GraphLoader: GraphLoading { public class GraphLoader: GraphLoading {
@ -76,15 +76,15 @@ public class GraphLoader: GraphLoading {
return (graph, workspace) return (graph, workspace)
} }
public func loadTuistConfig(path: AbsolutePath) throws -> TuistConfig { public func loadConfig(path: AbsolutePath) throws -> Config {
let cache = GraphLoaderCache() let cache = GraphLoaderCache()
if let tuistConfig = cache.tuistConfig(path) { if let config = cache.config(path) {
return tuistConfig return config
} else { } else {
let tuistConfig = try modelLoader.loadTuistConfig(at: path) let config = try modelLoader.loadConfig(at: path)
cache.add(tuistConfig: tuistConfig, path: path) cache.add(config: config, path: path)
return tuistConfig return config
} }
} }

View File

@ -8,7 +8,7 @@ public class GraphLoaderCache: GraphLoaderCaching {
// MARK: - GraphLoaderCaching // MARK: - GraphLoaderCaching
var tuistConfigs: [AbsolutePath: TuistConfig] = [:] var configs: [AbsolutePath: Config] = [:]
public var projects: [AbsolutePath: Project] = [:] public var projects: [AbsolutePath: Project] = [:]
public var packages: [AbsolutePath: [PackageNode]] = [:] public var packages: [AbsolutePath: [PackageNode]] = [:]
public var precompiledNodes: [AbsolutePath: PrecompiledNode] = [:] public var precompiledNodes: [AbsolutePath: PrecompiledNode] = [:]
@ -50,12 +50,12 @@ public class GraphLoaderCache: GraphLoaderCaching {
packageNodes[package.path] = package packageNodes[package.path] = package
} }
public func tuistConfig(_ path: AbsolutePath) -> TuistConfig? { public func config(_ path: AbsolutePath) -> Config? {
tuistConfigs[path] configs[path]
} }
public func add(tuistConfig: TuistConfig, path: AbsolutePath) { public func add(config: Config, path: AbsolutePath) {
tuistConfigs[path] = tuistConfig configs[path] = config
} }
public func project(_ path: AbsolutePath) -> Project? { public func project(_ path: AbsolutePath) -> Project? {

View File

@ -48,7 +48,7 @@ public class TargetNode: GraphNode {
return false return false
} }
return path == otherTagetNode.path return path == otherTagetNode.path
&& target == otherTagetNode.target && name == otherTagetNode.name
} }
// MARK: - Encodable // MARK: - Encodable

View File

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

View File

@ -0,0 +1,13 @@
import Foundation
public struct AnalyzeAction: Equatable {
// MARK: - Attributes
public let configurationName: String
// MARK: - Init
public init(configurationName: String) {
self.configurationName = configurationName
}
}

View File

@ -3,12 +3,13 @@ import Foundation
import TuistSupport import TuistSupport
/// This model allows to configure Tuist. /// This model allows to configure Tuist.
public struct TuistConfig: Equatable, Hashable { public struct Config: Equatable, Hashable {
/// Contains options related to the project generation. /// Contains options related to the project generation.
/// ///
/// - xcodeProjectName: Name used for the Xcode project /// - xcodeProjectName: Name used for the Xcode project
public enum GenerationOption: Hashable, Equatable { public enum GenerationOption: Hashable, Equatable {
case xcodeProjectName(String) case xcodeProjectName(String)
case organizationName(String)
} }
/// Generation options. /// Generation options.
@ -17,20 +18,25 @@ public struct TuistConfig: Equatable, Hashable {
/// List of Xcode versions the project or set of projects is compatible with. /// List of Xcode versions the project or set of projects is compatible with.
public let compatibleXcodeVersions: CompatibleXcodeVersions public let compatibleXcodeVersions: CompatibleXcodeVersions
/// URL to the server that caching and insights will interact with.
public let cloudURL: URL?
/// Returns the default Tuist configuration. /// Returns the default Tuist configuration.
public static var `default`: TuistConfig { public static var `default`: Config {
TuistConfig(compatibleXcodeVersions: .all, Config(compatibleXcodeVersions: .all, cloudURL: nil, generationOptions: [])
generationOptions: [])
} }
/// Initializes the tuist cofiguration. /// Initializes the tuist cofiguration.
/// ///
/// - Parameters: /// - Parameters:
/// - compatibleXcodeVersions: List of Xcode versions the project or set of projects is compatible with. /// - compatibleXcodeVersions: List of Xcode versions the project or set of projects is compatible with.
/// - cloudURL: URL to the server that caching and insights will interact with.
/// - generationOptions: Generation options. /// - generationOptions: Generation options.
public init(compatibleXcodeVersions: CompatibleXcodeVersions, public init(compatibleXcodeVersions: CompatibleXcodeVersions,
cloudURL: URL?,
generationOptions: [GenerationOption]) { generationOptions: [GenerationOption]) {
self.compatibleXcodeVersions = compatibleXcodeVersions self.compatibleXcodeVersions = compatibleXcodeVersions
self.cloudURL = cloudURL
self.generationOptions = generationOptions self.generationOptions = generationOptions
} }
@ -38,11 +44,7 @@ public struct TuistConfig: Equatable, Hashable {
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(generationOptions) hasher.combine(generationOptions)
} hasher.combine(cloudURL)
hasher.combine(compatibleXcodeVersions)
// MARK: - Equatable
public static func == (lhs: TuistConfig, rhs: TuistConfig) -> Bool {
lhs.generationOptions == rhs.generationOptions
} }
} }

View File

@ -42,13 +42,14 @@ public extension Array where Element == LintingIssue {
let errorIssues = filter { $0.severity == .error } let errorIssues = filter { $0.severity == .error }
let warningIssues = filter { $0.severity == .warning } let warningIssues = filter { $0.severity == .warning }
warningIssues.forEach { issue in for issue in warningIssues {
Printer.shared.print(warning: "\(issue.description)") logger.warning("\(issue.description)")
} }
errorIssues.forEach { issue in for issue in errorIssues {
Printer.shared.print(errorMessage: "\(issue.description)") logger.error("\(issue.description)")
} }
if !errorIssues.isEmpty { throw LintingError() } if !errorIssues.isEmpty { throw LintingError() }
} }
} }

View File

@ -0,0 +1,19 @@
import Foundation
public struct ProfileAction: Equatable {
// MARK: - Attributes
public let configurationName: String
public let executable: TargetReference?
public let arguments: Arguments?
// MARK: - Init
public init(configurationName: String,
executable: TargetReference? = nil,
arguments: Arguments? = nil) {
self.configurationName = configurationName
self.executable = executable
self.arguments = arguments
}
}

View File

@ -11,6 +11,9 @@ public struct Project: Equatable, CustomStringConvertible {
/// Project name. /// Project name.
public let name: String public let name: String
/// Organization name.
public let organizationName: String?
/// Project file name. /// Project file name.
public let fileName: String public let fileName: String
@ -39,6 +42,7 @@ public struct Project: Equatable, CustomStringConvertible {
/// - Parameters: /// - Parameters:
/// - path: Path to the folder that contains the project manifest. /// - path: Path to the folder that contains the project manifest.
/// - name: Project name. /// - name: Project name.
/// - organizationName: Organization name.
/// - settings: The settings to apply at the project level /// - settings: The settings to apply at the project level
/// - filesGroup: The root group to place project files within /// - filesGroup: The root group to place project files within
/// - targets: The project targets /// - targets: The project targets
@ -46,6 +50,7 @@ public struct Project: Equatable, CustomStringConvertible {
/// *(Those won't be included in any build phases)* /// *(Those won't be included in any build phases)*
public init(path: AbsolutePath, public init(path: AbsolutePath,
name: String, name: String,
organizationName: String? = nil,
fileName: String? = nil, fileName: String? = nil,
settings: Settings, settings: Settings,
filesGroup: ProjectGroup, filesGroup: ProjectGroup,
@ -55,6 +60,7 @@ public struct Project: Equatable, CustomStringConvertible {
additionalFiles: [FileElement] = []) { additionalFiles: [FileElement] = []) {
self.path = path self.path = path
self.name = name self.name = name
self.organizationName = organizationName
self.fileName = fileName ?? name self.fileName = fileName ?? name
self.targets = targets self.targets = targets
self.packages = packages self.packages = packages

View File

@ -10,6 +10,8 @@ public struct Scheme: Equatable {
public let testAction: TestAction? public let testAction: TestAction?
public let runAction: RunAction? public let runAction: RunAction?
public let archiveAction: ArchiveAction? public let archiveAction: ArchiveAction?
public let profileAction: ProfileAction?
public let analyzeAction: AnalyzeAction?
// MARK: - Init // MARK: - Init
@ -18,13 +20,17 @@ public struct Scheme: Equatable {
buildAction: BuildAction? = nil, buildAction: BuildAction? = nil,
testAction: TestAction? = nil, testAction: TestAction? = nil,
runAction: RunAction? = nil, runAction: RunAction? = nil,
archiveAction: ArchiveAction? = nil) { archiveAction: ArchiveAction? = nil,
profileAction: ProfileAction? = nil,
analyzeAction: AnalyzeAction? = nil) {
self.name = name self.name = name
self.shared = shared self.shared = shared
self.buildAction = buildAction self.buildAction = buildAction
self.testAction = testAction self.testAction = testAction
self.runAction = runAction self.runAction = runAction
self.archiveAction = archiveAction self.archiveAction = archiveAction
self.profileAction = profileAction
self.analyzeAction = analyzeAction
} }
public func targetDependencies() -> [TargetReference] { public func targetDependencies() -> [TargetReference] {
@ -39,6 +45,7 @@ public struct Scheme: Equatable {
runAction?.executable.map { [$0] }, runAction?.executable.map { [$0] },
archiveAction?.preActions.compactMap(\.target), archiveAction?.preActions.compactMap(\.target),
archiveAction?.postActions.compactMap(\.target), archiveAction?.postActions.compactMap(\.target),
profileAction?.executable.map { [$0] },
] ]
let targets = targetSources.compactMap { $0 }.flatMap { $0 }.uniqued() let targets = targetSources.compactMap { $0 }.flatMap { $0 }.uniqued()

View File

@ -25,10 +25,10 @@ public protocol GeneratorModelLoading {
/// - Throws: Error encountered during the loading process (e.g. Missing workspace) /// - Throws: Error encountered during the loading process (e.g. Missing workspace)
func loadWorkspace(at path: AbsolutePath) throws -> Workspace func loadWorkspace(at path: AbsolutePath) throws -> Workspace
/// Load a TusitConfig model at the specified path /// Load a Config model at the specified path
/// ///
/// - Parameter path: The absolute path for the tuistconfig model to load /// - Parameter path: The absolute path for the Config model to load
/// - Returns: The tuistconfig loaded from the specified path /// - Returns: The config loaded from the specified path
/// - Throws: Error encountered during the loading process (e.g. Missing tuistconfig) /// - Throws: Error encountered during the loading process (e.g. Missing Config file)
func loadTuistConfig(at path: AbsolutePath) throws -> TuistConfig func loadConfig(at path: AbsolutePath) throws -> Config
} }

View File

@ -47,17 +47,17 @@ public protocol GraphLoaderCaching: AnyObject {
/// - name: Name of the target. /// - name: Name of the target.
func targetNode(_ path: AbsolutePath, name: String) -> TargetNode? func targetNode(_ path: AbsolutePath, name: String) -> TargetNode?
// MARK: - TuistConfig // MARK: - Config
/// It returns a Tuist configuration if it exists at the given directory. /// It returns a Tuist configuration if it exists at the given directory.
/// - Parameter path: Path to the directory that contains the TuistConfig. /// - Parameter path: Path to the directory that contains the Config.
func tuistConfig(_ path: AbsolutePath) -> TuistConfig? func config(_ path: AbsolutePath) -> Config?
/// Caches a TuistConfig representation. /// Caches a Config representation.
/// - Parameters: /// - Parameters:
/// - tuistConfig: Tuist configuration. /// - config: Tuist configuration.
/// - path: Path to the directory that contains th /// - path: Path to the directory that contains th
func add(tuistConfig: TuistConfig, path: AbsolutePath) func add(config: Config, path: AbsolutePath)
// MARK: - CocoaPods // MARK: - CocoaPods

View File

@ -15,8 +15,8 @@ public final class MockGraphLoader: GraphLoading {
return try loadWorkspaceStub?(path) ?? (Graph.test(), Workspace.test()) return try loadWorkspaceStub?(path) ?? (Graph.test(), Workspace.test())
} }
public var loadTuistConfigStub: ((AbsolutePath) throws -> (TuistConfig))? public var loadConfigStub: ((AbsolutePath) throws -> (Config))?
public func loadTuistConfig(path: AbsolutePath) throws -> TuistConfig { public func loadConfig(path: AbsolutePath) throws -> Config {
try loadTuistConfigStub?(path) ?? TuistConfig.test() try loadConfigStub?(path) ?? Config.test()
} }
} }

View File

@ -16,8 +16,8 @@ public final class MockGraphLoaderCache: GraphLoaderCaching {
var precompiledNodeStub: ((AbsolutePath) -> PrecompiledNode?)? var precompiledNodeStub: ((AbsolutePath) -> PrecompiledNode?)?
var addTargetNodeArgs: [TargetNode] = [] var addTargetNodeArgs: [TargetNode] = []
var targetNodeStub: ((AbsolutePath, String) -> TargetNode?)? var targetNodeStub: ((AbsolutePath, String) -> TargetNode?)?
var tuistConfigStub: [AbsolutePath: TuistConfig] = [:] var configStub: [AbsolutePath: Config] = [:]
var addTuistConfigArgs: [(tuistConfig: TuistConfig, path: AbsolutePath)] = [] var addConfigArgs: [(config: Config, path: AbsolutePath)] = []
public var cocoapodsNodes: [AbsolutePath: CocoaPodsNode] = [:] public var cocoapodsNodes: [AbsolutePath: CocoaPodsNode] = [:]
var cocoapodsStub: [AbsolutePath: CocoaPodsNode] = [:] var cocoapodsStub: [AbsolutePath: CocoaPodsNode] = [:]
var addCococaPodsArgs: [CocoaPodsNode] = [] var addCococaPodsArgs: [CocoaPodsNode] = []
@ -42,12 +42,12 @@ public final class MockGraphLoaderCache: GraphLoaderCaching {
addCococaPodsArgs.append(cocoapods) addCococaPodsArgs.append(cocoapods)
} }
public func tuistConfig(_ path: AbsolutePath) -> TuistConfig? { public func config(_ path: AbsolutePath) -> Config? {
tuistConfigStub[path] configStub[path]
} }
public func add(tuistConfig: TuistConfig, path: AbsolutePath) { public func add(config: Config, path: AbsolutePath) {
addTuistConfigArgs.append((tuistConfig: tuistConfig, path: path)) addConfigArgs.append((config: config, path: path))
} }
public func project(_ path: AbsolutePath) -> Project? { public func project(_ path: AbsolutePath) -> Project? {

View File

@ -0,0 +1,9 @@
import Basic
import Foundation
@testable import TuistCore
public extension AnalyzeAction {
static func test(configurationName: String = "Beta Release") -> AnalyzeAction {
AnalyzeAction(configurationName: configurationName)
}
}

View File

@ -0,0 +1,13 @@
import Basic
import Foundation
@testable import TuistCore
public extension Config {
static func test(compatibleXcodeVersions: CompatibleXcodeVersions = .all,
cloudURL: URL? = nil,
generationOptions: [GenerationOption] = []) -> Config {
Config(compatibleXcodeVersions: compatibleXcodeVersions,
cloudURL: cloudURL,
generationOptions: generationOptions)
}
}

View File

@ -0,0 +1,13 @@
import Basic
import Foundation
@testable import TuistCore
public extension ProfileAction {
static func test(configurationName: String = "Beta Release",
executable: TargetReference? = TargetReference(projectPath: "/Project", name: "App"),
arguments: Arguments? = Arguments.test()) -> ProfileAction {
ProfileAction(configurationName: configurationName,
executable: executable,
arguments: arguments)
}
}

View File

@ -5,6 +5,7 @@ import Foundation
public extension Project { public extension Project {
static func test(path: AbsolutePath = AbsolutePath("/Project"), static func test(path: AbsolutePath = AbsolutePath("/Project"),
name: String = "Project", name: String = "Project",
organizationName: String? = nil,
fileName: String? = nil, fileName: String? = nil,
settings: Settings = Settings.test(), settings: Settings = Settings.test(),
filesGroup: ProjectGroup = .group(name: "Project"), filesGroup: ProjectGroup = .group(name: "Project"),
@ -14,6 +15,7 @@ public extension Project {
additionalFiles: [FileElement] = []) -> Project { additionalFiles: [FileElement] = []) -> Project {
Project(path: path, Project(path: path,
name: name, name: name,
organizationName: organizationName,
fileName: fileName, fileName: fileName,
settings: settings, settings: settings,
filesGroup: filesGroup, filesGroup: filesGroup,
@ -25,6 +27,7 @@ public extension Project {
static func empty(path: AbsolutePath = AbsolutePath("/test/"), static func empty(path: AbsolutePath = AbsolutePath("/test/"),
name: String = "Project", name: String = "Project",
organizationName: String? = nil,
settings: Settings = .default, settings: Settings = .default,
filesGroup: ProjectGroup = .group(name: "Project"), filesGroup: ProjectGroup = .group(name: "Project"),
targets: [Target] = [], targets: [Target] = [],
@ -33,6 +36,7 @@ public extension Project {
additionalFiles: [FileElement] = []) -> Project { additionalFiles: [FileElement] = []) -> Project {
Project(path: path, Project(path: path,
name: name, name: name,
organizationName: organizationName,
settings: settings, settings: settings,
filesGroup: filesGroup, filesGroup: filesGroup,
targets: targets, targets: targets,

View File

@ -8,12 +8,16 @@ public extension Scheme {
buildAction: BuildAction? = BuildAction.test(), buildAction: BuildAction? = BuildAction.test(),
testAction: TestAction? = TestAction.test(), testAction: TestAction? = TestAction.test(),
runAction: RunAction? = RunAction.test(), runAction: RunAction? = RunAction.test(),
archiveAction: ArchiveAction? = ArchiveAction.test()) -> Scheme { archiveAction: ArchiveAction? = ArchiveAction.test(),
profileAction: ProfileAction? = ProfileAction.test(),
analyzeAction: AnalyzeAction? = AnalyzeAction.test()) -> Scheme {
Scheme(name: name, Scheme(name: name,
shared: shared, shared: shared,
buildAction: buildAction, buildAction: buildAction,
testAction: testAction, testAction: testAction,
runAction: runAction, runAction: runAction,
archiveAction: archiveAction) archiveAction: archiveAction,
profileAction: profileAction,
analyzeAction: analyzeAction)
} }
} }

View File

@ -1,11 +0,0 @@
import Basic
import Foundation
@testable import TuistCore
public extension TuistConfig {
static func test(compatibleXcodeVersions: CompatibleXcodeVersions = .all,
generationOptions: [GenerationOption] = []) -> TuistConfig {
TuistConfig(compatibleXcodeVersions: compatibleXcodeVersions,
generationOptions: generationOptions)
}
}

View File

@ -50,7 +50,7 @@ final class BundleCommand: Command {
init(parser: ArgumentParser, init(parser: ArgumentParser,
versionsController: VersionsControlling, versionsController: VersionsControlling,
installer: Installing) { installer: Installing) {
_ = parser.add(subparser: BundleCommand.command, overview: BundleCommand.overview) let subParser = parser.add(subparser: BundleCommand.command, overview: BundleCommand.overview)
self.versionsController = versionsController self.versionsController = versionsController
self.installer = installer self.installer = installer
} }
@ -66,13 +66,13 @@ final class BundleCommand: Command {
} }
let version = try String(contentsOf: versionFilePath.url) let version = try String(contentsOf: versionFilePath.url)
Printer.shared.print(section: "Bundling the version \(version) in the directory \(binFolderPath.pathString)") logger.notice("Bundling the version \(version) in the directory \(binFolderPath.pathString)", metadata: .section)
let versionPath = versionsController.path(version: version) let versionPath = versionsController.path(version: version)
// Installing // Installing
if !FileHandler.shared.exists(versionPath) { if !FileHandler.shared.exists(versionPath) {
Printer.shared.print("Version \(version) not available locally. Installing...") logger.notice("Version \(version) not available locally. Installing...")
try installer.install(version: version, force: false) try installer.install(version: version, force: false)
} }
@ -82,6 +82,6 @@ final class BundleCommand: Command {
} }
try FileHandler.shared.copy(from: versionPath, to: binFolderPath) try FileHandler.shared.copy(from: versionPath, to: binFolderPath)
Printer.shared.print(success: "tuist bundled successfully at \(binFolderPath.pathString)") logger.notice("tuist bundled successfully at \(binFolderPath.pathString)", metadata: .success)
} }
} }

View File

@ -81,6 +81,6 @@ public final class CommandRegistry {
// MARK: - Static // MARK: - Static
static func processArguments() -> [String] { static func processArguments() -> [String] {
Array(ProcessInfo.processInfo.arguments) CommandRunner.arguments()
} }
} }

View File

@ -63,9 +63,9 @@ class CommandRunner: CommandRunning {
switch resolvedVersion { switch resolvedVersion {
case let .bin(path): case let .bin(path):
Printer.shared.print("Using bundled version at path \(path.pathString)") logger.notice("Using bundled version at path \(path.pathString)")
case let .versionFile(path, value): case let .versionFile(path, value):
Printer.shared.print("Using version \(value) defined at \(path.pathString)") logger.notice("Using version \(value) defined at \(path.pathString)")
default: default:
break break
} }
@ -100,7 +100,7 @@ class CommandRunner: CommandRunning {
func runVersion(_ version: String) throws { func runVersion(_ version: String) throws {
if !versionsController.versions().contains(where: { $0.description == version }) { if !versionsController.versions().contains(where: { $0.description == version }) {
Printer.shared.print("Version \(version) not found locally. Installing...") logger.notice("Version \(version) not found locally. Installing...")
try installer.install(version: version, force: false) try installer.install(version: version, force: false)
} }
@ -109,7 +109,13 @@ class CommandRunner: CommandRunning {
} }
func runAtPath(_ path: AbsolutePath) throws { func runAtPath(_ path: AbsolutePath) throws {
var args = [path.appending(component: Constants.binName).pathString] var args: [String] = []
if CommandLine.arguments.contains("--verbose") {
args.append("TUIST_VERBOSE=true")
}
args.append(path.appending(component: Constants.binName).pathString)
args.append(contentsOf: Array(arguments().dropFirst())) args.append(contentsOf: Array(arguments().dropFirst()))
var environment = ProcessInfo.processInfo.environment var environment = ProcessInfo.processInfo.environment
@ -125,6 +131,6 @@ class CommandRunner: CommandRunning {
// MARK: - Static // MARK: - Static
static func arguments() -> [String] { static func arguments() -> [String] {
Array(ProcessInfo.processInfo.arguments) Array(ProcessInfo.processInfo.arguments).filter { $0 != "--verbose" }
} }
} }

View File

@ -60,7 +60,7 @@ final class InstallCommand: Command {
let version = result.get(versionArgument)! let version = result.get(versionArgument)!
let versions = versionsController.versions().map { $0.description } let versions = versionsController.versions().map { $0.description }
if versions.contains(version) { if versions.contains(version) {
Printer.shared.print(warning: "Version \(version) already installed, skipping") logger.warning("Version \(version) already installed, skipping")
return return
} }
try installer.install(version: version, force: force) try installer.install(version: version, force: force)

View File

@ -46,19 +46,19 @@ class LocalCommand: Command {
// MARK: - Fileprivate // MARK: - Fileprivate
private func printLocalVersions() throws { private func printLocalVersions() throws {
Printer.shared.print(section: "The following versions are available in the local environment:") logger.notice("The following versions are available in the local environment:", metadata: .section)
let versions = versionController.semverVersions() let versions = versionController.semverVersions()
let output = versions.sorted().reversed().map { "- \($0)" }.joined(separator: "\n") let output = versions.sorted().reversed().map { "- \($0)" }.joined(separator: "\n")
Printer.shared.print("\(output)") logger.notice("\(output)")
} }
private func createVersionFile(version: String) throws { private func createVersionFile(version: String) throws {
let currentPath = FileHandler.shared.currentPath let currentPath = FileHandler.shared.currentPath
Printer.shared.print(section: "Generating \(Constants.versionFileName) file with version \(version)") logger.notice("Generating \(Constants.versionFileName) file with version \(version)", metadata: .section)
let tuistVersionPath = currentPath.appending(component: Constants.versionFileName) let tuistVersionPath = currentPath.appending(component: Constants.versionFileName)
try "\(version)".write(to: URL(fileURLWithPath: tuistVersionPath.pathString), try "\(version)".write(to: URL(fileURLWithPath: tuistVersionPath.pathString),
atomically: true, atomically: true,
encoding: .utf8) encoding: .utf8)
Printer.shared.print(success: "File generated at path \(tuistVersionPath.pathString)") logger.notice("File generated at path \(tuistVersionPath.pathString)", metadata: .success)
} }
} }

View File

@ -40,9 +40,9 @@ final class UninstallCommand: Command {
let versions = versionsController.versions().map { $0.description } let versions = versionsController.versions().map { $0.description }
if versions.contains(version) { if versions.contains(version) {
try versionsController.uninstall(version: version) try versionsController.uninstall(version: version)
Printer.shared.print(success: "Version \(version) uninstalled") logger.notice("Version \(version) uninstalled", metadata: .success)
} else { } else {
Printer.shared.print(warning: "Version \(version) cannot be uninstalled because it's not installed") logger.warning("Version \(version) cannot be uninstalled because it's not installed")
} }
} }
} }

View File

@ -51,7 +51,7 @@ final class UpdateCommand: Command {
/// - Throws: An error if the update process fails. /// - Throws: An error if the update process fails.
func run(with result: ArgumentParser.Result) throws { func run(with result: ArgumentParser.Result) throws {
let force = result.get(forceArgument) ?? false let force = result.get(forceArgument) ?? false
Printer.shared.print(section: "Checking for updates...") logger.notice("Checking for updates...", metadata: .section)
try updater.update(force: force) try updater.update(force: force)
} }
} }

View File

@ -18,6 +18,6 @@ class VersionCommand: NSObject, Command {
// MARK: - Command // MARK: - Command
func run(with _: ArgumentParser.Result) { func run(with _: ArgumentParser.Result) {
Printer.shared.print("\(Constants.version)") logger.notice("\(Constants.version)")
} }
} }

View File

@ -78,7 +78,7 @@ final class Installer: Installing {
func install(version: String, temporaryDirectory: TemporaryDirectory, force: Bool = false) throws { func install(version: String, temporaryDirectory: TemporaryDirectory, force: Bool = false) throws {
// We ignore the Swift version and install from the soruce code // We ignore the Swift version and install from the soruce code
if force { if force {
Printer.shared.print("Forcing the installation of \(version) from the source code") logger.notice("Forcing the installation of \(version) from the source code")
try installFromSource(version: version, try installFromSource(version: version,
temporaryDirectory: temporaryDirectory) temporaryDirectory: temporaryDirectory)
return return
@ -102,16 +102,17 @@ final class Installer: Installing {
try versionsController.install(version: version, installation: { installationDirectory in try versionsController.install(version: version, installation: { installationDirectory in
// Download bundle // Download bundle
Printer.shared.print("Downloading version \(version)") logger.notice("Downloading version \(version)")
let downloadPath = temporaryDirectory.path.appending(component: Constants.bundleName) let downloadPath = temporaryDirectory.path.appending(component: Constants.bundleName)
try System.shared.run("/usr/bin/curl", "-LSs", "--output", downloadPath.pathString, bundleURL.absoluteString) try System.shared.run("/usr/bin/curl", "-LSs", "--output", downloadPath.pathString, bundleURL.absoluteString)
// Unzip // Unzip
Printer.shared.print("Installing...") logger.notice("Installing...")
try System.shared.run("/usr/bin/unzip", "-q", downloadPath.pathString, "-d", installationDirectory.pathString) try System.shared.run("/usr/bin/unzip", "-q", downloadPath.pathString, "-d", installationDirectory.pathString)
try createTuistVersionFile(version: version, path: installationDirectory) try createTuistVersionFile(version: version, path: installationDirectory)
Printer.shared.print("Version \(version) installed") logger.notice("Version \(version) installed")
}) })
} }
@ -123,7 +124,7 @@ final class Installer: Installing {
let buildDirectory = temporaryDirectory.path.appending(RelativePath(".build/release/")) let buildDirectory = temporaryDirectory.path.appending(RelativePath(".build/release/"))
// Cloning and building // Cloning and building
Printer.shared.print("Pulling source code") logger.notice("Pulling source code")
_ = try System.shared.observable(["/usr/bin/env", "git", "clone", Constants.gitRepositoryURL, temporaryDirectory.path.pathString]) _ = try System.shared.observable(["/usr/bin/env", "git", "clone", Constants.gitRepositoryURL, temporaryDirectory.path.pathString])
.mapToString() .mapToString()
.printStandardError() .printStandardError()
@ -143,7 +144,7 @@ final class Installer: Installing {
} }
} }
Printer.shared.print("Building using Swift (it might take a while)") logger.notice("Building using Swift (it might take a while)")
let swiftPath = try System.shared let swiftPath = try System.shared
.observable(["/usr/bin/xcrun", "-f", "swift"]) .observable(["/usr/bin/xcrun", "-f", "swift"])
.mapToString() .mapToString()
@ -184,7 +185,7 @@ final class Installer: Installing {
to: installationDirectory) to: installationDirectory)
try createTuistVersionFile(version: version, path: installationDirectory) try createTuistVersionFile(version: version, path: installationDirectory)
Printer.shared.print("Version \(version) installed") logger.notice("Version \(version) installed")
} }
} }

View File

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

View File

@ -34,22 +34,22 @@ final class Updater: Updating {
} }
guard let highestRemoteVersion = try googleCloudStorageClient.latestVersion().toBlocking().first() else { guard let highestRemoteVersion = try googleCloudStorageClient.latestVersion().toBlocking().first() else {
Printer.shared.print("No remote versions found") logger.warning("No remote versions found")
return return
} }
if force { if force {
Printer.shared.print("Forcing the update of version \(highestRemoteVersion)") logger.notice("Forcing the update of version \(highestRemoteVersion)")
try installer.install(version: highestRemoteVersion.description, force: true) try installer.install(version: highestRemoteVersion.description, force: true)
} else if let highestLocalVersion = versionsController.semverVersions().sorted().last { } else if let highestLocalVersion = versionsController.semverVersions().sorted().last {
if highestRemoteVersion <= highestLocalVersion { if highestRemoteVersion <= highestLocalVersion {
Printer.shared.print("There are no updates available") logger.notice("There are no updates available")
} else { } else {
Printer.shared.print("Installing new version available \(highestRemoteVersion)") logger.notice("Installing new version available \(highestRemoteVersion)")
try installer.install(version: highestRemoteVersion.description, force: false) try installer.install(version: highestRemoteVersion.description, force: false)
} }
} else { } else {
Printer.shared.print("No local versions available. Installing the latest version \(highestRemoteVersion)") logger.notice("No local versions available. Installing the latest version \(highestRemoteVersion)")
try installer.install(version: highestRemoteVersion.description, force: false) try installer.install(version: highestRemoteVersion.description, force: false)
} }
} }

View File

@ -0,0 +1,17 @@
import Foundation
/// Command Descriptor
///
/// Describes a command that needs to be executed as part of
/// generating a project or workspace.
///
/// - seealso: `SideEffectsDescriptor`
public struct CommandDescriptor {
public var command: [String]
/// Creates a command descriptor
/// - Parameter command: The command and its arguments to perform
public init(command: [String]) {
self.command = command
}
}

View File

@ -0,0 +1,37 @@
import Basic
import Foundation
/// File Descriptor
///
/// Describes a file operation that needs to take place as
/// part of generating a project or workspace.
///
/// - seealso: `SideEffectsDescriptor`
public struct FileDescriptor {
public enum State {
case present
case absent
}
/// Path to the file
public var path: AbsolutePath
/// The contents of the file
public var contents: Data?
/// The desired state of the file (`.present` creates a fiile, `.absent` deletes a file)
public var state: State
/// Creates a File Descriptor
/// - Parameters:
/// - path: Path to the file
/// - contents: The contents of the file (Optional)
/// - state: The desired state of the file (`.present` creates a fiile, `.absent` deletes a file)
public init(path: AbsolutePath,
contents: Data? = nil,
state: FileDescriptor.State = .present) {
self.path = path
self.contents = contents
self.state = state
}
}

View File

@ -0,0 +1,40 @@
import Basic
import Foundation
import XcodeProj
/// Project Descriptor
///
/// Contains the information needed to generate a project.
///
/// Can be used in conjunction with `XcodeProjWriter` to
/// generate an `.xcodeproj` file.
///
/// - seealso: `XcodeProjWriter`
public struct ProjectDescriptor {
/// Path to the project
public var path: AbsolutePath
/// Path to the xcodeproj file
public var xcodeprojPath: AbsolutePath
/// The XcodeProj representation of this project
public var xcodeProj: XcodeProj
/// The scheme descriptors of all the schemes within this project
public var schemeDescriptors: [SchemeDescriptor]
/// The side effects required for generating this project
public var sideEffectDescriptors: [SideEffectDescriptor]
public init(path: AbsolutePath,
xcodeprojPath: AbsolutePath,
xcodeProj: XcodeProj,
schemeDescriptors: [SchemeDescriptor],
sideEffectDescriptors: [SideEffectDescriptor]) {
self.path = path
self.xcodeprojPath = xcodeprojPath
self.xcodeProj = xcodeProj
self.schemeDescriptors = schemeDescriptors
self.sideEffectDescriptors = sideEffectDescriptors
}
}

View File

@ -0,0 +1,26 @@
import Foundation
import XcodeProj
/// Scheme Descriptor
///
/// Contains the information needed to generate a scheme.
///
/// When part of a `ProjectDescriptor` or `WorkspaceDescriptor`, it
/// can be used in conjunction with `XcodeProjWriter` to generate
/// an `.xcscheme` file.
///
/// - seealso: `ProjectDescriptor`
/// - seealso: `WorkspaceDescriptor`
/// - seealso: `XcodeProjWriter`
public struct SchemeDescriptor {
/// The XCScheme scheme representation
public var xcScheme: XCScheme
/// The Scheme type shared vs user scheme
public var shared: Bool
public init(xcScheme: XCScheme, shared: Bool) {
self.xcScheme = xcScheme
self.shared = shared
}
}

View File

@ -0,0 +1,22 @@
import Basic
import Foundation
/// Side Effect Descriptor
///
/// Describes a side effect that needs to take place without performing it
/// immediately within a component. This allows components to be side effect free,
/// determenistic and much easier to test.
///
/// When part of a `ProjectDescriptor` or `WorkspaceDescriptor`, it
/// can be used in conjunction with `XcodeProjWriter` to perform side effects.
///
/// - seealso: `ProjectDescriptor`
/// - seealso: `WorkspaceDescriptor`
/// - seealso: `XcodeProjWriter`
public enum SideEffectDescriptor {
/// Create / Remove a file
case file(FileDescriptor)
/// Perform a command
case command(CommandDescriptor)
}

View File

@ -0,0 +1,47 @@
import Basic
import Foundation
import XcodeProj
/// Workspace Descriptor
///
/// Contains the information needed to generate a workspace
/// and all its projects.
///
/// Can be used in conjunction with `XcodeProjWriter` to
/// generate an `.xcworkspace` file along with all its
/// `.xcodeproj` files.
///
/// - seealso: `XcodeProjWriter`
public struct WorkspaceDescriptor {
/// Path to the workspace
public var path: AbsolutePath
/// Path to the xcworkspace file
public var xcworkspacePath: AbsolutePath
/// The XCWorkspace representation of the workspace
public var xcworkspace: XCWorkspace
/// The project descriptors of all the projects within this workspace
public var projectDescriptors: [ProjectDescriptor]
/// The scheme descriptors of all the schemes within this workspace
public var schemeDescriptors: [SchemeDescriptor]
/// The side effects required for generating this workspace
public var sideEffectDescriptors: [SideEffectDescriptor]
public init(path: AbsolutePath,
xcworkspacePath: AbsolutePath,
xcworkspace: XCWorkspace,
projectDescriptors: [ProjectDescriptor],
schemeDescriptors: [SchemeDescriptor],
sideEffectDescriptors: [SideEffectDescriptor]) {
self.path = path
self.xcworkspacePath = xcworkspacePath
self.xcworkspace = xcworkspace
self.projectDescriptors = projectDescriptors
self.schemeDescriptors = schemeDescriptors
self.sideEffectDescriptors = sideEffectDescriptors
}
}

View File

@ -206,22 +206,21 @@ final class BuildPhaseGenerator: BuildPhaseGenerating {
var buildFilesCache = Set<AbsolutePath>() var buildFilesCache = Set<AbsolutePath>()
try files.sorted().forEach { buildFilePath in try files.sorted().forEach { buildFilePath in
let pathString = buildFilePath.pathString let pathString = buildFilePath.pathString
let pathRange = NSRange(location: 0, length: pathString.count) let isLocalized = pathString.contains(".lproj/")
let isLocalized = ProjectFileElements.localizedRegex.firstMatch(in: pathString, options: [], range: pathRange) != nil
let isLproj = buildFilePath.extension == "lproj" let isLproj = buildFilePath.extension == "lproj"
let isAsset = ProjectFileElements.assetRegex.firstMatch(in: pathString, options: [], range: pathRange) != nil let isAssetWithinXCAssets = pathString.contains(".xcassets/")
/// Assets that are part of a .xcassets folder /// Assets that are part of a .xcassets folder
/// are not added individually. The whole folder is added /// are not added individually. The whole folder is added
/// instead as a group. /// instead as a group.
if isAsset { if isAssetWithinXCAssets {
return return
} }
var element: (element: PBXFileElement, path: AbsolutePath)? var element: (element: PBXFileElement, path: AbsolutePath)?
if isLocalized { if isLocalized {
let name = buildFilePath.components.last! let name = buildFilePath.basename
let path = buildFilePath.parentDirectory.parentDirectory.appending(component: name) let path = buildFilePath.parentDirectory.parentDirectory.appending(component: name)
guard let group = fileElements.group(path: path) else { guard let group = fileElements.group(path: path) else {
throw BuildPhaseGenerationError.missingFileReference(buildFilePath) throw BuildPhaseGenerationError.missingFileReference(buildFilePath)

View File

@ -14,10 +14,11 @@ protocol DerivedFileGenerating {
/// - Throws: An error if the generation of the derived files errors. /// - Throws: An error if the generation of the derived files errors.
/// - Returns: A project that might have got mutated after the generation of derived files, and a /// - Returns: A project that might have got mutated after the generation of derived files, and a
/// function to be called after the project generation to delete the derived files that are not necessary anymore. /// function to be called after the project generation to delete the derived files that are not necessary anymore.
func generate(graph: Graphing, project: Project, sourceRootPath: AbsolutePath) throws -> (Project, () throws -> Void) func generate(graph: Graphing, project: Project, sourceRootPath: AbsolutePath) throws -> (Project, [SideEffectDescriptor])
} }
final class DerivedFileGenerator: DerivedFileGenerating { final class DerivedFileGenerator: DerivedFileGenerating {
typealias ProjectTransformation = (project: Project, sideEffects: [SideEffectDescriptor])
fileprivate static let derivedFolderName = "Derived" fileprivate static let derivedFolderName = "Derived"
fileprivate static let infoPlistsFolderName = "InfoPlists" fileprivate static let infoPlistsFolderName = "InfoPlists"
@ -32,17 +33,10 @@ final class DerivedFileGenerator: DerivedFileGenerating {
self.infoPlistContentProvider = infoPlistContentProvider self.infoPlistContentProvider = infoPlistContentProvider
} }
func generate(graph: Graphing, project: Project, sourceRootPath: AbsolutePath) throws -> (Project, () throws -> Void) { func generate(graph: Graphing, project: Project, sourceRootPath: AbsolutePath) throws -> (Project, [SideEffectDescriptor]) {
/// The files that are not necessary anymore should be deleted after we generate the project. let transformation = try generateInfoPlists(graph: graph, project: project, sourceRootPath: sourceRootPath)
/// Otherwise, Xcode will try to reload their references before the project generation.
var toDelete: Set<AbsolutePath> = []
let (project, infoPlistsToDelete) = try generateInfoPlists(graph: graph, project: project, sourceRootPath: sourceRootPath)
toDelete.formUnion(infoPlistsToDelete) return (transformation.project, transformation.sideEffects)
return (project, {
try toDelete.forEach { try FileHandler.shared.delete($0) }
})
} }
/// Genreates the Info.plist files. /// Genreates the Info.plist files.
@ -53,8 +47,9 @@ final class DerivedFileGenerator: DerivedFileGenerating {
/// - sourceRootPath: Path to the directory in which the project is getting generated. /// - sourceRootPath: Path to the directory in which the project is getting generated.
/// - Returns: A set with paths to the Info.plist files that are no longer necessary and therefore need to be removed. /// - Returns: A set with paths to the Info.plist files that are no longer necessary and therefore need to be removed.
/// - Throws: An error if the encoding of the Info.plist content fails. /// - Throws: An error if the encoding of the Info.plist content fails.
func generateInfoPlists(graph: Graphing, project: Project, sourceRootPath: AbsolutePath) throws -> (Project, Set<AbsolutePath>) { func generateInfoPlists(graph: Graphing,
let infoPlistsPath = DerivedFileGenerator.infoPlistsPath(sourceRootPath: sourceRootPath) project: Project,
sourceRootPath: AbsolutePath) throws -> ProjectTransformation {
let targetsWithGeneratableInfoPlists = project.targets.filter { let targetsWithGeneratableInfoPlists = project.targets.filter {
if let infoPlist = $0.infoPlist, case InfoPlist.file = infoPlist { if let infoPlist = $0.infoPlist, case InfoPlist.file = infoPlist {
return false return false
@ -70,44 +65,58 @@ final class DerivedFileGenerator: DerivedFileGenerating {
} }
let toDelete = Set(existing).subtracting(new) let toDelete = Set(existing).subtracting(new)
if !FileHandler.shared.exists(infoPlistsPath), !targetsWithGeneratableInfoPlists.isEmpty { let deletions = toDelete.map {
try FileHandler.shared.createFolder(infoPlistsPath) SideEffectDescriptor.file(FileDescriptor(path: $0, state: .absent))
} }
// Generate the Info.plist // Generate the Info.plist
let newTargets = try project.targets.map { (target) -> Target in let transformation = try project.targets.map { (target) -> (Target, [SideEffectDescriptor]) in
guard targetsWithGeneratableInfoPlists.contains(target) else { return target } guard targetsWithGeneratableInfoPlists.contains(target),
let infoPlist = target.infoPlist else {
return (target, [])
}
guard let infoPlist = target.infoPlist else { return target } guard let dictionary = infoPlistDictionary(infoPlist: infoPlist,
project: project,
let dictionary: [String: Any] target: target,
graph: graph) else {
if case let InfoPlist.dictionary(content) = infoPlist { return (target, [])
dictionary = content.mapValues { $0.value }
} else if case let InfoPlist.extendingDefault(extended) = infoPlist,
let content = self.infoPlistContentProvider.content(graph: graph,
project: project,
target: target,
extendedWith: extended) {
dictionary = content
} else {
return target
} }
let path = DerivedFileGenerator.infoPlistPath(target: target, sourceRootPath: sourceRootPath) let path = DerivedFileGenerator.infoPlistPath(target: target, sourceRootPath: sourceRootPath)
if FileHandler.shared.exists(path) { try FileHandler.shared.delete(path) }
let data = try PropertyListSerialization.data(fromPropertyList: dictionary, let data = try PropertyListSerialization.data(fromPropertyList: dictionary,
format: .xml, format: .xml,
options: 0) options: 0)
try data.write(to: path.url) let sideEffet = SideEffectDescriptor.file(FileDescriptor(path: path, contents: data))
// Override the Info.plist value to point to te generated one // Override the Info.plist value to point to te generated one
return target.with(infoPlist: InfoPlist.file(path: path)) return (target.with(infoPlist: InfoPlist.file(path: path)), [sideEffet])
} }
return (project.with(targets: newTargets), toDelete) return (project: project.with(targets: transformation.map { $0.0 }),
sideEffects: deletions + transformation.flatMap { $0.1 })
}
private func infoPlistDictionary(infoPlist: InfoPlist,
project: Project,
target: Target,
graph: Graphing) -> [String: Any]? {
switch infoPlist {
case let .dictionary(content):
return content.mapValues { $0.value }
case let .extendingDefault(extended):
if let content = infoPlistContentProvider.content(graph: graph,
project: project,
target: target,
extendedWith: extended) {
return content
}
return nil
default:
return nil
}
} }
/// Returns the path to the directory that contains all the derived files. /// Returns the path to the directory that contains all the derived files.

View File

@ -0,0 +1,110 @@
import Basic
import Foundation
import TuistCore
import TuistSupport
/// Project Generation Configuration
///
/// Allow specifying additional generation options
/// for an individual project.
public struct ProjectGenerationConfig {
/// The source root path of the generated Xcode project
public var sourceRootPath: AbsolutePath?
/// The xcodeproj file path
public var xcodeprojPath: AbsolutePath?
public init(sourceRootPath: AbsolutePath? = nil,
xcodeprojPath: AbsolutePath? = nil) {
self.sourceRootPath = sourceRootPath
self.xcodeprojPath = xcodeprojPath
}
}
/// Descriptor Generator
///
/// This component genertes`XcodeProj` representations of a given graph model.
/// No sideeffects take place as a result of this generation.
///
/// - Seealso: `GraphLoader`
/// - Seealso: `GraphLinter`
/// - Seealso: `XcodeProjWriter`
///
public protocol DescriptorGenerating {
/// Generate an individual project descriptor
///
/// - Parameters:
/// - project: Project model
/// - graph: Graph model
///
/// - Seealso: `GraphLoader`
func generateProject(project: Project, graph: Graph) throws -> ProjectDescriptor
/// Generate an individual project descriptor with some additional configuration
///
/// - Parameters:
/// - project: Project model
/// - graph: Graph model
/// - config: The project generation configuration
///
/// - Seealso: `GraphLoader`
func generateProject(project: Project, graph: Graph, config: ProjectGenerationConfig) throws -> ProjectDescriptor
/// Generate a workspace descriptor
///
/// - Parameters:
/// - project: Workspace model
/// - graph: Graph model
///
/// - Seealso: `GraphLoader`
func generateWorkspace(workspace: Workspace, graph: Graph) throws -> WorkspaceDescriptor
}
// MARK: -
/// Default implementation of `DescriptorGenerating`
public final class DescriptorGenerator: DescriptorGenerating {
private let workspaceGenerator: WorkspaceGenerating
private let projectGenerator: ProjectGenerating
public convenience init(defaultSettingsProvider: DefaultSettingsProviding = DefaultSettingsProvider()) {
let configGenerator = ConfigGenerator(defaultSettingsProvider: defaultSettingsProvider)
let targetGenerator = TargetGenerator(configGenerator: configGenerator)
let schemesGenerator = SchemesGenerator()
let workspaceStructureGenerator = WorkspaceStructureGenerator()
let projectGenerator = ProjectGenerator(targetGenerator: targetGenerator,
configGenerator: configGenerator,
schemesGenerator: schemesGenerator)
let workspaceGenerator = WorkspaceGenerator(projectGenerator: projectGenerator,
workspaceStructureGenerator: workspaceStructureGenerator,
schemesGenerator: schemesGenerator)
self.init(workspaceGenerator: workspaceGenerator,
projectGenerator: projectGenerator)
}
init(workspaceGenerator: WorkspaceGenerating,
projectGenerator: ProjectGenerating) {
self.workspaceGenerator = workspaceGenerator
self.projectGenerator = projectGenerator
}
public func generateProject(project: Project, graph: Graph) throws -> ProjectDescriptor {
try projectGenerator.generate(project: project,
graph: graph,
sourceRootPath: nil,
xcodeprojPath: nil)
}
public func generateProject(project: Project, graph: Graph, config: ProjectGenerationConfig) throws -> ProjectDescriptor {
try projectGenerator.generate(project: project,
graph: graph,
sourceRootPath: config.sourceRootPath,
xcodeprojPath: config.xcodeprojPath)
}
public func generateWorkspace(workspace: Workspace, graph: Graph) throws -> WorkspaceDescriptor {
try workspaceGenerator.generate(workspace: workspace,
path: workspace.path,
graph: graph)
}
}

View File

@ -4,6 +4,11 @@ import TuistCore
import TuistSupport import TuistSupport
/// A component responsible for generating Xcode projects & workspaces /// A component responsible for generating Xcode projects & workspaces
@available(
*,
deprecated,
message: "Generating is deprecated and will be removed in a future Tuist version. Please use `DescriptorGenerating` instead."
)
public protocol Generating { public protocol Generating {
/// Generates an Xcode project at a given path. Only the specified project is generated (excluding its dependencies). /// Generates an Xcode project at a given path. Only the specified project is generated (excluding its dependencies).
/// ///
@ -55,11 +60,19 @@ public protocol Generating {
/// ///
/// - seealso: Generating /// - seealso: Generating
/// - seealso: GeneratorModelLoading /// - seealso: GeneratorModelLoading
@available(
*,
deprecated,
message: "Generator is deprecated and will be removed in a future Tuist version. Please use `DescriptorGenerator` instead."
)
public class Generator: Generating { public class Generator: Generating {
private let graphLoader: GraphLoading private let graphLoader: GraphLoading
private let graphLinter: GraphLinting private let graphLinter: GraphLinting
private let workspaceGenerator: WorkspaceGenerating private let workspaceGenerator: WorkspaceGenerating
private let projectGenerator: ProjectGenerating private let projectGenerator: ProjectGenerating
private let writer: XcodeProjWriting
private let cocoapodsInteractor: CocoaPodsInteracting
private let swiftPackageManagerInteractor: SwiftPackageManagerInteracting
/// Instance to lint the Tuist configuration against the system. /// Instance to lint the Tuist configuration against the system.
private let environmentLinter: EnvironmentLinting private let environmentLinter: EnvironmentLinting
@ -74,29 +87,40 @@ public class Generator: Generating {
configGenerator: configGenerator) configGenerator: configGenerator)
let environmentLinter = EnvironmentLinter() let environmentLinter = EnvironmentLinter()
let workspaceStructureGenerator = WorkspaceStructureGenerator() let workspaceStructureGenerator = WorkspaceStructureGenerator()
let cocoapodsInteractor = CocoaPodsInteractor()
let schemesGenerator = SchemesGenerator() let schemesGenerator = SchemesGenerator()
let workspaceGenerator = WorkspaceGenerator(projectGenerator: projectGenerator, let workspaceGenerator = WorkspaceGenerator(projectGenerator: projectGenerator,
workspaceStructureGenerator: workspaceStructureGenerator, workspaceStructureGenerator: workspaceStructureGenerator,
cocoapodsInteractor: cocoapodsInteractor,
schemesGenerator: schemesGenerator) schemesGenerator: schemesGenerator)
let writer = XcodeProjWriter()
let cocoapodsInteractor: CocoaPodsInteracting = CocoaPodsInteractor()
let swiftPackageManagerInteractor: SwiftPackageManagerInteracting = SwiftPackageManagerInteractor()
self.init(graphLoader: graphLoader, self.init(graphLoader: graphLoader,
graphLinter: graphLinter, graphLinter: graphLinter,
workspaceGenerator: workspaceGenerator, workspaceGenerator: workspaceGenerator,
projectGenerator: projectGenerator, projectGenerator: projectGenerator,
environmentLinter: environmentLinter) environmentLinter: environmentLinter,
writer: writer,
cocoapodsInteractor: cocoapodsInteractor,
swiftPackageManagerInteractor: swiftPackageManagerInteractor)
} }
init(graphLoader: GraphLoading, init(graphLoader: GraphLoading,
graphLinter: GraphLinting, graphLinter: GraphLinting,
workspaceGenerator: WorkspaceGenerating, workspaceGenerator: WorkspaceGenerating,
projectGenerator: ProjectGenerating, projectGenerator: ProjectGenerating,
environmentLinter: EnvironmentLinting) { environmentLinter: EnvironmentLinting,
writer: XcodeProjWriting,
cocoapodsInteractor: CocoaPodsInteracting,
swiftPackageManagerInteractor: SwiftPackageManagerInteracting) {
self.graphLoader = graphLoader self.graphLoader = graphLoader
self.graphLinter = graphLinter self.graphLinter = graphLinter
self.workspaceGenerator = workspaceGenerator self.workspaceGenerator = workspaceGenerator
self.projectGenerator = projectGenerator self.projectGenerator = projectGenerator
self.environmentLinter = environmentLinter self.environmentLinter = environmentLinter
self.writer = writer
self.cocoapodsInteractor = cocoapodsInteractor
self.swiftPackageManagerInteractor = swiftPackageManagerInteractor
} }
public func generateProject(_ project: Project, public func generateProject(_ project: Project,
@ -107,31 +131,35 @@ public class Generator: Generating {
/// are relative to the directory that contains the manifest. /// are relative to the directory that contains the manifest.
let sourceRootPath = sourceRootPath ?? project.path let sourceRootPath = sourceRootPath ?? project.path
let generatedProject = try projectGenerator.generate(project: project, let descriptor = try projectGenerator.generate(project: project,
graph: graph, graph: graph,
sourceRootPath: sourceRootPath, sourceRootPath: sourceRootPath,
xcodeprojPath: xcodeprojPath) xcodeprojPath: xcodeprojPath)
return generatedProject.path
try writer.write(project: descriptor)
return descriptor.xcodeprojPath
} }
public func generateProject(at path: AbsolutePath) throws -> (AbsolutePath, Graphing) { public func generateProject(at path: AbsolutePath) throws -> (AbsolutePath, Graphing) {
let tuistConfig = try graphLoader.loadTuistConfig(path: path) let config = try graphLoader.loadConfig(path: path)
try environmentLinter.lint(config: tuistConfig).printAndThrowIfNeeded() try environmentLinter.lint(config: config).printAndThrowIfNeeded()
let (graph, project) = try graphLoader.loadProject(path: path) let (graph, project) = try graphLoader.loadProject(path: path)
try graphLinter.lint(graph: graph).printAndThrowIfNeeded() try graphLinter.lint(graph: graph).printAndThrowIfNeeded()
let generatedProject = try projectGenerator.generate(project: project, let descriptor = try projectGenerator.generate(project: project,
graph: graph, graph: graph,
sourceRootPath: path, sourceRootPath: path,
xcodeprojPath: nil) xcodeprojPath: nil)
return (generatedProject.path, graph)
try writer.write(project: descriptor)
return (descriptor.xcodeprojPath, graph)
} }
public func generateProjectWorkspace(at path: AbsolutePath, public func generateProjectWorkspace(at path: AbsolutePath,
workspaceFiles: [AbsolutePath]) throws -> (AbsolutePath, Graphing) { workspaceFiles: [AbsolutePath]) throws -> (AbsolutePath, Graphing) {
let tuistConfig = try graphLoader.loadTuistConfig(path: path) let config = try graphLoader.loadConfig(path: path)
try environmentLinter.lint(config: tuistConfig).printAndThrowIfNeeded() try environmentLinter.lint(config: config).printAndThrowIfNeeded()
let (graph, project) = try graphLoader.loadProject(path: path) let (graph, project) = try graphLoader.loadProject(path: path)
try graphLinter.lint(graph: graph).printAndThrowIfNeeded() try graphLinter.lint(graph: graph).printAndThrowIfNeeded()
@ -141,17 +169,20 @@ public class Generator: Generating {
projects: graph.projectPaths, projects: graph.projectPaths,
additionalFiles: workspaceFiles.map(FileElement.file)) additionalFiles: workspaceFiles.map(FileElement.file))
let workspacePath = try workspaceGenerator.generate(workspace: workspace, let descriptor = try workspaceGenerator.generate(workspace: workspace,
path: path, path: path,
graph: graph, graph: graph)
tuistConfig: tuistConfig) try writer.write(workspace: descriptor)
return (workspacePath, graph)
try postGenerationActions(for: graph, workspaceName: descriptor.xcworkspacePath.basename)
return (descriptor.xcworkspacePath, graph)
} }
public func generateWorkspace(at path: AbsolutePath, public func generateWorkspace(at path: AbsolutePath,
workspaceFiles: [AbsolutePath]) throws -> (AbsolutePath, Graphing) { workspaceFiles: [AbsolutePath]) throws -> (AbsolutePath, Graphing) {
let tuistConfig = try graphLoader.loadTuistConfig(path: path) let config = try graphLoader.loadConfig(path: path)
try environmentLinter.lint(config: tuistConfig).printAndThrowIfNeeded() try environmentLinter.lint(config: config).printAndThrowIfNeeded()
let (graph, workspace) = try graphLoader.loadWorkspace(path: path) let (graph, workspace) = try graphLoader.loadWorkspace(path: path)
try graphLinter.lint(graph: graph).printAndThrowIfNeeded() try graphLinter.lint(graph: graph).printAndThrowIfNeeded()
@ -159,10 +190,18 @@ public class Generator: Generating {
.merging(projects: graph.projectPaths) .merging(projects: graph.projectPaths)
.adding(files: workspaceFiles) .adding(files: workspaceFiles)
let workspacePath = try workspaceGenerator.generate(workspace: updatedWorkspace, let descriptor = try workspaceGenerator.generate(workspace: updatedWorkspace,
path: path, path: path,
graph: graph, graph: graph)
tuistConfig: tuistConfig) try writer.write(workspace: descriptor)
return (workspacePath, graph)
try postGenerationActions(for: graph, workspaceName: descriptor.xcworkspacePath.basename)
return (descriptor.xcworkspacePath, graph)
}
private func postGenerationActions(for graph: Graph, workspaceName: String) throws {
try swiftPackageManagerInteractor.install(graph: graph, workspaceName: workspaceName)
try cocoapodsInteractor.install(graph: graph)
} }
} }

View File

@ -23,9 +23,6 @@ class ProjectFileElements {
// swiftlint:disable:next force_try // swiftlint:disable:next force_try
static let localizedRegex = try! NSRegularExpression(pattern: "(.+\\.lproj)/.+", static let localizedRegex = try! NSRegularExpression(pattern: "(.+\\.lproj)/.+",
options: []) options: [])
// swiftlint:disable:next force_try
static let assetRegex = try! NSRegularExpression(pattern: ".+/.+\\.xcassets/.+",
options: [])
// MARK: - Attributes // MARK: - Attributes

View File

@ -29,13 +29,13 @@ protocol ProjectGenerating: AnyObject {
/// - graph: Dependencies graph. /// - graph: Dependencies graph.
/// - sourceRootPath: Directory where the files are relative to. /// - sourceRootPath: Directory where the files are relative to.
/// - xcodeprojPath: Path to the Xcode project. When not given, the xcodeproj is generated at sourceRootPath. /// - xcodeprojPath: Path to the Xcode project. When not given, the xcodeproj is generated at sourceRootPath.
/// - Returns: Generated project descriptor
func generate(project: Project, func generate(project: Project,
graph: Graphing, graph: Graphing,
sourceRootPath: AbsolutePath?, sourceRootPath: AbsolutePath?,
xcodeprojPath: AbsolutePath?) throws -> GeneratedProject xcodeprojPath: AbsolutePath?) throws -> ProjectDescriptor
} }
// swiftlint:disable type_body_length
final class ProjectGenerator: ProjectGenerating { final class ProjectGenerator: ProjectGenerating {
// MARK: - Attributes // MARK: - Attributes
@ -72,11 +72,12 @@ final class ProjectGenerator: ProjectGenerating {
// MARK: - ProjectGenerating // MARK: - ProjectGenerating
// swiftlint:disable:next function_body_length
func generate(project: Project, func generate(project: Project,
graph: Graphing, graph: Graphing,
sourceRootPath: AbsolutePath? = nil, sourceRootPath: AbsolutePath? = nil,
xcodeprojPath: AbsolutePath? = nil) throws -> GeneratedProject { xcodeprojPath: AbsolutePath? = nil) throws -> ProjectDescriptor {
Printer.shared.print("Generating project \(project.name)") logger.notice("Generating project \(project.name)")
// Getting the path. // Getting the path.
let sourceRootPath = sourceRootPath ?? project.path let sourceRootPath = sourceRootPath ?? project.path
@ -84,22 +85,9 @@ final class ProjectGenerator: ProjectGenerating {
// If the xcodeproj path is not given, we generate it under the source root path. // If the xcodeproj path is not given, we generate it under the source root path.
let xcodeprojPath = xcodeprojPath ?? sourceRootPath.appending(component: "\(project.fileName).xcodeproj") let xcodeprojPath = xcodeprojPath ?? sourceRootPath.appending(component: "\(project.fileName).xcodeproj")
// Project and workspace.
return try generateProjectAndWorkspace(project: project,
graph: graph,
sourceRootPath: sourceRootPath,
xcodeprojPath: xcodeprojPath)
}
// MARK: - Fileprivate
// swiftlint:disable:next function_body_length
private func generateProjectAndWorkspace(project: Project,
graph: Graphing,
sourceRootPath: AbsolutePath,
xcodeprojPath: AbsolutePath) throws -> GeneratedProject {
// Derived files // Derived files
let (project, deleteOldDerivedFiles) = try derivedFileGenerator.generate(graph: graph, project: project, sourceRootPath: sourceRootPath) // TODO: experiment with moving this outside the project generator to avoid needing to mutate the project
let (project, sideEffects) = try derivedFileGenerator.generate(graph: graph, project: project, sourceRootPath: sourceRootPath)
let workspaceData = XCWorkspaceData(children: []) let workspaceData = XCWorkspaceData(children: [])
let workspace = XCWorkspace(data: workspaceData) let workspace = XCWorkspace(data: workspaceData)
@ -107,19 +95,13 @@ final class ProjectGenerator: ProjectGenerating {
let pbxproj = PBXProj(objectVersion: projectConstants.objectVersion, let pbxproj = PBXProj(objectVersion: projectConstants.objectVersion,
archiveVersion: projectConstants.archiveVersion, archiveVersion: projectConstants.archiveVersion,
classes: [:]) classes: [:])
let groups = ProjectGroups.generate(project: project, pbxproj: pbxproj, xcodeprojPath: xcodeprojPath, sourceRootPath: sourceRootPath)
let groups = ProjectGroups.generate(project: project,
pbxproj: pbxproj,
xcodeprojPath: xcodeprojPath,
sourceRootPath: sourceRootPath)
let fileElements = ProjectFileElements() let fileElements = ProjectFileElements()
try fileElements.generateProjectFiles(project: project, try fileElements.generateProjectFiles(project: project,
graph: graph, graph: graph,
groups: groups, groups: groups,
pbxproj: pbxproj, pbxproj: pbxproj,
sourceRootPath: sourceRootPath) sourceRootPath: sourceRootPath)
let configurationList = try configGenerator.generateProjectConfig(project: project, pbxproj: pbxproj, fileElements: fileElements) let configurationList = try configGenerator.generateProjectConfig(project: project, pbxproj: pbxproj, fileElements: fileElements)
let pbxProject = try generatePbxproject(project: project, let pbxProject = try generatePbxproject(project: project,
projectFileElements: fileElements, projectFileElements: fileElements,
@ -141,16 +123,26 @@ final class ProjectGenerator: ProjectGenerating {
try generateSwiftPackageReferences(project: project, try generateSwiftPackageReferences(project: project,
pbxproj: pbxproj, pbxproj: pbxproj,
pbxProject: pbxProject) pbxProject: pbxProject)
try deleteOldDerivedFiles()
return try write(xcodeprojPath: xcodeprojPath, let generatedProject = GeneratedProject(pbxproj: pbxproj,
nativeTargets: nativeTargets, path: xcodeprojPath,
workspace: workspace, targets: nativeTargets,
pbxproj: pbxproj, name: xcodeprojPath.basename)
project: project,
graph: graph) let schemes = try schemesGenerator.generateProjectSchemes(project: project,
generatedProject: generatedProject,
graph: graph)
let xcodeProj = XcodeProj(workspace: workspace, pbxproj: pbxproj)
return ProjectDescriptor(path: project.path,
xcodeprojPath: xcodeprojPath,
xcodeProj: xcodeProj,
schemeDescriptors: schemes,
sideEffectDescriptors: sideEffects)
} }
// MARK: - Fileprivate
private func generatePbxproject(project: Project, private func generatePbxproject(project: Project,
projectFileElements: ProjectFileElements, projectFileElements: ProjectFileElements,
configurationList: XCConfigurationList, configurationList: XCConfigurationList,
@ -158,6 +150,7 @@ final class ProjectGenerator: ProjectGenerating {
pbxproj: PBXProj) throws -> PBXProject { pbxproj: PBXProj) throws -> PBXProject {
let defaultRegions = ["en", "Base"] let defaultRegions = ["en", "Base"]
let knownRegions = Set(defaultRegions + projectFileElements.knownRegions).sorted() let knownRegions = Set(defaultRegions + projectFileElements.knownRegions).sorted()
let attributes = project.organizationName.map { ["ORGANIZATIONNAME": $0] } ?? [:]
let pbxProject = PBXProject(name: project.name, let pbxProject = PBXProject(name: project.name,
buildConfigurationList: configurationList, buildConfigurationList: configurationList,
compatibilityVersion: Xcode.Default.compatibilityVersion, compatibilityVersion: Xcode.Default.compatibilityVersion,
@ -169,7 +162,8 @@ final class ProjectGenerator: ProjectGenerating {
projectDirPath: "", projectDirPath: "",
projects: [], projects: [],
projectRoots: [], projectRoots: [],
targets: []) targets: [],
attributes: attributes)
pbxproj.add(object: pbxProject) pbxproj.add(object: pbxProject)
pbxproj.rootObject = pbxProject pbxproj.rootObject = pbxProject
return pbxProject return pbxProject
@ -264,84 +258,6 @@ final class ProjectGenerator: ProjectGenerating {
pbxProject.packages = packageReferences.sorted { $0.key < $1.key }.map { $1 } pbxProject.packages = packageReferences.sorted { $0.key < $1.key }.map { $1 }
} }
private func write(xcodeprojPath: AbsolutePath,
nativeTargets: [String: PBXNativeTarget],
workspace: XCWorkspace,
pbxproj: PBXProj,
project: Project,
graph: Graphing) throws -> GeneratedProject {
let fileHandler = FileHandler.shared
func write(xcodeprojPath: AbsolutePath) throws -> GeneratedProject {
let generatedProject = GeneratedProject(pbxproj: pbxproj,
path: xcodeprojPath,
targets: nativeTargets,
name: xcodeprojPath.basename)
try writeXcodeproj(workspace: workspace,
pbxproj: pbxproj,
xcodeprojPath: xcodeprojPath)
try writeSchemes(project: project,
generatedProject: generatedProject,
xcprojectPath: xcodeprojPath,
graph: graph)
return generatedProject
}
guard fileHandler.exists(xcodeprojPath) else {
return try write(xcodeprojPath: xcodeprojPath)
}
var generatedProject: GeneratedProject!
try fileHandler.inTemporaryDirectory { temporaryPath in
let temporaryPath = temporaryPath.appending(component: xcodeprojPath.basename)
generatedProject = try write(xcodeprojPath: temporaryPath)
let pathsToReplace = self.pathsToReplace(xcodeProjPath: temporaryPath)
try pathsToReplace.forEach {
let relativeFile = $0.relative(to: temporaryPath)
let writeToPath = xcodeprojPath.appending(relativeFile)
try fileHandler.createFolder(writeToPath.parentDirectory)
try fileHandler.replace(writeToPath, with: $0)
}
}
return generatedProject.at(path: xcodeprojPath)
}
private func pathsToReplace(xcodeProjPath: AbsolutePath) -> [AbsolutePath] {
var paths = [
"project.pbxproj",
"project.xcworkspace",
"xcshareddata/xcschemes",
]
if FileHandler.shared.exists(xcodeProjPath.appending(component: "xcuserdata")) {
paths.append("xcuserdata/**/*.xcscheme")
}
return paths.flatMap {
FileHandler.shared.glob(xcodeProjPath, glob: $0)
}
}
private func writeXcodeproj(workspace: XCWorkspace,
pbxproj: PBXProj,
xcodeprojPath: AbsolutePath) throws {
let xcodeproj = XcodeProj(workspace: workspace, pbxproj: pbxproj)
try xcodeproj.write(path: xcodeprojPath.path)
}
private func writeSchemes(project: Project,
generatedProject: GeneratedProject,
xcprojectPath: AbsolutePath,
graph: Graphing) throws {
try schemesGenerator.generateProjectSchemes(project: project,
xcprojectPath: xcprojectPath,
generatedProject: generatedProject,
graph: graph)
}
private func determineProjectConstants(graph: Graphing) throws -> ProjectConstants { private func determineProjectConstants(graph: Graphing) throws -> ProjectConstants {
if !graph.packages.isEmpty { if !graph.packages.isEmpty {
return .xcode11 return .xcode11

View File

@ -15,9 +15,8 @@ protocol SchemesGenerating {
/// - graph: Tuist graph. /// - graph: Tuist graph.
/// - Throws: A FatalError if the generation of the schemes fails. /// - Throws: A FatalError if the generation of the schemes fails.
func generateWorkspaceSchemes(workspace: Workspace, func generateWorkspaceSchemes(workspace: Workspace,
xcworkspacePath: AbsolutePath,
generatedProjects: [AbsolutePath: GeneratedProject], generatedProjects: [AbsolutePath: GeneratedProject],
graph: Graphing) throws graph: Graphing) throws -> [SchemeDescriptor]
/// Generates the schemes for the project targets. /// Generates the schemes for the project targets.
/// ///
@ -28,17 +27,8 @@ protocol SchemesGenerating {
/// - graph: Tuist graph. /// - graph: Tuist graph.
/// - Throws: A FatalError if the generation of the schemes fails. /// - Throws: A FatalError if the generation of the schemes fails.
func generateProjectSchemes(project: Project, func generateProjectSchemes(project: Project,
xcprojectPath: AbsolutePath,
generatedProject: GeneratedProject, generatedProject: GeneratedProject,
graph: Graphing) throws graph: Graphing) throws -> [SchemeDescriptor]
/// Wipes shared and user schemes at a workspace or project path. This is needed
/// currently to support the workspace scheme generation case where a workspace that
/// already exists on disk is being regenerated. Wiping the schemes directory prevents
/// older custom schemes from persisting after regeneration.
///
/// - Parameter at: Path to the workspace or project.
func wipeSchemes(at: AbsolutePath) throws
} }
// swiftlint:disable:next type_body_length // swiftlint:disable:next type_body_length
@ -49,42 +39,24 @@ final class SchemesGenerator: SchemesGenerating {
/// Default version for generated schemes. /// Default version for generated schemes.
private static let defaultVersion = "1.3" private static let defaultVersion = "1.3"
/// Generates the schemes for the workspace targets.
///
/// - Parameters:
/// - workspace: Workspace model.
/// - xcworkspacePath: Path to the workspace.
/// - generatedProject: Generated Xcode project.
/// - graph: Tuist graph.
/// - Throws: A FatalError if the generation of the schemes fails.
func generateWorkspaceSchemes(workspace: Workspace, func generateWorkspaceSchemes(workspace: Workspace,
xcworkspacePath: AbsolutePath,
generatedProjects: [AbsolutePath: GeneratedProject], generatedProjects: [AbsolutePath: GeneratedProject],
graph: Graphing) throws { graph: Graphing) throws -> [SchemeDescriptor] {
try workspace.schemes.forEach { scheme in let schemes = try workspace.schemes.map { scheme in
try generateScheme(scheme: scheme, try generateScheme(scheme: scheme,
xcPath: xcworkspacePath,
path: workspace.path, path: workspace.path,
graph: graph, graph: graph,
generatedProjects: generatedProjects) generatedProjects: generatedProjects)
} }
return schemes
} }
/// Generate schemes for a project.
///
/// - Parameters:
/// - project: Project manifest.
/// - xcprojectPath: Path to project's .xcodeproj.
/// - generatedProject: Generated Project
/// - graph: Tuist graph.
func generateProjectSchemes(project: Project, func generateProjectSchemes(project: Project,
xcprojectPath: AbsolutePath,
generatedProject: GeneratedProject, generatedProject: GeneratedProject,
graph: Graphing) throws { graph: Graphing) throws -> [SchemeDescriptor] {
/// Generate custom schemes from manifest let customSchemes: [SchemeDescriptor] = try project.schemes.map { scheme in
try project.schemes.forEach { scheme in
try generateScheme(scheme: scheme, try generateScheme(scheme: scheme,
xcPath: xcprojectPath,
path: project.path, path: project.path,
graph: graph, graph: graph,
generatedProjects: [project.path: generatedProject]) generatedProjects: [project.path: generatedProject])
@ -94,14 +66,15 @@ final class SchemesGenerator: SchemesGenerating {
let buildConfiguration = defaultDebugBuildConfigurationName(in: project) let buildConfiguration = defaultDebugBuildConfigurationName(in: project)
let userDefinedSchemes = Set(project.schemes.map(\.name)) let userDefinedSchemes = Set(project.schemes.map(\.name))
let defaultSchemeTargets = project.targets.filter { !userDefinedSchemes.contains($0.name) } let defaultSchemeTargets = project.targets.filter { !userDefinedSchemes.contains($0.name) }
try defaultSchemeTargets.forEach { target in let defaultSchemes: [SchemeDescriptor] = try defaultSchemeTargets.map { target in
let scheme = createDefaultScheme(target: target, project: project, buildConfiguration: buildConfiguration, graph: graph) let scheme = createDefaultScheme(target: target, project: project, buildConfiguration: buildConfiguration, graph: graph)
try generateScheme(scheme: scheme, return try generateScheme(scheme: scheme,
xcPath: xcprojectPath, path: project.path,
path: project.path, graph: graph,
graph: graph, generatedProjects: [project.path: generatedProject])
generatedProjects: [project.path: generatedProject])
} }
return customSchemes + defaultSchemes
} }
/// Wipes shared and user schemes at a workspace or project path. This is needed /// Wipes shared and user schemes at a workspace or project path. This is needed
@ -149,13 +122,9 @@ final class SchemesGenerator: SchemesGenerating {
/// - graph: Tuist graph. /// - graph: Tuist graph.
/// - generatedProjects: Project paths mapped to generated projects. /// - generatedProjects: Project paths mapped to generated projects.
private func generateScheme(scheme: Scheme, private func generateScheme(scheme: Scheme,
xcPath: AbsolutePath,
path: AbsolutePath, path: AbsolutePath,
graph: Graphing, graph: Graphing,
generatedProjects: [AbsolutePath: GeneratedProject]) throws { generatedProjects: [AbsolutePath: GeneratedProject]) throws -> SchemeDescriptor {
let schemeDirectory = try createSchemesDirectory(path: xcPath, shared: scheme.shared)
let schemePath = schemeDirectory.appending(component: "\(scheme.name).xcscheme")
let generatedBuildAction = try schemeBuildAction(scheme: scheme, let generatedBuildAction = try schemeBuildAction(scheme: scheme,
graph: graph, graph: graph,
rootPath: path, rootPath: path,
@ -181,17 +150,17 @@ final class SchemesGenerator: SchemesGenerating {
rootPath: path, rootPath: path,
generatedProjects: generatedProjects) generatedProjects: generatedProjects)
let scheme = XCScheme(name: scheme.name, let xcscheme = XCScheme(name: scheme.name,
lastUpgradeVersion: SchemesGenerator.defaultLastUpgradeVersion, lastUpgradeVersion: SchemesGenerator.defaultLastUpgradeVersion,
version: SchemesGenerator.defaultVersion, version: SchemesGenerator.defaultVersion,
buildAction: generatedBuildAction, buildAction: generatedBuildAction,
testAction: generatedTestAction, testAction: generatedTestAction,
launchAction: generatedLaunchAction, launchAction: generatedLaunchAction,
profileAction: generatedProfileAction, profileAction: generatedProfileAction,
analyzeAction: generatedAnalyzeAction, analyzeAction: generatedAnalyzeAction,
archiveAction: generatedArchiveAction) archiveAction: generatedArchiveAction)
try scheme.write(path: schemePath.path, override: true) return SchemeDescriptor(xcScheme: xcscheme, shared: scheme.shared)
} }
/// Generates the scheme build action. /// Generates the scheme build action.
@ -382,9 +351,23 @@ final class SchemesGenerator: SchemesGenerating {
rootPath: AbsolutePath, rootPath: AbsolutePath,
generatedProjects: [AbsolutePath: GeneratedProject]) throws -> XCScheme.ProfileAction? { generatedProjects: [AbsolutePath: GeneratedProject]) throws -> XCScheme.ProfileAction? {
guard var target = try defaultTargetReference(scheme: scheme) else { return nil } guard var target = try defaultTargetReference(scheme: scheme) else { return nil }
if let executable = scheme.runAction?.executable { var commandlineArguments: XCScheme.CommandLineArguments?
var environments: [XCScheme.EnvironmentVariable]?
if let action = scheme.profileAction, let executable = action.executable {
target = executable target = executable
if let arguments = action.arguments {
commandlineArguments = XCScheme.CommandLineArguments(arguments: commandlineArgruments(arguments.launch))
environments = environmentVariables(arguments.environment)
}
} else if let action = scheme.runAction, let executable = action.executable {
target = executable
if let arguments = action.arguments {
commandlineArguments = XCScheme.CommandLineArguments(arguments: commandlineArgruments(arguments.launch))
environments = environmentVariables(arguments.environment)
}
} }
guard let targetNode = try graph.target(path: target.projectPath, name: target.name) else { return nil } guard let targetNode = try graph.target(path: target.projectPath, name: target.name) else { return nil }
guard let buildableReference = try createBuildableReference(targetReference: target, guard let buildableReference = try createBuildableReference(targetReference: target,
graph: graph, graph: graph,
@ -400,10 +383,12 @@ final class SchemesGenerator: SchemesGenerating {
macroExpansion = buildableReference macroExpansion = buildableReference
} }
let buildConfiguration = defaultReleaseBuildConfigurationName(in: targetNode.project) let buildConfiguration = scheme.profileAction?.configurationName ?? defaultReleaseBuildConfigurationName(in: targetNode.project)
return XCScheme.ProfileAction(buildableProductRunnable: buildableProductRunnable, return XCScheme.ProfileAction(buildableProductRunnable: buildableProductRunnable,
buildConfiguration: buildConfiguration, buildConfiguration: buildConfiguration,
macroExpansion: macroExpansion) macroExpansion: macroExpansion,
commandlineArguments: commandlineArguments,
environmentVariables: environments)
} }
/// Returns the scheme analyze action. /// Returns the scheme analyze action.
@ -421,7 +406,7 @@ final class SchemesGenerator: SchemesGenerating {
guard let target = try defaultTargetReference(scheme: scheme), guard let target = try defaultTargetReference(scheme: scheme),
let targetNode = try graph.target(path: target.projectPath, name: target.name) else { return nil } let targetNode = try graph.target(path: target.projectPath, name: target.name) else { return nil }
let buildConfiguration = defaultDebugBuildConfigurationName(in: targetNode.project) let buildConfiguration = scheme.analyzeAction?.configurationName ?? defaultDebugBuildConfigurationName(in: targetNode.project)
return XCScheme.AnalyzeAction(buildConfiguration: buildConfiguration) return XCScheme.AnalyzeAction(buildConfiguration: buildConfiguration)
} }

View File

@ -28,14 +28,11 @@ protocol WorkspaceGenerating: AnyObject {
/// - workspace: Workspace model. /// - workspace: Workspace model.
/// - path: Path to the directory where the generation command is executed from. /// - path: Path to the directory where the generation command is executed from.
/// - graph: In-memory representation of the graph. /// - graph: In-memory representation of the graph.
/// - tuistConfig: Tuist configuration. /// - Returns: Generated workspace descriptor
/// - Returns: Path to the generated workspace.
/// - Throws: An error if the generation fails. /// - Throws: An error if the generation fails.
@discardableResult
func generate(workspace: Workspace, func generate(workspace: Workspace,
path: AbsolutePath, path: AbsolutePath,
graph: Graphing, graph: Graphing) throws -> WorkspaceDescriptor
tuistConfig: TuistConfig) throws -> AbsolutePath
} }
final class WorkspaceGenerator: WorkspaceGenerating { final class WorkspaceGenerator: WorkspaceGenerating {
@ -43,64 +40,55 @@ final class WorkspaceGenerator: WorkspaceGenerating {
private let projectGenerator: ProjectGenerating private let projectGenerator: ProjectGenerating
private let workspaceStructureGenerator: WorkspaceStructureGenerating private let workspaceStructureGenerator: WorkspaceStructureGenerating
private let cocoapodsInteractor: CocoaPodsInteracting
private let schemesGenerator: SchemesGenerating private let schemesGenerator: SchemesGenerating
// MARK: - Init // MARK: - Init
convenience init(defaultSettingsProvider: DefaultSettingsProviding = DefaultSettingsProvider(), convenience init(defaultSettingsProvider: DefaultSettingsProviding = DefaultSettingsProvider()) {
cocoapodsInteractor: CocoaPodsInteracting = CocoaPodsInteractor()) {
let configGenerator = ConfigGenerator(defaultSettingsProvider: defaultSettingsProvider) let configGenerator = ConfigGenerator(defaultSettingsProvider: defaultSettingsProvider)
let targetGenerator = TargetGenerator(configGenerator: configGenerator) let targetGenerator = TargetGenerator(configGenerator: configGenerator)
let projectGenerator = ProjectGenerator(targetGenerator: targetGenerator, let projectGenerator = ProjectGenerator(targetGenerator: targetGenerator,
configGenerator: configGenerator) configGenerator: configGenerator)
self.init(projectGenerator: projectGenerator, self.init(projectGenerator: projectGenerator,
workspaceStructureGenerator: WorkspaceStructureGenerator(), workspaceStructureGenerator: WorkspaceStructureGenerator(),
cocoapodsInteractor: cocoapodsInteractor,
schemesGenerator: SchemesGenerator()) schemesGenerator: SchemesGenerator())
} }
init(projectGenerator: ProjectGenerating, init(projectGenerator: ProjectGenerating,
workspaceStructureGenerator: WorkspaceStructureGenerating, workspaceStructureGenerator: WorkspaceStructureGenerating,
cocoapodsInteractor: CocoaPodsInteracting,
schemesGenerator: SchemesGenerating) { schemesGenerator: SchemesGenerating) {
self.projectGenerator = projectGenerator self.projectGenerator = projectGenerator
self.workspaceStructureGenerator = workspaceStructureGenerator self.workspaceStructureGenerator = workspaceStructureGenerator
self.cocoapodsInteractor = cocoapodsInteractor
self.schemesGenerator = schemesGenerator self.schemesGenerator = schemesGenerator
} }
// MARK: - WorkspaceGenerating // MARK: - WorkspaceGenerating
/// Generates the given workspace. func generate(workspace: Workspace, path: AbsolutePath, graph: Graphing) throws -> WorkspaceDescriptor {
///
/// - Parameters:
/// - workspace: Workspace model.
/// - path: Path to the directory where the generation command is executed from.
/// - graph: In-memory representation of the graph.
/// - tuistConfig: Tuist configuration.
/// - Returns: Path to the generated workspace.
/// - Throws: An error if the generation fails.
@discardableResult
func generate(workspace: Workspace,
path: AbsolutePath,
graph: Graphing,
tuistConfig _: TuistConfig) throws -> AbsolutePath {
let workspaceName = "\(graph.name).xcworkspace" let workspaceName = "\(graph.name).xcworkspace"
Printer.shared.print(section: "Generating workspace \(workspaceName)") logger.notice("Generating workspace \(workspaceName)", metadata: .section)
/// Projects /// Projects
let projects = try graph.projects.map { project in
var generatedProjects = [AbsolutePath: GeneratedProject]() try projectGenerator.generate(project: project,
try graph.projects.forEach { project in graph: graph,
let generatedProject = try projectGenerator.generate(project: project, sourceRootPath: project.path,
graph: graph, xcodeprojPath: nil)
sourceRootPath: project.path,
xcodeprojPath: nil)
generatedProjects[project.path] = generatedProject
} }
let generatedProjects: [AbsolutePath: GeneratedProject] = Dictionary(uniqueKeysWithValues: projects.map { project in
let pbxproj = project.xcodeProj.pbxproj
let targets = pbxproj.nativeTargets.map {
($0.name, $0)
}
return (project.path,
GeneratedProject(pbxproj: pbxproj,
path: project.xcodeprojPath,
targets: Dictionary(targets, uniquingKeysWith: { $1 }),
name: project.xcodeprojPath.basename))
})
// Workspace structure // Workspace structure
let structure = workspaceStructureGenerator.generateStructure(path: path, let structure = workspaceStructureGenerator.generateStructure(path: path,
workspace: workspace, workspace: workspace,
@ -115,128 +103,18 @@ final class WorkspaceGenerator: WorkspaceGenerating {
path: path) path: path)
} }
try write(workspace: workspace,
xcworkspace: xcWorkspace,
generatedProjects: generatedProjects,
graph: graph,
to: workspacePath)
// Schemes // Schemes
try writeSchemes(workspace: workspace, let schemes = try schemesGenerator.generateWorkspaceSchemes(workspace: workspace,
xcworkspace: xcWorkspace, generatedProjects: generatedProjects,
generatedProjects: generatedProjects, graph: graph)
graph: graph,
to: workspacePath)
// SPM return WorkspaceDescriptor(path: path,
xcworkspacePath: workspacePath,
try generatePackageDependencyManager(at: path, xcworkspace: xcWorkspace,
workspace: workspace, projectDescriptors: projects,
workspaceName: workspaceName, schemeDescriptors: schemes,
graph: graph) sideEffectDescriptors: [])
// CocoaPods
try cocoapodsInteractor.install(graph: graph)
return workspacePath
}
private func generatePackageDependencyManager(
at path: AbsolutePath,
workspace _: Workspace,
workspaceName: String,
graph: Graphing
) throws {
let packages = graph.packages
if packages.isEmpty {
return
}
let hasRemotePackage = packages.first(where: { node in
switch node.package {
case .remote: return true
case .local: return false
}
}) != nil
let rootPackageResolvedPath = path.appending(component: ".package.resolved")
let workspacePackageResolvedFolderPath = path.appending(RelativePath("\(workspaceName)/xcshareddata/swiftpm"))
let workspacePackageResolvedPath = workspacePackageResolvedFolderPath.appending(component: "Package.resolved")
if hasRemotePackage, FileHandler.shared.exists(rootPackageResolvedPath) {
try FileHandler.shared.createFolder(workspacePackageResolvedFolderPath)
if FileHandler.shared.exists(workspacePackageResolvedPath) {
try FileHandler.shared.delete(workspacePackageResolvedPath)
}
try FileHandler.shared.copy(from: rootPackageResolvedPath, to: workspacePackageResolvedPath)
}
let workspacePath = path.appending(component: workspaceName)
// -list parameter is a workaround to resolve package dependencies for given workspace without specifying scheme
try System.shared.runAndPrint(["xcodebuild", "-resolvePackageDependencies", "-workspace", workspacePath.pathString, "-list"])
if hasRemotePackage {
if FileHandler.shared.exists(rootPackageResolvedPath) {
try FileHandler.shared.delete(rootPackageResolvedPath)
}
try FileHandler.shared.linkFile(atPath: workspacePackageResolvedPath, toPath: rootPackageResolvedPath)
}
}
private func write(workspace _: Workspace,
xcworkspace: XCWorkspace,
generatedProjects _: [AbsolutePath: GeneratedProject],
graph _: Graphing,
to: AbsolutePath) throws {
let workspaceDataFile = "contents.xcworkspacedata"
let fileHandler = FileHandler.shared
// If the workspace doesn't exist we can write it because there isn't any
// Xcode instance that might depend on it.
if !fileHandler.exists(to.appending(component: workspaceDataFile)) {
try xcworkspace.write(path: to.path)
return
}
// If the workspace exists, we want to reduce the likeliness of causing
// Xcode not to be able to reload the workspace.
// We only replace the current one if something has changed.
try fileHandler.inTemporaryDirectory { temporaryPath in
let temporaryPath = temporaryPath.appending(component: to.basename)
try xcworkspace.write(path: temporaryPath.path)
let workspaceData: (AbsolutePath) throws -> Data = {
let dataPath = $0.appending(component: workspaceDataFile)
return try Data(contentsOf: dataPath.url)
}
let currentData = try workspaceData(to)
let currentWorkspaceData = try workspaceData(temporaryPath)
guard currentData != currentWorkspaceData else {
return
}
try fileHandler.createFolder(to)
try fileHandler.replace(to.appending(component: workspaceDataFile),
with: temporaryPath.appending(component: workspaceDataFile))
}
}
private func writeSchemes(workspace: Workspace,
xcworkspace _: XCWorkspace,
generatedProjects: [AbsolutePath: GeneratedProject],
graph: Graphing,
to path: AbsolutePath) throws {
try schemesGenerator.wipeSchemes(at: path)
try schemesGenerator.generateWorkspaceSchemes(workspace: workspace,
xcworkspacePath: path,
generatedProjects: generatedProjects,
graph: graph)
} }
/// Create a XCWorkspaceDataElement.file from a path string. /// Create a XCWorkspaceDataElement.file from a path string.

View File

@ -7,14 +7,14 @@ public protocol EnvironmentLinting {
/// ///
/// - Parameter config: Tuist configuration to be linted against the system. /// - Parameter config: Tuist configuration to be linted against the system.
/// - Returns: A list of linting issues. /// - Returns: A list of linting issues.
func lint(config: TuistConfig) throws -> [LintingIssue] func lint(config: Config) throws -> [LintingIssue]
} }
public class EnvironmentLinter: EnvironmentLinting { public class EnvironmentLinter: EnvironmentLinting {
/// Default constructor. /// Default constructor.
public init() {} public init() {}
public func lint(config: TuistConfig) throws -> [LintingIssue] { public func lint(config: Config) throws -> [LintingIssue] {
var issues = [LintingIssue]() var issues = [LintingIssue]()
issues.append(contentsOf: try lintXcodeVersion(config: config)) issues.append(contentsOf: try lintXcodeVersion(config: config))
@ -28,7 +28,7 @@ public class EnvironmentLinter: EnvironmentLinting {
/// - Parameter config: Tuist configuration. /// - Parameter config: Tuist configuration.
/// - Returns: An array with a linting issue if the selected version is not compatible. /// - Returns: An array with a linting issue if the selected version is not compatible.
/// - Throws: An error if there's an error obtaining the selected Xcode version. /// - Throws: An error if there's an error obtaining the selected Xcode version.
func lintXcodeVersion(config: TuistConfig) throws -> [LintingIssue] { func lintXcodeVersion(config: Config) throws -> [LintingIssue] {
guard case let CompatibleXcodeVersions.list(compatibleVersions) = config.compatibleXcodeVersions else { guard case let CompatibleXcodeVersions.list(compatibleVersions) = config.compatibleXcodeVersions else {
return [] return []
} }

View File

@ -17,8 +17,10 @@ public class GraphLinter: GraphLinting {
// MARK: - Init // MARK: - Init
public convenience init() { public convenience init() {
self.init(projectLinter: ProjectLinter(), let projectLinter = ProjectLinter()
staticProductsLinter: StaticProductsGraphLinter()) let staticProductsLinter = StaticProductsGraphLinter()
self.init(projectLinter: projectLinter,
staticProductsLinter: staticProductsLinter)
} }
init(projectLinter: ProjectLinting, init(projectLinter: ProjectLinting,

View File

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

View File

@ -28,7 +28,7 @@ enum CocoaPodsInteractorError: FatalError, Equatable {
} }
} }
protocol CocoaPodsInteracting { public protocol CocoaPodsInteracting {
/// Runs 'pod install' for all the CocoaPods dependencies that have been indicated in the graph. /// Runs 'pod install' for all the CocoaPods dependencies that have been indicated in the graph.
/// ///
/// - Parameter graph: Project graph. /// - Parameter graph: Project graph.
@ -36,17 +36,19 @@ protocol CocoaPodsInteracting {
func install(graph: Graphing) throws func install(graph: Graphing) throws
} }
final class CocoaPodsInteractor: CocoaPodsInteracting { public final class CocoaPodsInteractor: CocoaPodsInteracting {
public init() {}
/// Runs 'pod install' for all the CocoaPods dependencies that have been indicated in the graph. /// Runs 'pod install' for all the CocoaPods dependencies that have been indicated in the graph.
/// ///
/// - Parameter graph: Project graph. /// - Parameter graph: Project graph.
/// - Throws: An error if the installation of the pods fails. /// - Throws: An error if the installation of the pods fails.
func install(graph: Graphing) throws { public func install(graph: Graphing) throws {
do { do {
try install(graph: graph, updatingRepo: false) try install(graph: graph, updatingRepo: false)
} catch let error as CocoaPodsInteractorError { } catch let error as CocoaPodsInteractorError {
if case CocoaPodsInteractorError.outdatedRepository = error { if case CocoaPodsInteractorError.outdatedRepository = error {
Printer.shared.print(warning: "The local CocoaPods specs repository is outdated. Re-running 'pod install' updating the repository.") logger.warning("The local CocoaPods specs repository is outdated. Re-running 'pod install' updating the repository.")
try self.install(graph: graph, updatingRepo: true) try self.install(graph: graph, updatingRepo: true)
} else { } else {
throw error throw error
@ -81,7 +83,8 @@ final class CocoaPodsInteractor: CocoaPodsInteracting {
// The installation of Pods might fail if the local repository that contains the specs // The installation of Pods might fail if the local repository that contains the specs
// is outdated. // is outdated.
Printer.shared.print(section: "Installing CocoaPods dependencies defined in \(node.podfilePath)") logger.notice("Installing CocoaPods dependencies defined in \(node.podfilePath)", metadata: .section)
var mightNeedRepoUpdate: Bool = false var mightNeedRepoUpdate: Bool = false
let outputClosure: ([UInt8]) -> Void = { bytes in let outputClosure: ([UInt8]) -> Void = { bytes in
let content = String(data: Data(bytes), encoding: .utf8) let content = String(data: Data(bytes), encoding: .utf8)

View File

@ -0,0 +1,85 @@
import Basic
import Foundation
import TuistCore
import TuistSupport
/// Swift Package Manager Interactor
///
/// This component is responsible for resolving
/// any Swift package manager dependencies declared
/// within projects in the graph.
///
public protocol SwiftPackageManagerInteracting {
/// Installs Swift Package dependencies for a given graph and workspace
///
/// - The instllation process involves performing a Swift package dependency
/// resolution to generated the `Package.resolved` file (via `xcodebuild`).
/// - This file is then symlinked to the root path of the workspace.
///
/// - Note: this should be called post generation and writing projects
/// and workspaces to disk.
///
/// - Parameters:
/// - graph: The graph of projects
/// - workspaceName: The name of the generated workspace (e.g. `MyWorkspace.xcworkspace`)
func install(graph: Graphing, workspaceName: String) throws
}
public class SwiftPackageManagerInteractor: SwiftPackageManagerInteracting {
private let fileHandler: FileHandling
public init(fileHandler: FileHandling = FileHandler.shared) {
self.fileHandler = fileHandler
}
public func install(graph: Graphing, workspaceName: String) throws {
try generatePackageDependencyManager(at: graph.entryPath,
workspaceName: workspaceName,
graph: graph)
}
private func generatePackageDependencyManager(
at path: AbsolutePath,
workspaceName: String,
graph: Graphing
) throws {
let packages = graph.packages
guard !packages.remotePackages.isEmpty else {
return
}
let rootPackageResolvedPath = path.appending(component: ".package.resolved")
let workspacePackageResolvedFolderPath = path.appending(RelativePath("\(workspaceName)/xcshareddata/swiftpm"))
let workspacePackageResolvedPath = workspacePackageResolvedFolderPath.appending(component: "Package.resolved")
if fileHandler.exists(rootPackageResolvedPath) {
try fileHandler.createFolder(workspacePackageResolvedFolderPath)
if fileHandler.exists(workspacePackageResolvedPath) {
try fileHandler.delete(workspacePackageResolvedPath)
}
try fileHandler.copy(from: rootPackageResolvedPath, to: workspacePackageResolvedPath)
}
let workspacePath = path.appending(component: workspaceName)
// -list parameter is a workaround to resolve package dependencies for given workspace without specifying scheme
try System.shared.runAndPrint(["xcodebuild", "-resolvePackageDependencies", "-workspace", workspacePath.pathString, "-list"])
if fileHandler.exists(rootPackageResolvedPath) {
try fileHandler.delete(rootPackageResolvedPath)
}
try fileHandler.linkFile(atPath: workspacePackageResolvedPath, toPath: rootPackageResolvedPath)
}
}
private extension Array where Element == PackageNode {
var remotePackages: [PackageNode] {
compactMap { node in
switch node.package {
case .remote:
return node
default:
return nil
}
}
}
}

View File

@ -0,0 +1,99 @@
import Basic
import Foundation
import TuistSupport
import XcodeProj
public protocol XcodeProjWriting {
func write(project: ProjectDescriptor) throws
func write(workspace: WorkspaceDescriptor) throws
}
// MARK: -
public final class XcodeProjWriter: XcodeProjWriting {
private let fileHandler: FileHandling
private let system: Systeming
public init(fileHandler: FileHandling = FileHandler.shared,
system: Systeming = System.shared) {
self.fileHandler = fileHandler
self.system = system
}
public func write(project: ProjectDescriptor) throws {
let project = enrichingXcodeProjWithSchemes(descriptor: project)
try project.xcodeProj.write(path: project.xcodeprojPath.path)
try project.schemeDescriptors.forEach { try write(scheme: $0, xccontainerPath: project.xcodeprojPath) }
try project.sideEffectDescriptors.forEach(perform)
}
public func write(workspace: WorkspaceDescriptor) throws {
try workspace.projectDescriptors.forEach(write)
try workspace.xcworkspace.write(path: workspace.xcworkspacePath.path, override: true)
try workspace.schemeDescriptors.forEach { try write(scheme: $0, xccontainerPath: workspace.xcworkspacePath) }
try workspace.sideEffectDescriptors.forEach(perform)
}
// MARK: -
private func enrichingXcodeProjWithSchemes(descriptor: ProjectDescriptor) -> ProjectDescriptor {
let sharedSchemes = descriptor.schemeDescriptors.filter { $0.shared }
let userSchemes = descriptor.schemeDescriptors.filter { !$0.shared }
let xcodeProj = descriptor.xcodeProj
let sharedData = xcodeProj.sharedData ?? XCSharedData(schemes: [])
sharedData.schemes.append(contentsOf: sharedSchemes.map { $0.xcScheme })
xcodeProj.sharedData = sharedData
return ProjectDescriptor(path: descriptor.path,
xcodeprojPath: descriptor.xcodeprojPath,
xcodeProj: descriptor.xcodeProj,
schemeDescriptors: userSchemes,
sideEffectDescriptors: descriptor.sideEffectDescriptors)
}
private func write(scheme: SchemeDescriptor,
xccontainerPath: AbsolutePath) throws {
let schemeDirectory = self.schemeDirectory(path: xccontainerPath, shared: scheme.shared)
let schemePath = schemeDirectory.appending(component: "\(scheme.xcScheme.name).xcscheme")
try fileHandler.createFolder(schemeDirectory)
try scheme.xcScheme.write(path: schemePath.path, override: true)
}
private func perform(sideEffect: SideEffectDescriptor) throws {
switch sideEffect {
case let .file(file):
try process(file: file)
case let .command(command):
try perform(command: command)
}
}
private func process(file: FileDescriptor) throws {
switch file.state {
case .present:
try fileHandler.createFolder(file.path.parentDirectory)
if let contents = file.contents {
try contents.write(to: file.path.url)
} else {
try fileHandler.touch(file.path)
}
case .absent:
try fileHandler.delete(file.path)
}
}
private func perform(command: CommandDescriptor) throws {
try system.run(command.command)
}
private func schemeDirectory(path: AbsolutePath, shared: Bool = true) -> AbsolutePath {
if shared {
return path.appending(RelativePath("xcshareddata/xcschemes"))
} else {
let username = NSUserName()
return path.appending(RelativePath("xcuserdata/\(username).xcuserdatad/xcschemes"))
}
}
}

View File

@ -0,0 +1,31 @@
import Basic
import Foundation
import XcodeProj
@testable import TuistGenerator
public extension ProjectDescriptor {
static func test(path: AbsolutePath = AbsolutePath("/Test"),
xcodeprojPath: AbsolutePath? = nil,
schemes: [SchemeDescriptor] = [],
sideEffects: [SideEffectDescriptor] = []) -> ProjectDescriptor {
let mainGroup = PBXGroup()
let configurationList = XCConfigurationList()
let pbxProject = PBXProject(name: "Test",
buildConfigurationList: configurationList,
compatibilityVersion: "1",
mainGroup: mainGroup)
let pbxproj = PBXProj(objectVersion: 50,
archiveVersion: 10)
pbxproj.add(object: mainGroup)
pbxproj.add(object: pbxProject)
pbxproj.add(object: configurationList)
pbxproj.rootObject = pbxProject
let xcodeProj = XcodeProj(workspace: XCWorkspace(), pbxproj: pbxproj)
return ProjectDescriptor(path: path,
xcodeprojPath: xcodeprojPath ?? path.appending(component: "Test.xcodeproj"),
xcodeProj: xcodeProj,
schemeDescriptors: schemes,
sideEffectDescriptors: sideEffects)
}
}

View File

@ -0,0 +1,12 @@
import Basic
import Foundation
import XcodeProj
@testable import TuistGenerator
public extension SchemeDescriptor {
static func test(name: String, shared: Bool) -> SchemeDescriptor {
let scheme = XCScheme(name: name, lastUpgradeVersion: "1131", version: "1")
return SchemeDescriptor(xcScheme: scheme, shared: shared)
}
}

View File

@ -0,0 +1,20 @@
import Basic
import Foundation
import XcodeProj
@testable import TuistGenerator
public extension WorkspaceDescriptor {
static func test(path: AbsolutePath = AbsolutePath("/Test"),
xcworkspacePath: AbsolutePath = AbsolutePath("/Test/Project.xcworkspace"),
projects: [ProjectDescriptor] = [],
schemes: [SchemeDescriptor] = [],
sideEffects: [SideEffectDescriptor] = []) -> WorkspaceDescriptor {
WorkspaceDescriptor(path: path,
xcworkspacePath: xcworkspacePath,
xcworkspace: XCWorkspace(),
projectDescriptors: projects,
schemeDescriptors: schemes,
sideEffectDescriptors: sideEffects)
}
}

View File

@ -0,0 +1,38 @@
import Basic
import Foundation
import TuistCore
import XcodeProj
@testable import TuistGenerator
final class MockDescriptorGenerator: DescriptorGenerating {
enum MockError: Error {
case stubNotImplemented
}
var generateProjectSub: ((Project, Graph) throws -> ProjectDescriptor)?
func generateProject(project: Project, graph: Graph) throws -> ProjectDescriptor {
guard let generateProjectSub = generateProjectSub else {
throw MockError.stubNotImplemented
}
return try generateProjectSub(project, graph)
}
var generateProjectWithConfigStub: ((Project, Graph, ProjectGenerationConfig) throws -> ProjectDescriptor)?
func generateProject(project: Project, graph: Graph, config: ProjectGenerationConfig) throws -> ProjectDescriptor {
guard let generateProjectWithConfigStub = generateProjectWithConfigStub else {
throw MockError.stubNotImplemented
}
return try generateProjectWithConfigStub(project, graph, config)
}
var generateWorkspaceStub: ((Workspace, Graph) throws -> WorkspaceDescriptor)?
func generateWorkspace(workspace: Workspace, graph: Graph) throws -> WorkspaceDescriptor {
guard let generateWorkspaceStub = generateWorkspaceStub else {
throw MockError.stubNotImplemented
}
return try generateWorkspaceStub(workspace, graph)
}
}

View File

@ -0,0 +1,15 @@
import Foundation
@testable import TuistGenerator
final class MockXcodeProjWriter: XcodeProjWriting {
var writeProjectCalls: [ProjectDescriptor] = []
func write(project: ProjectDescriptor) throws {
writeProjectCalls.append(project)
}
var writeworkspaceCalls: [WorkspaceDescriptor] = []
func write(workspace: WorkspaceDescriptor) throws {
writeworkspaceCalls.append(workspace)
}
}

View File

@ -4,11 +4,11 @@ import TuistCore
public final class MockEnvironmentLinter: EnvironmentLinting { public final class MockEnvironmentLinter: EnvironmentLinting {
public var lintStub: [LintingIssue]? public var lintStub: [LintingIssue]?
public var lintArgs: [TuistConfig] = [] public var lintArgs: [Config] = []
public init() {} public init() {}
public func lint(config: TuistConfig) throws -> [LintingIssue] { public func lint(config: Config) throws -> [LintingIssue] {
lintArgs.append(config) lintArgs.append(config)
return lintStub ?? [] return lintStub ?? []
} }

View File

@ -0,0 +1 @@
import Foundation

View File

@ -17,10 +17,7 @@ protocol CacheControlling {
final class CacheController: CacheControlling { final class CacheController: CacheControlling {
/// Xcode project generator. /// Xcode project generator.
private let generator: Generating private let generator: ProjectGenerating
/// Manifest loader.
private let manifestLoader: ManifestLoading
/// Utility to build the xcframeworks. /// Utility to build the xcframeworks.
private let xcframeworkBuilder: XCFrameworkBuilding private let xcframeworkBuilder: XCFrameworkBuilding
@ -31,28 +28,26 @@ final class CacheController: CacheControlling {
/// Cache. /// Cache.
private let cache: CacheStoraging private let cache: CacheStoraging
init(generator: Generating = Generator(), init(generator: ProjectGenerating = ProjectGenerator(),
manifestLoader: ManifestLoading = ManifestLoader(),
xcframeworkBuilder: XCFrameworkBuilding = XCFrameworkBuilder(xcodeBuildController: XcodeBuildController()), xcframeworkBuilder: XCFrameworkBuilding = XCFrameworkBuilder(xcodeBuildController: XcodeBuildController()),
cache: CacheStoraging = Cache(), cache: CacheStoraging = Cache(),
graphContentHasher: GraphContentHashing = GraphContentHasher()) { graphContentHasher: GraphContentHashing = GraphContentHasher()) {
self.generator = generator self.generator = generator
self.manifestLoader = manifestLoader
self.xcframeworkBuilder = xcframeworkBuilder self.xcframeworkBuilder = xcframeworkBuilder
self.cache = cache self.cache = cache
self.graphContentHasher = graphContentHasher self.graphContentHasher = graphContentHasher
} }
func cache(path: AbsolutePath) throws { func cache(path: AbsolutePath) throws {
let (path, graph) = try generator.generateWorkspace(at: path, manifestLoader: manifestLoader) let (path, graph) = try generator.generateWithGraph(path: path, projectOnly: false)
Printer.shared.print(section: "Hashing cacheable frameworks") logger.notice("Hashing cacheable frameworks")
let cacheableTargets = try self.cacheableTargets(graph: graph) let cacheableTargets = try self.cacheableTargets(graph: graph)
let completables = try cacheableTargets.map { try buildAndCacheXCFramework(path: path, target: $0.key, hash: $0.value) } let completables = try cacheableTargets.map { try buildAndCacheXCFramework(path: path, target: $0.key, hash: $0.value) }
_ = try Completable.zip(completables).toBlocking().last() _ = try Completable.zip(completables).toBlocking().last()
Printer.shared.print(success: "All cacheable frameworks have been cached successfully") logger.notice("All cacheable frameworks have been cached successfully", metadata: .success)
} }
/// Returns all the targets that are cacheable and their hashes. /// Returns all the targets that are cacheable and their hashes.
@ -61,7 +56,7 @@ final class CacheController: CacheControlling {
try graphContentHasher.contentHashes(for: graph) try graphContentHasher.contentHashes(for: graph)
.filter { target, hash in .filter { target, hash in
if let exists = try self.cache.exists(hash: hash).toBlocking().first(), exists { if let exists = try self.cache.exists(hash: hash).toBlocking().first(), exists {
Printer.shared.print("The target \(.bold(.raw(target.name))) with hash \(.bold(.raw(hash))) is already in the cache. Skipping...") logger.pretty("The target \(.bold(.raw(target.name))) with hash \(.bold(.raw(hash))) is already in the cache. Skipping...")
return false return false
} }
return true return true

View File

@ -27,6 +27,6 @@ class BuildCommand: NSObject, RawCommand {
} }
func run(arguments _: [String]) throws { func run(arguments _: [String]) throws {
Printer.shared.print("Command not available yet") logger.notice("Command not available yet")
} }
} }

View File

@ -43,7 +43,7 @@ public final class CommandRegistry {
} }
public static func processArguments() -> [String] { public static func processArguments() -> [String] {
Array(ProcessInfo.processInfo.arguments) Array(ProcessInfo.processInfo.arguments).filter { $0 != "--verbose" }
} }
// MARK: - Internal // MARK: - Internal

View File

@ -44,6 +44,6 @@ class DumpCommand: NSObject, Command {
} }
let project = try manifestLoader.loadProject(at: path) let project = try manifestLoader.loadProject(at: path)
let json: JSON = try project.toJSON() let json: JSON = try project.toJSON()
Printer.shared.print("\(json.toString(prettyPrint: true))") logger.notice("\(json.toString(prettyPrint: true))")
} }
} }

View File

@ -52,10 +52,10 @@ class EditCommand: NSObject, Command {
try! FileHandler.shared.delete(EditCommand.temporaryDirectory.path) try! FileHandler.shared.delete(EditCommand.temporaryDirectory.path)
exit(0) exit(0)
} }
Printer.shared.print(success: "Opening Xcode to edit the project. Press \(.keystroke("CTRL + C")) once you are done editing") logger.pretty("Opening Xcode to edit the project. Press \(.keystroke("CTRL + C")) once you are done editing")
try opener.open(path: xcodeprojPath, wait: true) try opener.open(path: xcodeprojPath, wait: true)
} else { } else {
Printer.shared.print(success: "Xcode project generated at \(xcodeprojPath.pathString)") logger.notice("Xcode project generated at \(xcodeprojPath.pathString)", metadata: .success)
} }
} }

View File

@ -18,10 +18,7 @@ class FocusCommand: NSObject, Command {
// MARK: - Attributes // MARK: - Attributes
/// Generator instance to generate the project workspace. /// Generator instance to generate the project workspace.
private let generator: Generating private let generator: ProjectGenerating
/// Manifest loader instance that can load project maifests from disk
private let manifestLoader: ManifestLoading
/// Opener instance to run open in the system. /// Opener instance to run open in the system.
private let opener: Opening private let opener: Opening
@ -32,14 +29,8 @@ class FocusCommand: NSObject, Command {
/// ///
/// - Parameter parser: Argument parser that parses the CLI arguments. /// - Parameter parser: Argument parser that parses the CLI arguments.
required convenience init(parser: ArgumentParser) { required convenience init(parser: ArgumentParser) {
let manifestLoader = ManifestLoader()
let manifestLinter = ManifestLinter()
let modelLoader = GeneratorModelLoader(manifestLoader: manifestLoader,
manifestLinter: manifestLinter)
let generator = Generator(modelLoader: modelLoader)
self.init(parser: parser, self.init(parser: parser,
generator: generator, generator: ProjectGenerator(),
manifestLoader: manifestLoader,
opener: Opener()) opener: Opener())
} }
@ -48,24 +39,20 @@ class FocusCommand: NSObject, Command {
/// - Parameters: /// - Parameters:
/// - parser: Argument parser that parses the CLI arguments. /// - parser: Argument parser that parses the CLI arguments.
/// - generator: Generator instance to generate the project workspace. /// - generator: Generator instance to generate the project workspace.
/// - manifestLoader: Manifest loader instance that can load project maifests from disk
/// - opener: Opener instance to run open in the system. /// - opener: Opener instance to run open in the system.
init(parser: ArgumentParser, init(parser: ArgumentParser,
generator: Generating, generator: ProjectGenerating,
manifestLoader: ManifestLoading,
opener: Opening) { opener: Opening) {
parser.add(subparser: FocusCommand.command, overview: FocusCommand.overview) parser.add(subparser: FocusCommand.command, overview: FocusCommand.overview)
self.generator = generator self.generator = generator
self.manifestLoader = manifestLoader
self.opener = opener self.opener = opener
} }
func run(with _: ArgumentParser.Result) throws { func run(with _: ArgumentParser.Result) throws {
let path = FileHandler.shared.currentPath let path = FileHandler.shared.currentPath
let (workspacePath, _) = try generator.generate(at: path, let workspacePath = try generator.generate(path: path,
manifestLoader: manifestLoader, projectOnly: false)
projectOnly: false)
try opener.open(path: workspacePath) try opener.open(path: workspacePath)
} }

View File

@ -13,32 +13,25 @@ class GenerateCommand: NSObject, Command {
// MARK: - Attributes // MARK: - Attributes
private let generator: Generating
private let manifestLoader: ManifestLoading
private let clock: Clock private let clock: Clock
private let generator: ProjectGenerating
let pathArgument: OptionArgument<String> let pathArgument: OptionArgument<String>
let projectOnlyArgument: OptionArgument<Bool> let projectOnlyArgument: OptionArgument<Bool>
// MARK: - Init // MARK: - Init
required convenience init(parser: ArgumentParser) { required convenience init(parser: ArgumentParser) {
let manifestLoader = ManifestLoader() let projectGenerator = ProjectGenerator()
let manifestLinter = ManifestLinter()
let modelLoader = GeneratorModelLoader(manifestLoader: manifestLoader, manifestLinter: manifestLinter)
let generator = Generator(modelLoader: modelLoader)
self.init(parser: parser, self.init(parser: parser,
generator: generator, generator: projectGenerator,
manifestLoader: manifestLoader,
clock: WallClock()) clock: WallClock())
} }
init(parser: ArgumentParser, init(parser: ArgumentParser,
generator: Generating, generator: ProjectGenerating,
manifestLoader: ManifestLoading,
clock: Clock) { clock: Clock) {
let subParser = parser.add(subparser: GenerateCommand.command, overview: GenerateCommand.overview) let subParser = parser.add(subparser: GenerateCommand.command, overview: GenerateCommand.overview)
self.generator = generator self.generator = generator
self.manifestLoader = manifestLoader
self.clock = clock self.clock = clock
pathArgument = subParser.add(option: "--path", pathArgument = subParser.add(option: "--path",
@ -57,13 +50,12 @@ class GenerateCommand: NSObject, Command {
let path = self.path(arguments: arguments) let path = self.path(arguments: arguments)
let projectOnly = arguments.get(projectOnlyArgument) ?? false let projectOnly = arguments.get(projectOnlyArgument) ?? false
_ = try generator.generate(at: path, try generator.generate(path: path, projectOnly: projectOnly)
manifestLoader: manifestLoader,
projectOnly: projectOnly)
let time = String(format: "%.3f", timer.stop()) let time = String(format: "%.3f", timer.stop())
Printer.shared.print(success: "Project generated.")
Printer.shared.print("Total time taken: \(time)s") logger.notice("Project generated.", metadata: .success)
logger.notice("Total time taken: \(time)s")
} }
// MARK: - Fileprivate // MARK: - Fileprivate

View File

@ -45,11 +45,11 @@ class GraphCommand: NSObject, Command {
let path = FileHandler.shared.currentPath.appending(component: "graph.dot") let path = FileHandler.shared.currentPath.appending(component: "graph.dot")
if FileHandler.shared.exists(path) { if FileHandler.shared.exists(path) {
Printer.shared.print("Deleting existing graph at \(path.pathString)") logger.notice("Deleting existing graph at \(path.pathString)")
try FileHandler.shared.delete(path) try FileHandler.shared.delete(path)
} }
try FileHandler.shared.write(graph, path: path, atomically: true) try FileHandler.shared.write(graph, path: path, atomically: true)
Printer.shared.print(success: "Graph exported to \(path.pathString)") logger.notice("Graph exported to \(path.pathString)", metadata: .success)
} }
} }

View File

@ -77,6 +77,7 @@ class InitCommand: NSObject, Command {
kind: String.self, kind: String.self,
usage: "The name of the project. If it's not passed (Default: Name of the directory).", usage: "The name of the project. If it's not passed (Default: Name of the directory).",
completion: nil) completion: nil)
self.playgroundGenerator = playgroundGenerator self.playgroundGenerator = playgroundGenerator
} }
@ -92,10 +93,10 @@ class InitCommand: NSObject, Command {
try generateWorkspaceSwift(name: name, platform: platform, path: path) try generateWorkspaceSwift(name: name, platform: platform, path: path)
try generateSwiftFiles(name: name, platform: platform, path: path) try generateSwiftFiles(name: name, platform: platform, path: path)
try generatePlaygrounds(name: name, path: path, platform: platform) try generatePlaygrounds(name: name, path: path, platform: platform)
try generateTuistConfig(path: path) try generateConfig(path: path)
try generateGitIgnore(path: path) try generateGitIgnore(path: path)
Printer.shared.print(success: "Project generated at path \(path.pathString).") logger.notice("Project generated at path \(path.pathString).", metadata: .success)
} }
// MARK: - Fileprivate // MARK: - Fileprivate
@ -328,15 +329,19 @@ class InitCommand: NSObject, Command {
try content.write(to: setupPath.url, atomically: true, encoding: .utf8) try content.write(to: setupPath.url, atomically: true, encoding: .utf8)
} }
private func generateTuistConfig(path: AbsolutePath) throws { private func generateConfig(path: AbsolutePath) throws {
let content = """ let content = """
import ProjectDescription import ProjectDescription
let config = TuistConfig(generationOptions: [ let config = Config(generationOptions: [
]) ])
""" """
let setupPath = path.appending(component: Manifest.tuistConfig.fileName) let configPath = path.appending(RelativePath("\(Constants.tuistDirectoryName)/\(Manifest.config.fileName)"))
try content.write(to: setupPath.url, atomically: true, encoding: .utf8) if !FileHandler.shared.exists(configPath.parentDirectory) {
try FileHandler.shared.createFolder(configPath.parentDirectory)
}
try content.write(to: configPath.url, atomically: true, encoding: .utf8)
} }
// swiftlint:disable:next function_body_length // swiftlint:disable:next function_body_length

View File

@ -78,28 +78,28 @@ class LintCommand: NSObject, Command {
let manifests = manifestLoading.manifests(at: path) let manifests = manifestLoading.manifests(at: path)
var graph: Graphing! var graph: Graphing!
Printer.shared.print(section: "Loading the dependency graph") logger.notice("Loading the dependency graph")
if manifests.contains(.workspace) { if manifests.contains(.workspace) {
Printer.shared.print("Loading workspace at \(path.pathString)") logger.notice("Loading workspace at \(path.pathString)")
(graph, _) = try graphLoader.loadWorkspace(path: path) (graph, _) = try graphLoader.loadWorkspace(path: path)
} else if manifests.contains(.project) { } else if manifests.contains(.project) {
Printer.shared.print("Loading project at \(path.pathString)") logger.notice("Loading project at \(path.pathString)")
(graph, _) = try graphLoader.loadProject(path: path) (graph, _) = try graphLoader.loadProject(path: path)
} else { } else {
throw LintCommandError.manifestNotFound(path) throw LintCommandError.manifestNotFound(path)
} }
Printer.shared.print(section: "Running linters") logger.notice("Running linters")
let config = try graphLoader.loadTuistConfig(path: path) let config = try graphLoader.loadConfig(path: path)
var issues: [LintingIssue] = [] var issues: [LintingIssue] = []
Printer.shared.print("Linting the environment") logger.notice("Linting the environment")
issues.append(contentsOf: try environmentLinter.lint(config: config)) issues.append(contentsOf: try environmentLinter.lint(config: config))
Printer.shared.print("Linting the loaded dependency graph") logger.notice("Linting the loaded dependency graph")
issues.append(contentsOf: graphLinter.lint(graph: graph)) issues.append(contentsOf: graphLinter.lint(graph: graph))
if issues.isEmpty { if issues.isEmpty {
Printer.shared.print(success: "No linting issues found") logger.notice("No linting issues found", metadata: .success)
} else { } else {
try issues.printAndThrowIfNeeded() try issues.printAndThrowIfNeeded()
} }

View File

@ -18,6 +18,6 @@ class VersionCommand: NSObject, Command {
// MARK: - Command // MARK: - Command
func run(with _: ArgumentParser.Result) { func run(with _: ArgumentParser.Result) {
Printer.shared.print("\(Constants.version)") logger.notice("\(Constants.version)")
} }
} }

View File

@ -1,40 +0,0 @@
import Basic
import Foundation
import TuistCore
import TuistGenerator
import TuistLoader
extension Generator {
/// Initializes a generator instance with all the dependencies that are specific to Tuist.
convenience init() {
let manifestLoader = ManifestLoader()
let manifestLinter = ManifestLinter()
let modelLoader = GeneratorModelLoader(manifestLoader: manifestLoader, manifestLinter: manifestLinter)
self.init(modelLoader: modelLoader)
}
}
extension Generating {
func generate(at path: AbsolutePath,
manifestLoader: ManifestLoading,
projectOnly: Bool) throws -> (AbsolutePath, Graphing) {
if projectOnly {
return try generateProject(at: path)
} else {
return try generateWorkspace(at: path,
manifestLoader: manifestLoader)
}
}
func generateWorkspace(at path: AbsolutePath,
manifestLoader: ManifestLoading) throws -> (AbsolutePath, Graphing) {
let manifests = manifestLoader.manifests(at: path)
if manifests.contains(.workspace) {
return try generateWorkspace(at: path, workspaceFiles: [])
} else if manifests.contains(.project) {
return try generateProjectWorkspace(at: path, workspaceFiles: [])
} else {
throw ManifestLoaderError.manifestNotFound(path)
}
}
}

View File

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

View File

@ -33,7 +33,7 @@ protocol ProjectEditing: AnyObject {
final class ProjectEditor: ProjectEditing { final class ProjectEditor: ProjectEditing {
/// Project generator. /// Project generator.
let generator: Generating let generator: DescriptorGenerating
/// Project editor mapper. /// Project editor mapper.
let projectEditorMapper: ProjectEditorMapping let projectEditorMapper: ProjectEditorMapping
@ -47,16 +47,21 @@ final class ProjectEditor: ProjectEditing {
/// Utility to locate the helpers directory. /// Utility to locate the helpers directory.
let helpersDirectoryLocator: HelpersDirectoryLocating let helpersDirectoryLocator: HelpersDirectoryLocating
init(generator: Generating = Generator(), /// Xcode Project writer
private let writer: XcodeProjWriting
init(generator: DescriptorGenerating = DescriptorGenerator(),
projectEditorMapper: ProjectEditorMapping = ProjectEditorMapper(), projectEditorMapper: ProjectEditorMapping = ProjectEditorMapper(),
resourceLocator: ResourceLocating = ResourceLocator(), resourceLocator: ResourceLocating = ResourceLocator(),
manifestFilesLocator: ManifestFilesLocating = ManifestFilesLocator(), manifestFilesLocator: ManifestFilesLocating = ManifestFilesLocator(),
helpersDirectoryLocator: HelpersDirectoryLocating = HelpersDirectoryLocator()) { helpersDirectoryLocator: HelpersDirectoryLocating = HelpersDirectoryLocator(),
writer: XcodeProjWriting = XcodeProjWriter()) {
self.generator = generator self.generator = generator
self.projectEditorMapper = projectEditorMapper self.projectEditorMapper = projectEditorMapper
self.resourceLocator = resourceLocator self.resourceLocator = resourceLocator
self.manifestFilesLocator = manifestFilesLocator self.manifestFilesLocator = manifestFilesLocator
self.helpersDirectoryLocator = helpersDirectoryLocator self.helpersDirectoryLocator = helpersDirectoryLocator
self.writer = writer
} }
func edit(at: AbsolutePath, in dstDirectory: AbsolutePath) throws -> AbsolutePath { func edit(at: AbsolutePath, in dstDirectory: AbsolutePath) throws -> AbsolutePath {
@ -82,9 +87,13 @@ final class ProjectEditor: ProjectEditing {
manifests: manifests.map { $0.1 }, manifests: manifests.map { $0.1 },
helpers: helpers, helpers: helpers,
projectDescriptionPath: projectDesciptionPath) projectDescriptionPath: projectDesciptionPath)
return try generator.generateProject(project,
graph: graph, let config = ProjectGenerationConfig(sourceRootPath: project.path,
sourceRootPath: project.path,
xcodeprojPath: xcodeprojPath) xcodeprojPath: xcodeprojPath)
let descriptor = try generator.generateProject(project: project,
graph: graph,
config: config)
try writer.write(project: descriptor)
return descriptor.xcodeprojPath
} }
} }

View File

@ -0,0 +1,121 @@
import Basic
import Foundation
import TuistCore
import TuistGenerator
import TuistLoader
protocol ProjectGenerating {
@discardableResult
func generate(path: AbsolutePath, projectOnly: Bool) throws -> AbsolutePath
func generateWithGraph(path: AbsolutePath, projectOnly: Bool) throws -> (AbsolutePath, Graphing)
}
class ProjectGenerator: ProjectGenerating {
private let manifestLoader: ManifestLoading = ManifestLoader()
private let manifestLinter: ManifestLinting = ManifestLinter()
private let graphLinter: GraphLinting = GraphLinter()
private let environmentLinter: EnvironmentLinting = EnvironmentLinter()
private let generator: DescriptorGenerating = DescriptorGenerator()
private let writer: XcodeProjWriting = XcodeProjWriter()
private let cocoapodsInteractor: CocoaPodsInteracting = CocoaPodsInteractor()
private let swiftPackageManagerInteractor: SwiftPackageManagerInteracting = SwiftPackageManagerInteractor()
private let modelLoader: GeneratorModelLoading
private let graphLoader: GraphLoading
init() {
modelLoader = GeneratorModelLoader(manifestLoader: manifestLoader,
manifestLinter: manifestLinter)
graphLoader = GraphLoader(modelLoader: modelLoader)
}
func generate(path: AbsolutePath, projectOnly: Bool) throws -> AbsolutePath {
let (generatedPath, _) = try generateWithGraph(path: path, projectOnly: projectOnly)
return generatedPath
}
func generateWithGraph(path: AbsolutePath, projectOnly: Bool) throws -> (AbsolutePath, Graphing) {
let manifests = manifestLoader.manifests(at: path)
if projectOnly {
return try generateProject(path: path)
} else if manifests.contains(.workspace) {
return try generateWorkspace(path: path)
} else if manifests.contains(.project) {
return try generateProjectWorkspace(path: path)
} else {
throw ManifestLoaderError.manifestNotFound(path)
}
}
private func generateProject(path: AbsolutePath) throws -> (AbsolutePath, Graph) {
// Load
let (graph, project) = try graphLoader.loadProject(path: path)
// Lint
try lint(graph: graph)
// Generate
let projectDescriptor = try generator.generateProject(project: project, graph: graph)
// Write
try writer.write(project: projectDescriptor)
// Post Generate Actions
try postGenerationActions(for: graph, workspaceName: projectDescriptor.xcodeprojPath.basename)
return (projectDescriptor.xcodeprojPath, graph)
}
private func generateWorkspace(path: AbsolutePath) throws -> (AbsolutePath, Graph) {
// Load
let (graph, workspace) = try graphLoader.loadWorkspace(path: path)
// Lint
try lint(graph: graph)
// Generate
let updatedWorkspace = workspace.merging(projects: graph.projectPaths)
let workspaceDescriptor = try generator.generateWorkspace(workspace: updatedWorkspace,
graph: graph)
// Write
try writer.write(workspace: workspaceDescriptor)
// Post Generate Actions
try postGenerationActions(for: graph, workspaceName: workspaceDescriptor.xcworkspacePath.basename)
return (workspaceDescriptor.xcworkspacePath, graph)
}
private func generateProjectWorkspace(path: AbsolutePath) throws -> (AbsolutePath, Graph) {
// Load
let (graph, project) = try graphLoader.loadProject(path: path)
// Lint
try lint(graph: graph)
// Generate
let workspace = Workspace(path: path, name: project.name, projects: graph.projectPaths)
let workspaceDescriptor = try generator.generateWorkspace(workspace: workspace, graph: graph)
// Write
try writer.write(workspace: workspaceDescriptor)
// Post Generate Actions
try postGenerationActions(for: graph, workspaceName: workspaceDescriptor.xcworkspacePath.basename)
return (workspaceDescriptor.xcworkspacePath, graph)
}
private func lint(graph: Graphing) throws {
let config = try graphLoader.loadConfig(path: graph.entryPath)
try environmentLinter.lint(config: config).printAndThrowIfNeeded()
try graphLinter.lint(graph: graph).printAndThrowIfNeeded()
}
private func postGenerationActions(for graph: Graph, workspaceName: String) throws {
try swiftPackageManagerInteractor.install(graph: graph, workspaceName: workspaceName)
try cocoapodsInteractor.install(graph: graph)
}
}

View File

@ -7,11 +7,21 @@ import TuistSupport
public class GeneratorModelLoader: GeneratorModelLoading { public class GeneratorModelLoader: GeneratorModelLoading {
private let manifestLoader: ManifestLoading private let manifestLoader: ManifestLoading
private let manifestLinter: ManifestLinting private let manifestLinter: ManifestLinting
private let rootDirectoryLocator: RootDirectoryLocating
public init(manifestLoader: ManifestLoading, public convenience init(manifestLoader: ManifestLoading,
manifestLinter: ManifestLinting) { manifestLinter: ManifestLinting) {
self.init(manifestLoader: manifestLoader,
manifestLinter: manifestLinter,
rootDirectoryLocator: RootDirectoryLocator())
}
init(manifestLoader: ManifestLoading,
manifestLinter: ManifestLinting,
rootDirectoryLocator: RootDirectoryLocating) {
self.manifestLoader = manifestLoader self.manifestLoader = manifestLoader
self.manifestLinter = manifestLinter self.manifestLinter = manifestLinter
self.rootDirectoryLocator = rootDirectoryLocator
} }
/// Load a Project model at the specified path /// Load a Project model at the specified path
@ -22,11 +32,11 @@ public class GeneratorModelLoader: GeneratorModelLoading {
/// - Throws: Error encountered during the loading process (e.g. Missing project) /// - Throws: Error encountered during the loading process (e.g. Missing project)
public func loadProject(at path: AbsolutePath) throws -> TuistCore.Project { public func loadProject(at path: AbsolutePath) throws -> TuistCore.Project {
let manifest = try manifestLoader.loadProject(at: path) let manifest = try manifestLoader.loadProject(at: path)
let tuistConfig = try loadTuistConfig(at: path) let config = try loadConfig(at: path)
let generatorPaths = GeneratorPaths(manifestDirectory: path) let generatorPaths = GeneratorPaths(manifestDirectory: path)
try manifestLinter.lint(project: manifest).printAndThrowIfNeeded() try manifestLinter.lint(project: manifest).printAndThrowIfNeeded()
let project = try TuistCore.Project.from(manifest: manifest, generatorPaths: generatorPaths) let project = try TuistCore.Project.from(manifest: manifest, generatorPaths: generatorPaths)
return try enriched(model: project, with: tuistConfig) return try enriched(model: project, with: config)
} }
public func loadWorkspace(at path: AbsolutePath) throws -> TuistCore.Workspace { public func loadWorkspace(at path: AbsolutePath) throws -> TuistCore.Workspace {
@ -39,32 +49,55 @@ public class GeneratorModelLoader: GeneratorModelLoading {
return workspace return workspace
} }
public func loadTuistConfig(at path: AbsolutePath) throws -> TuistCore.TuistConfig { public func loadConfig(at path: AbsolutePath) throws -> TuistCore.Config {
guard let tuistConfigPath = FileHandler.shared.locateDirectoryTraversingParents(from: path, path: Manifest.tuistConfig.fileName) else { // If the Config.swift file exists in the root Tuist/ directory, we load it from there
return TuistCore.TuistConfig.default if let rootDirectoryPath = rootDirectoryLocator.locate(from: path) {
let configPath = rootDirectoryPath.appending(RelativePath("\(Constants.tuistDirectoryName)/\(Manifest.config.fileName)"))
if FileHandler.shared.exists(configPath) {
let manifest = try manifestLoader.loadConfig(at: configPath.parentDirectory)
return try TuistCore.Config.from(manifest: manifest)
}
} }
let manifest = try manifestLoader.loadTuistConfig(at: tuistConfigPath.parentDirectory) // We first try to load the deprecated file. If it doesn't exist, we load the new file name.
return try TuistCore.TuistConfig.from(manifest: manifest) let fileNames = [Manifest.config]
.flatMap { [$0.deprecatedFileName, $0.fileName] }
.compactMap { $0 }
for fileName in fileNames {
guard let configPath = FileHandler.shared.locateDirectoryTraversingParents(from: path, path: fileName) else {
continue
}
let manifest = try manifestLoader.loadConfig(at: configPath.parentDirectory)
return try TuistCore.Config.from(manifest: manifest)
}
return TuistCore.Config.default
} }
private func enriched(model: TuistCore.Project, private func enriched(model: TuistCore.Project, with config: TuistCore.Config) throws -> TuistCore.Project {
with config: TuistCore.TuistConfig) throws -> TuistCore.Project {
var enrichedModel = model var enrichedModel = model
// Xcode project file name // Xcode project file name
let xcodeFileName = xcodeFileNameOverride(from: config, for: model) let xcodeFileName = xcodeFileNameOverride(from: config, for: model)
enrichedModel = enrichedModel.replacing(fileName: xcodeFileName) enrichedModel = enrichedModel.replacing(fileName: xcodeFileName)
// Xcode project organization name
if let organizationName = organizationNameOverride(from: config) {
enrichedModel = enrichedModel.replacing(organizationName: organizationName)
}
return enrichedModel return enrichedModel
} }
private func xcodeFileNameOverride(from config: TuistCore.TuistConfig, private func xcodeFileNameOverride(from config: TuistCore.Config, for model: TuistCore.Project) -> String? {
for model: TuistCore.Project) -> String? {
var xcodeFileName = config.generationOptions.compactMap { item -> String? in var xcodeFileName = config.generationOptions.compactMap { item -> String? in
switch item { switch item {
case let .xcodeProjectName(projectName): case let .xcodeProjectName(projectName):
return projectName.description return projectName.description
default:
return nil
} }
}.first }.first
@ -74,4 +107,15 @@ public class GeneratorModelLoader: GeneratorModelLoading {
return xcodeFileName return xcodeFileName
} }
private func organizationNameOverride(from config: TuistCore.Config) -> String? {
config.generationOptions.compactMap { item -> String? in
switch item {
case let .organizationName(name):
return name
default:
return nil
}
}.first
}
} }

View File

@ -51,12 +51,12 @@ public enum ManifestLoaderError: FatalError, Equatable {
} }
public protocol ManifestLoading { public protocol ManifestLoading {
/// Loads the TuistConfig.swift in the given directory. /// Loads the Config.swift in the given directory.
/// ///
/// - Parameter path: Path to the directory that contains the TuistConfig.swift file. /// - Parameter path: Path to the directory that contains the Config.swift file.
/// - Returns: Loaded TuistConfig.swift file. /// - Returns: Loaded Config.swift file.
/// - Throws: An error if the file has a syntax error. /// - Throws: An error if the file has a syntax error.
func loadTuistConfig(at path: AbsolutePath) throws -> ProjectDescription.TuistConfig func loadConfig(at path: AbsolutePath) throws -> ProjectDescription.Config
/// Loads the Project.swift in the given directory. /// Loads the Project.swift in the given directory.
/// - Parameter path: Path to the directory that contains the Project.swift. /// - Parameter path: Path to the directory that contains the Project.swift.
@ -104,8 +104,8 @@ public class ManifestLoader: ManifestLoading {
Set(manifestFilesLocator.locate(at: path).map { $0.0 }) Set(manifestFilesLocator.locate(at: path).map { $0.0 })
} }
public func loadTuistConfig(at path: AbsolutePath) throws -> ProjectDescription.TuistConfig { public func loadConfig(at path: AbsolutePath) throws -> ProjectDescription.Config {
try loadManifest(.tuistConfig, at: path) try loadManifest(.config, at: path)
} }
public func loadProject(at path: AbsolutePath) throws -> ProjectDescription.Project { public func loadProject(at path: AbsolutePath) throws -> ProjectDescription.Project {
@ -134,12 +134,19 @@ public class ManifestLoader: ManifestLoading {
// MARK: - Private // MARK: - Private
private func loadManifest<T: Decodable>(_ manifest: Manifest, at path: AbsolutePath) throws -> T { private func loadManifest<T: Decodable>(_ manifest: Manifest, at path: AbsolutePath) throws -> T {
let manifestPath = path.appending(component: manifest.fileName) var fileNames = [manifest.fileName]
guard FileHandler.shared.exists(manifestPath) else { if let deprecatedFileName = manifest.deprecatedFileName {
throw ManifestLoaderError.manifestNotFound(manifest, path) fileNames.insert(deprecatedFileName, at: 0)
} }
let data = try loadManifestData(at: manifestPath)
return try decoder.decode(T.self, from: data) for fileName in fileNames {
let manifestPath = path.appending(component: fileName)
if !FileHandler.shared.exists(manifestPath) { continue }
let data = try loadManifestData(at: manifestPath)
return try decoder.decode(T.self, from: data)
}
throw ManifestLoaderError.manifestNotFound(manifest, path)
} }
private func loadManifestData(at path: AbsolutePath) throws -> Data { private func loadManifestData(at path: AbsolutePath) throws -> Data {

View File

@ -50,7 +50,7 @@ public class SetupLoader: SetupLoading {
.printAndThrowIfNeeded() .printAndThrowIfNeeded()
try setup.forEach { command in try setup.forEach { command in
if try !command.isMet(projectPath: path) { if try !command.isMet(projectPath: path) {
Printer.shared.print(subsection: "Configuring \(command.name)") logger.notice("Configuring \(command.name)", metadata: .subsection)
try command.meet(projectPath: path) try command.meet(projectPath: path)
} }
} }

View File

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

View File

@ -0,0 +1,13 @@
import Basic
import Foundation
import ProjectDescription
import TuistCore
extension TuistCore.AnalyzeAction {
static func from(manifest: ProjectDescription.AnalyzeAction,
generatorPaths _: GeneratorPaths) throws -> TuistCore.AnalyzeAction {
let configurationName = manifest.configurationName
return AnalyzeAction(configurationName: configurationName)
}
}

View File

@ -0,0 +1,60 @@
import Basic
import Foundation
import ProjectDescription
import TuistCore
import TuistSupport
enum ConfigManifestMapperError: FatalError {
/// Thrown when the cloud URL is invalid.
case invalidCloudURL(String)
/// Error type.
var type: ErrorType {
switch self {
case .invalidCloudURL: return .abort
}
}
/// Error description.
var description: String {
switch self {
case let .invalidCloudURL(url):
return "The cloud URL '\(url)' is not a valid URL"
}
}
}
extension TuistCore.Config {
/// Maps a ProjectDescription.Config instance into a TuistCore.Config model.
/// - Parameters:
/// - manifest: Manifest representation of Tuist config.
/// - generatorPaths: Generator paths.
static func from(manifest: ProjectDescription.Config) throws -> TuistCore.Config {
let generationOptions = try manifest.generationOptions.map { try TuistCore.Config.GenerationOption.from(manifest: $0) }
let compatibleXcodeVersions = TuistCore.CompatibleXcodeVersions.from(manifest: manifest.compatibleXcodeVersions)
var cloudURL: URL?
if let manifestCloudURL = manifest.cloudURL {
if let manifestCloudURL = URL(string: manifestCloudURL) {
cloudURL = manifestCloudURL
} else {
throw ConfigManifestMapperError.invalidCloudURL(manifestCloudURL)
}
}
return TuistCore.Config(compatibleXcodeVersions: compatibleXcodeVersions, cloudURL: cloudURL, generationOptions: generationOptions)
}
}
extension TuistCore.Config.GenerationOption {
/// Maps a ProjectDescription.Config.GenerationOptions instance into a TuistCore.Config.GenerationOptions model.
/// - Parameters:
/// - manifest: Manifest representation of Tuist config generation options
/// - generatorPaths: Generator paths.
static func from(manifest: ProjectDescription.Config.GenerationOptions) throws -> TuistCore.Config.GenerationOption {
switch manifest {
case let .xcodeProjectName(templateString):
return .xcodeProjectName(templateString.description)
case let .organizationName(name):
return .organizationName(name)
}
}
}

View File

@ -19,10 +19,10 @@ extension TuistCore.FileElement {
if files.isEmpty { if files.isEmpty {
if FileHandler.shared.isFolder(path) { if FileHandler.shared.isFolder(path) {
Printer.shared.print(warning: "'\(path.pathString)' is a directory, try using: '\(path.pathString)/**' to list its files") logger.warning("'\(path.pathString)' is a directory, try using: '\(path.pathString)/**' to list its files")
} else { } else {
// FIXME: This should be done in a linter. // FIXME: This should be done in a linter.
Printer.shared.print(warning: "No files found at: \(path.pathString)") logger.warning("No files found at: \(path.pathString)")
} }
} }
@ -32,13 +32,13 @@ extension TuistCore.FileElement {
func folderReferences(_ path: AbsolutePath) -> [AbsolutePath] { func folderReferences(_ path: AbsolutePath) -> [AbsolutePath] {
guard FileHandler.shared.exists(path) else { guard FileHandler.shared.exists(path) else {
// FIXME: This should be done in a linter. // FIXME: This should be done in a linter.
Printer.shared.print(warning: "\(path.pathString) does not exist") logger.warning("\(path.pathString) does not exist")
return [] return []
} }
guard FileHandler.shared.isFolder(path) else { guard FileHandler.shared.isFolder(path) else {
// FIXME: This should be done in a linter. // FIXME: This should be done in a linter.
Printer.shared.print(warning: "\(path.pathString) is not a directory - folder reference paths need to point to directories") logger.warning("\(path.pathString) is not a directory - folder reference paths need to point to directories")
return [] return []
} }

View File

@ -0,0 +1,22 @@
import Basic
import Foundation
import ProjectDescription
import TuistCore
extension TuistCore.ProfileAction {
static func from(manifest: ProjectDescription.ProfileAction,
generatorPaths: GeneratorPaths) throws -> TuistCore.ProfileAction {
let configurationName = manifest.configurationName
let arguments = manifest.arguments.map { TuistCore.Arguments.from(manifest: $0) }
var executableResolved: TuistCore.TargetReference?
if let executable = manifest.executable {
executableResolved = TargetReference(projectPath: try generatorPaths.resolveSchemeActionProjectPath(executable.projectPath),
name: executable.targetName)
}
return ProfileAction(configurationName: configurationName,
executable: executableResolved,
arguments: arguments)
}
}

Some files were not shown because too many files have changed in this diff Show More