Argument parser (#1154)
* Implement GenerateCommand and TuistCommand.
* Handle verbose.
* Add GenerateService tests.
* Remove service protocol.
* Revert parser changes.
* Fix swiftlint issues.
* Readd GenerateService.
* Run swiftformat.
* Adopt ArgumentParser library for Up command
* Fix formatting
* Rewrite ScaffoldCommand.
* Rewrite scaffold and list tests.
* Fix running list subcommand.
* Convert InitCommand.
* Add InitService tests.
* Fix double optional.
* Run swiftformat.
* Rewrite Focus command.
* Migrate edit command.
* Migrate dump command.
* Migrate graph command.
* Migrate lint command.
* Migrate Version command.
* Revert "Migrate Version command."
This reverts commit b4a69d89da
.
* Migrate Version command.
* Migrate build command.
* Migrate cache command.
* Migrate CreateIssue command.
* Migrate cloud commands.
* Migrate signing command.
* Migrate local command.
* Rewrite env commands.
* Remove env CommandRegistry.
* Fix install tests.
* Fix editor tests.
* Fix processing tuist command.
* Change options to flag.
* Edit changelog.
Co-authored-by: Daniel Jankowski <daniell.jankowskii@gmail.com>
This commit is contained in:
parent
623b9e16dc
commit
12c87d111e
|
@ -6,6 +6,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
|
|||
|
||||
### Changed
|
||||
|
||||
- Migrate to new argument parser https://github.com/tuist/tuist/pull/1154 by @fortmarek
|
||||
- Only warn about copying Info.plist when it's the target's Info.plist https://github.com/tuist/tuist/pull/1203 by @sgrgrsn
|
||||
|
||||
### Added
|
||||
|
|
|
@ -109,6 +109,15 @@
|
|||
"version": "0.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-argument-parser",
|
||||
"repositoryURL": "https://github.com/apple/swift-argument-parser",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "8d31a0905c346a45c87773ad50862b5b3df8dff6",
|
||||
"version": "0.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "llbuild",
|
||||
"repositoryURL": "https://github.com/apple/swift-llbuild.git",
|
||||
|
|
|
@ -38,6 +38,7 @@ let package = Package(
|
|||
.package(url: "https://github.com/stencilproject/Stencil", .branch("master")),
|
||||
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMajor(from: "4.1.0")),
|
||||
.package(url: "https://github.com/httpswift/swifter.git", .upToNextMajor(from: "1.4.7")),
|
||||
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.0.4")),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
@ -58,7 +59,7 @@ let package = Package(
|
|||
),
|
||||
.target(
|
||||
name: "TuistKit",
|
||||
dependencies: ["XcodeProj", "SPMUtility", "TuistSupport", "TuistGenerator", "TuistCache", "TuistAutomation", "ProjectDescription", "Signals", "RxSwift", "RxBlocking", "Checksum", "TuistLoader", "TuistInsights", "TuistScaffold", "TuistSigning", "TuistCloud"]
|
||||
dependencies: ["XcodeProj", "SPMUtility", "ArgumentParser", "TuistSupport", "TuistGenerator", "TuistCache", "TuistAutomation", "ProjectDescription", "Signals", "RxSwift", "RxBlocking", "Checksum", "TuistLoader", "TuistInsights", "TuistScaffold", "TuistSigning", "TuistCloud"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TuistKitTests",
|
||||
|
@ -74,7 +75,7 @@ let package = Package(
|
|||
),
|
||||
.target(
|
||||
name: "TuistEnvKit",
|
||||
dependencies: ["SPMUtility", "TuistSupport", "RxSwift", "RxBlocking"]
|
||||
dependencies: ["ArgumentParser", "SPMUtility", "TuistSupport", "RxSwift", "RxBlocking"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TuistEnvKitTests",
|
||||
|
|
|
@ -1,87 +1,14 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
enum BundleCommandError: FatalError, Equatable {
|
||||
case missingVersionFile(AbsolutePath)
|
||||
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .missingVersionFile:
|
||||
return .abort
|
||||
}
|
||||
struct BundleCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "bundle",
|
||||
abstract: "Bundles the version specified in the .tuist-version file into the .tuist-bin directory")
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .missingVersionFile(path):
|
||||
return "Couldn't find a .tuist-version file in the directory \(path.pathString)"
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: BundleCommandError, rhs: BundleCommandError) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.missingVersionFile(lhsPath), .missingVersionFile(rhsPath)):
|
||||
return lhsPath == rhsPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class BundleCommand: Command {
|
||||
// MARK: - Command
|
||||
|
||||
static var command: String = "bundle"
|
||||
static var overview: String = "Bundles the version specified in the .tuist-version file into the .tuist-bin directory"
|
||||
|
||||
// MARK: - Attributes
|
||||
|
||||
private let versionsController: VersionsControlling
|
||||
private let installer: Installing
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser,
|
||||
versionsController: VersionsController(),
|
||||
installer: Installer())
|
||||
}
|
||||
|
||||
init(parser: ArgumentParser,
|
||||
versionsController: VersionsControlling,
|
||||
installer: Installing) {
|
||||
_ = parser.add(subparser: BundleCommand.command, overview: BundleCommand.overview)
|
||||
self.versionsController = versionsController
|
||||
self.installer = installer
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
func run(with _: ArgumentParser.Result) throws {
|
||||
let versionFilePath = FileHandler.shared.currentPath.appending(component: Constants.versionFileName)
|
||||
let binFolderPath = FileHandler.shared.currentPath.appending(component: Constants.binFolderName)
|
||||
|
||||
if !FileHandler.shared.exists(versionFilePath) {
|
||||
throw BundleCommandError.missingVersionFile(FileHandler.shared.currentPath)
|
||||
}
|
||||
|
||||
let version = try String(contentsOf: versionFilePath.url)
|
||||
logger.notice("Bundling the version \(version) in the directory \(binFolderPath.pathString)", metadata: .section)
|
||||
|
||||
let versionPath = versionsController.path(version: version)
|
||||
|
||||
// Installing
|
||||
if !FileHandler.shared.exists(versionPath) {
|
||||
logger.notice("Version \(version) not available locally. Installing...")
|
||||
try installer.install(version: version, force: false)
|
||||
}
|
||||
|
||||
// Copying
|
||||
if FileHandler.shared.exists(binFolderPath) {
|
||||
try FileHandler.shared.delete(binFolderPath)
|
||||
}
|
||||
try FileHandler.shared.copy(from: versionPath, to: binFolderPath)
|
||||
|
||||
logger.notice("tuist bundled successfully at \(binFolderPath.pathString)", metadata: .success)
|
||||
func run() throws {
|
||||
try BundleService().run()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
public final class CommandRegistry {
|
||||
// MARK: - Attributes
|
||||
|
||||
let parser: ArgumentParser
|
||||
var commands: [Command] = []
|
||||
private let errorHandler: ErrorHandling
|
||||
private let processArguments: () -> [String]
|
||||
private let commandRunner: CommandRunning
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public convenience init() {
|
||||
self.init(processArguments: CommandRegistry.processArguments,
|
||||
commands: [
|
||||
LocalCommand.self,
|
||||
BundleCommand.self,
|
||||
UpdateCommand.self,
|
||||
InstallCommand.self,
|
||||
UninstallCommand.self,
|
||||
VersionCommand.self,
|
||||
])
|
||||
}
|
||||
|
||||
init(processArguments: @escaping () -> [String],
|
||||
errorHandler: ErrorHandling = ErrorHandler(),
|
||||
commandRunner: CommandRunning = CommandRunner(),
|
||||
commands: [Command.Type] = []) {
|
||||
parser = ArgumentParser(commandName: "tuist",
|
||||
usage: "<command> <options>",
|
||||
overview: "Manage the environment tuist versions.")
|
||||
self.processArguments = processArguments
|
||||
self.errorHandler = errorHandler
|
||||
self.commandRunner = commandRunner
|
||||
commands.forEach(register)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
public func run() {
|
||||
do {
|
||||
if processArguments().dropFirst().first == "--help-env" {
|
||||
parser.printUsage(on: stdoutStream)
|
||||
} else if let parsedArguments = try parse() {
|
||||
try process(arguments: parsedArguments)
|
||||
} else {
|
||||
try commandRunner.run()
|
||||
}
|
||||
} catch let error as FatalError {
|
||||
errorHandler.fatal(error: error)
|
||||
} catch {
|
||||
errorHandler.fatal(error: UnhandledError(error: error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
private func parse() throws -> ArgumentParser.Result? {
|
||||
let arguments = Array(processArguments().dropFirst())
|
||||
guard let firstArgument = arguments.first else { return nil }
|
||||
if commands.map({ type(of: $0).command }).contains(firstArgument) {
|
||||
return try parser.parse(arguments)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func register(command: Command.Type) {
|
||||
commands.append(command.init(parser: parser))
|
||||
}
|
||||
|
||||
private func process(arguments: ArgumentParser.Result) throws {
|
||||
let subparser = arguments.subparser(parser)!
|
||||
let command = commands.first(where: { type(of: $0).command == subparser })!
|
||||
try command.run(with: arguments)
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static func processArguments() -> [String] {
|
||||
CommandRunner.arguments()
|
||||
}
|
||||
}
|
|
@ -1,68 +1,26 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
/// Command that installs new versions of Tuist in the system.
|
||||
final class InstallCommand: Command {
|
||||
// MARK: - Command
|
||||
|
||||
/// Command name.
|
||||
static var command: String = "install"
|
||||
|
||||
/// Command description.
|
||||
static var overview: String = "Installs a version of tuist"
|
||||
|
||||
// MARK: - Attributes
|
||||
|
||||
/// Controller to manage system versions.
|
||||
private let versionsController: VersionsControlling
|
||||
|
||||
/// Installer instance to run the installation.
|
||||
private let installer: Installing
|
||||
|
||||
/// Version argument to specify the version that will be installed.
|
||||
let versionArgument: PositionalArgument<String>
|
||||
|
||||
/// Force argument (-f). When passed, it re-installs the version compiling it from the source.
|
||||
let forceArgument: OptionArgument<Bool>
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser,
|
||||
versionsController: VersionsController(),
|
||||
installer: Installer())
|
||||
struct InstallCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "install",
|
||||
abstract: "Installs a version of tuist")
|
||||
}
|
||||
|
||||
init(parser: ArgumentParser,
|
||||
versionsController: VersionsControlling,
|
||||
installer: Installing) {
|
||||
let subParser = parser.add(subparser: InstallCommand.command,
|
||||
overview: InstallCommand.overview)
|
||||
self.versionsController = versionsController
|
||||
self.installer = installer
|
||||
versionArgument = subParser.add(positional: "version",
|
||||
kind: String.self,
|
||||
optional: false,
|
||||
usage: "The version of tuist to be installed")
|
||||
forceArgument = subParser.add(option: "--force",
|
||||
shortName: "-f",
|
||||
kind: Bool.self,
|
||||
usage: "Re-installs the version compiling it from the source", completion: nil)
|
||||
}
|
||||
@Argument(
|
||||
help: "The version of tuist to be installed"
|
||||
)
|
||||
var version: String
|
||||
|
||||
/// Runs the install command.
|
||||
///
|
||||
/// - Parameter result: Result obtained from parsing the CLI arguments.
|
||||
/// - Throws: An error if the installation process fails.
|
||||
func run(with result: ArgumentParser.Result) throws {
|
||||
let force = result.get(forceArgument) ?? false
|
||||
let version = result.get(versionArgument)!
|
||||
let versions = versionsController.versions().map { $0.description }
|
||||
if versions.contains(version) {
|
||||
logger.warning("Version \(version) already installed, skipping")
|
||||
return
|
||||
}
|
||||
try installer.install(version: version, force: force)
|
||||
@Flag(
|
||||
name: .shortAndLong,
|
||||
help: "Re-installs the version compiling it from the source"
|
||||
)
|
||||
var force: Bool
|
||||
|
||||
func run() throws {
|
||||
try InstallService().run(version: version,
|
||||
force: force)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +1,21 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
class LocalCommand: Command {
|
||||
// MARK: - Command
|
||||
|
||||
static var command: String = "local"
|
||||
// swiftlint:disable:next line_length
|
||||
static var overview: String = "Creates a .tuist-version file to pin the tuist version that should be used in the current directory. If the version is not specified, it prints the local versions"
|
||||
|
||||
// MARK: - Attributes
|
||||
|
||||
let versionArgument: PositionalArgument<String>
|
||||
let versionController: VersionsControlling
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
required convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser,
|
||||
versionController: VersionsController())
|
||||
struct LocalCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "local",
|
||||
// swiftlint:disable:next line_length
|
||||
abstract: "Creates a .tuist-version file to pin the tuist version that should be used in the current directory. If the version is not specified, it prints the local versions")
|
||||
}
|
||||
|
||||
init(parser: ArgumentParser,
|
||||
versionController: VersionsControlling) {
|
||||
let subParser = parser.add(subparser: LocalCommand.command,
|
||||
overview: LocalCommand.overview)
|
||||
versionArgument = subParser.add(positional: "version",
|
||||
kind: String.self,
|
||||
optional: true,
|
||||
usage: "The version that you would like to pin your current directory to")
|
||||
self.versionController = versionController
|
||||
}
|
||||
@Argument(
|
||||
help: "The version that you would like to pin your current directory to"
|
||||
)
|
||||
var version: String?
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
func run(with result: ArgumentParser.Result) throws {
|
||||
if let version = result.get(versionArgument) {
|
||||
try createVersionFile(version: version)
|
||||
} else {
|
||||
try printLocalVersions()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
private func printLocalVersions() throws {
|
||||
logger.notice("The following versions are available in the local environment:", metadata: .section)
|
||||
let versions = versionController.semverVersions()
|
||||
let output = versions.sorted().reversed().map { "- \($0)" }.joined(separator: "\n")
|
||||
logger.notice("\(output)")
|
||||
}
|
||||
|
||||
private func createVersionFile(version: String) throws {
|
||||
let currentPath = FileHandler.shared.currentPath
|
||||
logger.notice("Generating \(Constants.versionFileName) file with version \(version)", metadata: .section)
|
||||
let tuistVersionPath = currentPath.appending(component: Constants.versionFileName)
|
||||
try "\(version)".write(to: URL(fileURLWithPath: tuistVersionPath.pathString),
|
||||
atomically: true,
|
||||
encoding: .utf8)
|
||||
logger.notice("File generated at path \(tuistVersionPath.pathString)", metadata: .success)
|
||||
func run() throws {
|
||||
try LocalService().run(version: version)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
public struct TuistCommand: ParsableCommand {
|
||||
public init() {}
|
||||
|
||||
public static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "tuist",
|
||||
abstract: "Manage the environment tuist versions",
|
||||
subcommands: [
|
||||
LocalCommand.self,
|
||||
BundleCommand.self,
|
||||
UpdateCommand.self,
|
||||
InstallCommand.self,
|
||||
UninstallCommand.self,
|
||||
VersionCommand.self,
|
||||
])
|
||||
}
|
||||
|
||||
public static func main(_: [String]? = nil) -> Never {
|
||||
let errorHandler = ErrorHandler()
|
||||
do {
|
||||
let processedArguments = processArguments()
|
||||
if processedArguments.dropFirst().first == "--help-env" {
|
||||
throw CleanExit.helpRequest(self)
|
||||
} else if let parsedArguments = try parse() {
|
||||
try parseAsRoot(parsedArguments).run()
|
||||
} else {
|
||||
try CommandRunner().run()
|
||||
}
|
||||
exit()
|
||||
} catch let error as CleanExit {
|
||||
_exit(exitCode(for: error).rawValue)
|
||||
} catch let error as FatalError {
|
||||
errorHandler.fatal(error: error)
|
||||
_exit(exitCode(for: error).rawValue)
|
||||
} catch {
|
||||
errorHandler.fatal(error: UnhandledError(error: error))
|
||||
_exit(exitCode(for: error).rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private static func parse() throws -> [String]? {
|
||||
let arguments = Array(processArguments().dropFirst())
|
||||
guard let firstArgument = arguments.first else { return nil }
|
||||
let containsCommand = configuration.subcommands.map { $0.configuration.commandName }.contains(firstArgument)
|
||||
if containsCommand {
|
||||
return arguments
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static func processArguments() -> [String] {
|
||||
CommandRunner.arguments()
|
||||
}
|
||||
}
|
|
@ -1,48 +1,19 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
final class UninstallCommand: Command {
|
||||
// MARK: - Command
|
||||
|
||||
static var command: String = "uninstall"
|
||||
static var overview: String = "Uninstalls a version of tuist"
|
||||
|
||||
// MARK: - Attributes
|
||||
|
||||
private let versionsController: VersionsControlling
|
||||
private let installer: Installing
|
||||
let versionArgument: PositionalArgument<String>
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser,
|
||||
versionsController: VersionsController(),
|
||||
installer: Installer())
|
||||
struct UninstallCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "uninstall",
|
||||
abstract: "Uninstalls a version of tuist")
|
||||
}
|
||||
|
||||
init(parser: ArgumentParser,
|
||||
versionsController: VersionsControlling,
|
||||
installer: Installing) {
|
||||
let subParser = parser.add(subparser: UninstallCommand.command,
|
||||
overview: UninstallCommand.overview)
|
||||
self.versionsController = versionsController
|
||||
self.installer = installer
|
||||
versionArgument = subParser.add(positional: "version",
|
||||
kind: String.self,
|
||||
optional: false,
|
||||
usage: "The version of tuist to be uninstalled")
|
||||
}
|
||||
@Argument(
|
||||
help: "The version of tuist to be uninstalled"
|
||||
)
|
||||
var version: String
|
||||
|
||||
func run(with result: ArgumentParser.Result) throws {
|
||||
let version = result.get(versionArgument)!
|
||||
let versions = versionsController.versions().map { $0.description }
|
||||
if versions.contains(version) {
|
||||
try versionsController.uninstall(version: version)
|
||||
logger.notice("Version \(version) uninstalled", metadata: .success)
|
||||
} else {
|
||||
logger.warning("Version \(version) cannot be uninstalled because it's not installed")
|
||||
}
|
||||
func run() throws {
|
||||
try UninstallService().run(version: version)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,57 +1,22 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
/// Command that updates the version of Tuist in the environment.
|
||||
final class UpdateCommand: Command {
|
||||
// MARK: - Command
|
||||
|
||||
/// Name of the command.
|
||||
static var command: String = "update"
|
||||
|
||||
/// Description of the command.
|
||||
static var overview: String = "Installs the latest version if it's not already installed"
|
||||
|
||||
// MARK: - Attributes
|
||||
|
||||
/// Updater instance that runs the update.
|
||||
private let updater: Updating
|
||||
struct UpdateCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "update",
|
||||
abstract: "Installs the latest version if it's not already installed")
|
||||
}
|
||||
|
||||
/// Force argument (-f). When passed, it re-installs the latest version compiling it from the source.
|
||||
let forceArgument: OptionArgument<Bool>
|
||||
@Flag(
|
||||
name: .shortAndLong,
|
||||
help: "Re-installs the latest version compiling it from the source"
|
||||
)
|
||||
var force: Bool
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
/// Initializes the update command.
|
||||
///
|
||||
/// - Parameter parser: Argument parser where the command should be registered.
|
||||
convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser,
|
||||
updater: Updater())
|
||||
}
|
||||
|
||||
/// Initializes the update command.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - parser: Argument parser where the command should be registered.
|
||||
/// - updater: Updater instance that runs the update.
|
||||
init(parser: ArgumentParser,
|
||||
updater: Updating) {
|
||||
let subParser = parser.add(subparser: UpdateCommand.command, overview: UpdateCommand.overview)
|
||||
self.updater = updater
|
||||
forceArgument = subParser.add(option: "--force",
|
||||
shortName: "-f",
|
||||
kind: Bool.self,
|
||||
usage: "Re-installs the latest version compiling it from the source", completion: nil)
|
||||
}
|
||||
|
||||
/// Runs the update command.
|
||||
///
|
||||
/// - Parameter result: Result obtained from parsing the CLI arguments.
|
||||
/// - Throws: An error if the update process fails.
|
||||
func run(with result: ArgumentParser.Result) throws {
|
||||
let force = result.get(forceArgument) ?? false
|
||||
logger.notice("Checking for updates...", metadata: .section)
|
||||
try updater.update(force: force)
|
||||
func run() throws {
|
||||
try UpdateService().run(force: force)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,11 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
class VersionCommand: NSObject, Command {
|
||||
// MARK: - Command
|
||||
|
||||
static let command = "envversion"
|
||||
static let overview = "Outputs the current version of tuist env."
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
required init(parser: ArgumentParser) {
|
||||
parser.add(subparser: VersionCommand.command, overview: VersionCommand.overview)
|
||||
}
|
||||
|
||||
// MARK: - Command
|
||||
|
||||
func run(with _: ArgumentParser.Result) {
|
||||
logger.notice("\(Constants.version)")
|
||||
struct VersionCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "envversion",
|
||||
abstract: "Outputs the current version of tuist env.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
enum BundleServiceError: FatalError, Equatable {
|
||||
case missingVersionFile(AbsolutePath)
|
||||
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .missingVersionFile:
|
||||
return .abort
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .missingVersionFile(path):
|
||||
return "Couldn't find a .tuist-version file in the directory \(path.pathString)"
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: BundleServiceError, rhs: BundleServiceError) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.missingVersionFile(lhsPath), .missingVersionFile(rhsPath)):
|
||||
return lhsPath == rhsPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class BundleService {
|
||||
private let versionsController: VersionsControlling
|
||||
private let installer: Installing
|
||||
|
||||
init(versionsController: VersionsControlling = VersionsController(),
|
||||
installer: Installing = Installer()) {
|
||||
self.versionsController = versionsController
|
||||
self.installer = installer
|
||||
}
|
||||
|
||||
func run() throws {
|
||||
let versionFilePath = FileHandler.shared.currentPath.appending(component: Constants.versionFileName)
|
||||
let binFolderPath = FileHandler.shared.currentPath.appending(component: Constants.binFolderName)
|
||||
|
||||
if !FileHandler.shared.exists(versionFilePath) {
|
||||
throw BundleServiceError.missingVersionFile(FileHandler.shared.currentPath)
|
||||
}
|
||||
|
||||
let version = try String(contentsOf: versionFilePath.url)
|
||||
logger.notice("Bundling the version \(version) in the directory \(binFolderPath.pathString)", metadata: .section)
|
||||
|
||||
let versionPath = versionsController.path(version: version)
|
||||
|
||||
// Installing
|
||||
if !FileHandler.shared.exists(versionPath) {
|
||||
logger.notice("Version \(version) not available locally. Installing...")
|
||||
try installer.install(version: version, force: false)
|
||||
}
|
||||
|
||||
// Copying
|
||||
if FileHandler.shared.exists(binFolderPath) {
|
||||
try FileHandler.shared.delete(binFolderPath)
|
||||
}
|
||||
try FileHandler.shared.copy(from: versionPath, to: binFolderPath)
|
||||
|
||||
logger.notice("tuist bundled successfully at \(binFolderPath.pathString)", metadata: .success)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
final class InstallService {
|
||||
/// Controller to manage system versions.
|
||||
private let versionsController: VersionsControlling
|
||||
|
||||
/// Installer instance to run the installation.
|
||||
private let installer: Installing
|
||||
|
||||
init(versionsController: VersionsControlling = VersionsController(),
|
||||
installer: Installing = Installer()) {
|
||||
self.versionsController = versionsController
|
||||
self.installer = installer
|
||||
}
|
||||
|
||||
func run(version: String, force: Bool) throws {
|
||||
let versions = versionsController.versions().map { $0.description }
|
||||
if versions.contains(version) {
|
||||
logger.warning("Version \(version) already installed, skipping")
|
||||
return
|
||||
}
|
||||
try installer.install(version: version, force: force)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
final class LocalService {
|
||||
private let versionController: VersionsControlling
|
||||
|
||||
init(versionController: VersionsControlling = VersionsController()) {
|
||||
self.versionController = versionController
|
||||
}
|
||||
|
||||
func run(version: String?) throws {
|
||||
if let version = version {
|
||||
try createVersionFile(version: version)
|
||||
} else {
|
||||
try printLocalVersions()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func printLocalVersions() throws {
|
||||
logger.notice("The following versions are available in the local environment:", metadata: .section)
|
||||
let versions = versionController.semverVersions()
|
||||
let output = versions.sorted().reversed().map { "- \($0)" }.joined(separator: "\n")
|
||||
logger.notice("\(output)")
|
||||
}
|
||||
|
||||
private func createVersionFile(version: String) throws {
|
||||
let currentPath = FileHandler.shared.currentPath
|
||||
logger.notice("Generating \(Constants.versionFileName) file with version \(version)", metadata: .section)
|
||||
let tuistVersionPath = currentPath.appending(component: Constants.versionFileName)
|
||||
try "\(version)".write(to: URL(fileURLWithPath: tuistVersionPath.pathString),
|
||||
atomically: true,
|
||||
encoding: .utf8)
|
||||
logger.notice("File generated at path \(tuistVersionPath.pathString)", metadata: .success)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
final class UninstallService {
|
||||
/// Controller to manage system versions.
|
||||
private let versionsController: VersionsControlling
|
||||
|
||||
/// Installer instance to run the installation.
|
||||
private let installer: Installing
|
||||
|
||||
init(versionsController: VersionsControlling = VersionsController(),
|
||||
installer: Installing = Installer()) {
|
||||
self.versionsController = versionsController
|
||||
self.installer = installer
|
||||
}
|
||||
|
||||
func run(version: String) throws {
|
||||
let versions = versionsController.versions().map { $0.description }
|
||||
if versions.contains(version) {
|
||||
try versionsController.uninstall(version: version)
|
||||
logger.notice("Version \(version) uninstalled", metadata: .success)
|
||||
} else {
|
||||
logger.warning("Version \(version) cannot be uninstalled because it's not installed")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
final class UpdateService {
|
||||
/// Updater instance that runs the update.
|
||||
private let updater: Updating
|
||||
|
||||
init(updater: Updating = Updater()) {
|
||||
self.updater = updater
|
||||
}
|
||||
|
||||
func run(force: Bool) throws {
|
||||
logger.notice("Checking for updates...", metadata: .section)
|
||||
try updater.update(force: force)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
final class VersionService {
|
||||
func run() throws {
|
||||
logger.notice("\(Constants.version)")
|
||||
}
|
||||
}
|
|
@ -1,32 +1,15 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
enum BuildCommandError: FatalError {
|
||||
// Error description
|
||||
var description: String {
|
||||
""
|
||||
}
|
||||
|
||||
// Error type
|
||||
var type: ErrorType { .abort }
|
||||
}
|
||||
|
||||
/// Command that builds a target from the project in the current directory.
|
||||
class BuildCommand: NSObject, RawCommand {
|
||||
/// Command name.
|
||||
static var command: String = "build"
|
||||
|
||||
/// Command description.
|
||||
static var overview: String = "Builds a project target."
|
||||
|
||||
/// Default constructor.
|
||||
required override init() {
|
||||
super.init()
|
||||
struct BuildCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "build",
|
||||
abstract: "Builds a project target")
|
||||
}
|
||||
|
||||
func run(arguments _: [String]) throws {
|
||||
logger.notice("Command not available yet")
|
||||
func run() throws {
|
||||
try BuildService().run()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,61 +1,22 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
/// Command to cache frameworks as .xcframeworks and speed up your and others' build times.
|
||||
class CacheCommand: NSObject, Command {
|
||||
// MARK: - Attributes
|
||||
|
||||
/// Name of the command.
|
||||
static let command = "cache"
|
||||
|
||||
/// Description of the command.
|
||||
static let overview = "Cache frameworks as .xcframeworks to speed up build times in generated projects"
|
||||
|
||||
/// Path to the project directory.
|
||||
let pathArgument: OptionArgument<String>
|
||||
|
||||
/// Cache controller.
|
||||
let cacheController: CacheControlling
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
/// Initializes the command with the CLI parser.
|
||||
///
|
||||
/// - Parameter parser: CLI parser where the command should register itself.
|
||||
public required convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser, cacheController: CacheController())
|
||||
struct CacheCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "cache",
|
||||
abstract: "Cache frameworks as .xcframeworks to speed up build times in generated projects")
|
||||
}
|
||||
|
||||
public init(parser: ArgumentParser,
|
||||
cacheController: CacheControlling) {
|
||||
let subParser = parser.add(subparser: CacheCommand.command, overview: CacheCommand.overview)
|
||||
self.cacheController = cacheController
|
||||
pathArgument = subParser.add(option: "--path",
|
||||
shortName: "-p",
|
||||
kind: String.self,
|
||||
usage: "The path to the directory that contains the project whose frameworks will be cached.",
|
||||
completion: .filename)
|
||||
}
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path to the directory that contains the project whose frameworks will be cached"
|
||||
)
|
||||
var path: String?
|
||||
|
||||
/// Runs the command using the result from parsing the command line arguments.
|
||||
///
|
||||
/// - Throws: An error if the the configuration of the environment fails.
|
||||
func run(with result: ArgumentParser.Result) throws {
|
||||
let path = self.path(arguments: result)
|
||||
try cacheController.cache(path: path)
|
||||
}
|
||||
|
||||
/// Parses the arguments and returns the path to the directory where
|
||||
/// the up command should be ran.
|
||||
///
|
||||
/// - Parameter arguments: Result from parsing the command line arguments.
|
||||
/// - Returns: Path to be used for the up command.
|
||||
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
|
||||
guard let path = arguments.get(pathArgument) else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
func run() throws {
|
||||
try CacheService().run(path: path)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,14 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistSigning
|
||||
import TuistSupport
|
||||
|
||||
class CloudAuthCommand: NSObject, Command {
|
||||
// MARK: - Attributes
|
||||
|
||||
static let command = "auth"
|
||||
static let overview = "Authenticates the user on the server with the URL defined in the Config.swift file."
|
||||
private let cloudAuthService = CloudAuthService()
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public required init(parser: ArgumentParser) {
|
||||
_ = parser.add(subparser: CloudAuthCommand.command, overview: CloudAuthCommand.overview)
|
||||
struct CloudAuthCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "auth",
|
||||
abstract: "Authenticates the user on the server with the URL defined in the Config.swift file")
|
||||
}
|
||||
|
||||
func run(with _: ArgumentParser.Result) throws {
|
||||
try cloudAuthService.authenticate()
|
||||
func run() throws {
|
||||
try CloudAuthService().authenticate()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,14 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistSigning
|
||||
import TuistSupport
|
||||
|
||||
class CloudLogoutCommand: NSObject, Command {
|
||||
// MARK: - Attributes
|
||||
|
||||
static let command = "logout"
|
||||
static let overview = "Removes any existing session to authenticate on the server with the URL defined in the Config.swift file."
|
||||
private let service = CloudLogoutService()
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public required init(parser: ArgumentParser) {
|
||||
_ = parser.add(subparser: CloudLogoutCommand.command, overview: CloudLogoutCommand.overview)
|
||||
struct CloudLogoutCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "logout",
|
||||
abstract: "Removes any existing session to authenticate on the server with the URL defined in the Config.swift file")
|
||||
}
|
||||
|
||||
func run(with _: ArgumentParser.Result) throws {
|
||||
try service.logout()
|
||||
func run() throws {
|
||||
try CloudLogoutService().logout()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,14 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistSigning
|
||||
import TuistSupport
|
||||
|
||||
class CloudSessionCommand: NSObject, Command {
|
||||
// MARK: - Attributes
|
||||
|
||||
static let command = "session"
|
||||
static let overview = "Prints any existing session to authenticate on the server with the URL defined in the Config.swift file."
|
||||
private let service = CloudSessionService()
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public required init(parser: ArgumentParser) {
|
||||
_ = parser.add(subparser: CloudSessionCommand.command, overview: CloudSessionCommand.overview)
|
||||
struct CloudSessionCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "session",
|
||||
abstract: "Prints any existing session to authenticate on the server with the URL defined in the Config.swift file")
|
||||
}
|
||||
|
||||
func run(with _: ArgumentParser.Result) throws {
|
||||
try service.printSession()
|
||||
func run() throws {
|
||||
try CloudSessionService().printSession()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,14 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistSigning
|
||||
import TuistSupport
|
||||
|
||||
class CloudCommand: NSObject, Command {
|
||||
// MARK: - Attributes
|
||||
|
||||
static let command = "cloud"
|
||||
static let overview = "A set of commands for cloud-related operations."
|
||||
let subcommands: [Command]
|
||||
|
||||
private let argumentParser: ArgumentParser
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public required init(parser: ArgumentParser) {
|
||||
_ = parser.add(subparser: CloudCommand.command, overview: CloudCommand.overview)
|
||||
let argumentParser = ArgumentParser(commandName: Self.command, usage: "tuist cloud <command> <options>", overview: Self.overview)
|
||||
let subcommands: [Command.Type] = [CloudAuthCommand.self, CloudSessionCommand.self, CloudLogoutCommand.self]
|
||||
self.subcommands = subcommands.map { $0.init(parser: argumentParser) }
|
||||
self.argumentParser = argumentParser
|
||||
}
|
||||
|
||||
func parse(with _: ArgumentParser, arguments: [String]) throws -> (ArgumentParser.Result, ArgumentParser) {
|
||||
return (try argumentParser.parse(Array(arguments.dropFirst())), argumentParser)
|
||||
}
|
||||
|
||||
func run(with _: ArgumentParser.Result) throws {
|
||||
argumentParser.printUsage(on: stdoutStream)
|
||||
struct CloudCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "cloud",
|
||||
abstract: "A set of commands for cloud-related operations", subcommands: [
|
||||
CloudAuthCommand.self,
|
||||
CloudSessionCommand.self,
|
||||
CloudLogoutCommand.self,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
public final class CommandRegistry {
|
||||
// MARK: - Attributes
|
||||
|
||||
let parser: ArgumentParser
|
||||
var commands: [Command] = []
|
||||
var rawCommands: [RawCommand] = []
|
||||
var hiddenCommands: [String: HiddenCommand] = [:]
|
||||
private let errorHandler: ErrorHandling
|
||||
private let processArguments: () -> [String]
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public convenience init() {
|
||||
self.init(errorHandler: ErrorHandler(),
|
||||
processArguments: CommandRegistry.processArguments)
|
||||
register(command: InitCommand.self)
|
||||
register(command: ScaffoldCommand.self)
|
||||
register(command: GenerateCommand.self)
|
||||
register(command: DumpCommand.self)
|
||||
register(command: VersionCommand.self)
|
||||
register(command: CreateIssueCommand.self)
|
||||
register(command: FocusCommand.self)
|
||||
register(command: UpCommand.self)
|
||||
register(command: GraphCommand.self)
|
||||
register(command: EditCommand.self)
|
||||
register(command: CacheCommand.self)
|
||||
register(command: LintCommand.self)
|
||||
register(command: SigningCommand.self)
|
||||
register(command: CloudCommand.self)
|
||||
register(rawCommand: BuildCommand.self)
|
||||
}
|
||||
|
||||
init(errorHandler: ErrorHandling,
|
||||
processArguments: @escaping () -> [String]) {
|
||||
self.errorHandler = errorHandler
|
||||
parser = ArgumentParser(commandName: "tuist",
|
||||
usage: "<command> <options>",
|
||||
overview: "Generate, build and test your Xcode projects.")
|
||||
self.processArguments = processArguments
|
||||
}
|
||||
|
||||
public static func processArguments() -> [String] {
|
||||
Array(ProcessInfo.processInfo.arguments).filter { $0 != "--verbose" }
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
func register(command: Command.Type) {
|
||||
commands.append(command.init(parser: parser))
|
||||
}
|
||||
|
||||
func register(hiddenCommand command: HiddenCommand.Type) {
|
||||
hiddenCommands[command.command] = command.init()
|
||||
}
|
||||
|
||||
func register(rawCommand command: RawCommand.Type) {
|
||||
rawCommands.append(command.init())
|
||||
parser.add(subparser: command.command, overview: command.overview)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
public func run() {
|
||||
do {
|
||||
// Hidden command
|
||||
if let hiddenCommand = hiddenCommand() {
|
||||
try hiddenCommand.run(arguments: argumentsDroppingCommand())
|
||||
|
||||
// Raw command
|
||||
} else if let commandName = commandName(),
|
||||
let command = rawCommands.first(where: { type(of: $0).command == commandName }) {
|
||||
try command.run(arguments: argumentsDroppingCommand())
|
||||
|
||||
// Normal command
|
||||
} else {
|
||||
guard let (parsedArguments, parser) = try parse() else {
|
||||
self.parser.printUsage(on: stdoutStream)
|
||||
return
|
||||
}
|
||||
try process(arguments: parsedArguments, parser: parser)
|
||||
}
|
||||
} catch let error as FatalError {
|
||||
errorHandler.fatal(error: error)
|
||||
} catch {
|
||||
errorHandler.fatal(error: UnhandledError(error: error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
func argumentsDroppingCommand() -> [String] {
|
||||
Array(processArguments().dropFirst(2))
|
||||
}
|
||||
|
||||
/// Returns the command name.
|
||||
///
|
||||
/// - Returns: Command name.
|
||||
func commandName() -> String? {
|
||||
let arguments = processArguments()
|
||||
if arguments.count < 2 { return nil }
|
||||
return arguments[1]
|
||||
}
|
||||
|
||||
private func parse() throws -> (ArgumentParser.Result, ArgumentParser)? {
|
||||
let arguments = Array(processArguments().dropFirst())
|
||||
guard let argumentName = arguments.first else { return nil }
|
||||
let subparser = try parser.parse([argumentName]).subparser(parser)
|
||||
if let command = commands.first(where: { type(of: $0).command == subparser }) {
|
||||
return try command.parse(with: parser, arguments: arguments)
|
||||
}
|
||||
return (try parser.parse(arguments), parser)
|
||||
}
|
||||
|
||||
private func hiddenCommand() -> HiddenCommand? {
|
||||
let arguments = Array(processArguments().dropFirst())
|
||||
guard let commandName = arguments.first else { return nil }
|
||||
return hiddenCommands[commandName]
|
||||
}
|
||||
|
||||
private func process(arguments: ArgumentParser.Result, parser: ArgumentParser) throws {
|
||||
guard let subparser = arguments.subparser(parser) else {
|
||||
parser.printUsage(on: stdoutStream)
|
||||
return
|
||||
}
|
||||
let allCommands = commands + commands.flatMap { $0.subcommands }
|
||||
if let command = allCommands.first(where: { type(of: $0).command == subparser }) {
|
||||
try command.run(with: arguments)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +1,14 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
class CreateIssueCommand: NSObject, Command {
|
||||
static let createIssueUrl: String = "https://github.com/tuist/tuist/issues/new"
|
||||
|
||||
// MARK: - Command
|
||||
|
||||
static let command = "create-issue"
|
||||
static let overview = "Opens the GitHub page to create a new issue."
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
required init(parser: ArgumentParser) {
|
||||
parser.add(subparser: CreateIssueCommand.command, overview: CreateIssueCommand.overview)
|
||||
struct CreateIssueCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "create-issue",
|
||||
abstract: "Opens the GitHub page to create a new issue")
|
||||
}
|
||||
|
||||
// MARK: - Command
|
||||
|
||||
func run(with _: ArgumentParser.Result) throws {
|
||||
try System.shared.run("/usr/bin/open", CreateIssueCommand.createIssueUrl)
|
||||
func run() throws {
|
||||
try CreateIssueService().run()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +1,24 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
class DumpCommand: NSObject, Command {
|
||||
// MARK: - Command
|
||||
|
||||
static let command = "dump"
|
||||
static let overview = "Outputs the project manifest as a JSON"
|
||||
struct DumpCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "dump",
|
||||
abstract: "Outputs the project manifest as a JSON")
|
||||
}
|
||||
|
||||
// MARK: - Attributes
|
||||
|
||||
private let manifestLoader: ManifestLoading
|
||||
let pathArgument: OptionArgument<String>
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path to the folder where the project manifest is"
|
||||
)
|
||||
var path: String?
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public required convenience init(parser: ArgumentParser) {
|
||||
self.init(manifestLoader: ManifestLoader(),
|
||||
parser: parser)
|
||||
}
|
||||
|
||||
init(manifestLoader: ManifestLoading,
|
||||
parser: ArgumentParser) {
|
||||
let subParser = parser.add(subparser: DumpCommand.command, overview: DumpCommand.overview)
|
||||
self.manifestLoader = manifestLoader
|
||||
pathArgument = subParser.add(option: "--path",
|
||||
shortName: "-p",
|
||||
kind: String.self,
|
||||
usage: "The path to the folder where the project manifest is",
|
||||
completion: .filename)
|
||||
}
|
||||
|
||||
// MARK: - Command
|
||||
|
||||
func run(with arguments: ArgumentParser.Result) throws {
|
||||
var path: AbsolutePath!
|
||||
if let argumentPath = arguments.get(pathArgument) {
|
||||
path = AbsolutePath(argumentPath, relativeTo: AbsolutePath.current)
|
||||
} else {
|
||||
path = AbsolutePath.current
|
||||
}
|
||||
let project = try manifestLoader.loadProject(at: path)
|
||||
let json: JSON = try project.toJSON()
|
||||
logger.notice("\(json.toString(prettyPrint: true))")
|
||||
func run() throws {
|
||||
try DumpService().run(path: path)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,88 +1,30 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import Signals
|
||||
import SPMUtility
|
||||
import TuistGenerator
|
||||
import TuistSupport
|
||||
|
||||
class EditCommand: NSObject, Command {
|
||||
// MARK: - Static
|
||||
|
||||
static let command = "edit"
|
||||
static let overview = "Generates a temporary project to edit the project in the current directory"
|
||||
|
||||
// MARK: - Attributes
|
||||
|
||||
private let projectEditor: ProjectEditing
|
||||
private let opener: Opening
|
||||
private let pathArgument: OptionArgument<String>
|
||||
private let permanentArgument: OptionArgument<Bool>
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
required convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser, projectEditor: ProjectEditor(), opener: Opener())
|
||||
struct EditCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "edit",
|
||||
abstract: "Generates a temporary project to edit the project in the current directory")
|
||||
}
|
||||
|
||||
init(parser: ArgumentParser, projectEditor: ProjectEditing, opener: Opening) {
|
||||
let subparser = parser.add(subparser: EditCommand.command, overview: EditCommand.overview)
|
||||
pathArgument = subparser.add(option: "--path",
|
||||
shortName: "-p",
|
||||
kind: String.self,
|
||||
usage: "The path to the directory whose project will be edited.",
|
||||
completion: .filename)
|
||||
permanentArgument = subparser.add(option: "--permanent",
|
||||
shortName: "-P",
|
||||
kind: Bool.self,
|
||||
usage: "It creates the project in the current directory or the one indicated by -p and doesn't block the process.") // swiftlint:disable:this line_length
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path to the directory whose project will be edited"
|
||||
)
|
||||
var path: String?
|
||||
|
||||
self.projectEditor = projectEditor
|
||||
self.opener = opener
|
||||
}
|
||||
@Flag(
|
||||
name: .shortAndLong,
|
||||
help: "It creates the project in the current directory or the one indicated by -p and doesn't block the process"
|
||||
)
|
||||
var permanent: Bool
|
||||
|
||||
func run(with arguments: ArgumentParser.Result) throws {
|
||||
let path = self.path(arguments: arguments)
|
||||
let permanent = self.permanent(arguments: arguments)
|
||||
let generationDirectory = permanent ? path : EditCommand.temporaryDirectory.path
|
||||
let xcodeprojPath = try projectEditor.edit(at: path, in: generationDirectory)
|
||||
|
||||
if !permanent {
|
||||
Signals.trap(signals: [.int, .abrt]) { _ in
|
||||
// swiftlint:disable:next force_try
|
||||
try! FileHandler.shared.delete(EditCommand.temporaryDirectory.path)
|
||||
exit(0)
|
||||
}
|
||||
logger.pretty("Opening Xcode to edit the project. Press \(.keystroke("CTRL + C")) once you are done editing")
|
||||
try opener.open(path: xcodeprojPath)
|
||||
} else {
|
||||
logger.notice("Xcode project generated at \(xcodeprojPath.pathString)", metadata: .success)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
fileprivate static var _temporaryDirectory: TemporaryDirectory?
|
||||
fileprivate static var temporaryDirectory: TemporaryDirectory {
|
||||
// swiftlint:disable:next identifier_name
|
||||
if let _temporaryDirectory = _temporaryDirectory { return _temporaryDirectory }
|
||||
// swiftlint:disable:next force_try
|
||||
_temporaryDirectory = try! TemporaryDirectory(removeTreeOnDeinit: true)
|
||||
return _temporaryDirectory!
|
||||
}
|
||||
|
||||
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
|
||||
if let path = arguments.get(pathArgument) {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
}
|
||||
|
||||
private func permanent(arguments: ArgumentParser.Result) -> Bool {
|
||||
if let permanent = arguments.get(permanentArgument) {
|
||||
return permanent
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
func run() throws {
|
||||
try EditService().run(path: path,
|
||||
permanent: permanent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import RxBlocking
|
||||
import RxSwift
|
||||
import SPMUtility
|
||||
import TuistCache
|
||||
import TuistCore
|
||||
import TuistGenerator
|
||||
|
@ -10,54 +10,13 @@ import TuistLoader
|
|||
import TuistSupport
|
||||
|
||||
/// The focus command generates the Xcode workspace and launches it on Xcode.
|
||||
class FocusCommand: NSObject, Command {
|
||||
// MARK: - Static
|
||||
|
||||
/// Command name that is used for the CLI.
|
||||
static let command = "focus"
|
||||
|
||||
/// Command description that is shown when using help from the CLI.
|
||||
static let overview = "Opens Xcode ready to focus on the project in the current directory."
|
||||
|
||||
// MARK: - Attributes
|
||||
|
||||
/// Generator instance to generate the project workspace.
|
||||
private let generator: ProjectGenerating
|
||||
|
||||
/// Opener instance to run open in the system.
|
||||
private let opener: Opening
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
/// Initializes the focus command with the argument parser where the command needs to register itself.
|
||||
///
|
||||
/// - Parameter parser: Argument parser that parses the CLI arguments.
|
||||
required convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser,
|
||||
generator: ProjectGenerator(graphMapperProvider: GraphMapperProvider(useCache: true)),
|
||||
opener: Opener())
|
||||
struct FocusCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "focus",
|
||||
abstract: "Opens Xcode ready to focus on the project in the current directory")
|
||||
}
|
||||
|
||||
/// Initializes the focus command with its attributes.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - parser: Argument parser that parses the CLI arguments.
|
||||
/// - generator: Generator instance to generate the project workspace.
|
||||
/// - opener: Opener instance to run open in the system.
|
||||
init(parser: ArgumentParser,
|
||||
generator: ProjectGenerating,
|
||||
opener: Opening) {
|
||||
parser.add(subparser: FocusCommand.command, overview: FocusCommand.overview)
|
||||
self.generator = generator
|
||||
self.opener = opener
|
||||
}
|
||||
|
||||
func run(with _: ArgumentParser.Result) throws {
|
||||
let path = FileHandler.shared.currentPath
|
||||
|
||||
let workspacePath = try generator.generate(path: path,
|
||||
projectOnly: false)
|
||||
|
||||
try opener.open(path: workspacePath)
|
||||
func run() throws {
|
||||
try FocusService().run()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,70 +1,28 @@
|
|||
import Basic
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistGenerator
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
class GenerateCommand: NSObject, Command {
|
||||
// MARK: - Static
|
||||
|
||||
static let command = "generate"
|
||||
static let overview = "Generates an Xcode workspace to start working on the project."
|
||||
|
||||
// MARK: - Attributes
|
||||
|
||||
private let clock: Clock
|
||||
private let generator: ProjectGenerating
|
||||
let pathArgument: OptionArgument<String>
|
||||
let projectOnlyArgument: OptionArgument<Bool>
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
required convenience init(parser: ArgumentParser) {
|
||||
let projectGenerator = ProjectGenerator()
|
||||
self.init(parser: parser,
|
||||
generator: projectGenerator,
|
||||
clock: WallClock())
|
||||
struct GenerateCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "generate",
|
||||
abstract: "Generates an Xcode workspace to start working on the project.",
|
||||
subcommands: [])
|
||||
}
|
||||
|
||||
init(parser: ArgumentParser,
|
||||
generator: ProjectGenerating,
|
||||
clock: Clock) {
|
||||
let subParser = parser.add(subparser: GenerateCommand.command, overview: GenerateCommand.overview)
|
||||
self.generator = generator
|
||||
self.clock = clock
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path where the project will be generated."
|
||||
)
|
||||
var path: String?
|
||||
|
||||
pathArgument = subParser.add(option: "--path",
|
||||
shortName: "-p",
|
||||
kind: String.self,
|
||||
usage: "The path where the project will be generated.",
|
||||
completion: .filename)
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
default: false,
|
||||
help: "Only generate the local project (without generating its dependencies)."
|
||||
)
|
||||
var projectOnly: Bool
|
||||
|
||||
projectOnlyArgument = subParser.add(option: "--project-only",
|
||||
kind: Bool.self,
|
||||
usage: "Only generate the local project (without generating its dependencies).")
|
||||
}
|
||||
|
||||
func run(with arguments: ArgumentParser.Result) throws {
|
||||
let timer = clock.startTimer()
|
||||
let path = self.path(arguments: arguments)
|
||||
let projectOnly = arguments.get(projectOnlyArgument) ?? false
|
||||
|
||||
try generator.generate(path: path, projectOnly: projectOnly)
|
||||
|
||||
let time = String(format: "%.3f", timer.stop())
|
||||
|
||||
logger.notice("Project generated.", metadata: .success)
|
||||
logger.notice("Total time taken: \(time)s")
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
|
||||
if let path = arguments.get(pathArgument) {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
func run() throws {
|
||||
try GenerateService().run(path: path,
|
||||
projectOnly: projectOnly)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +1,18 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistGenerator
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
/// Command that generates and exports a dot graph from the workspace or project in the current directory.
|
||||
class GraphCommand: NSObject, Command {
|
||||
/// Command name.
|
||||
static var command: String = "graph"
|
||||
|
||||
/// Command description.
|
||||
static var overview: String = "Generates a dot graph from the workspace or project in the current directory."
|
||||
|
||||
/// Dot graph generator.
|
||||
let dotGraphGenerator: DotGraphGenerating
|
||||
|
||||
/// Manifest loader.
|
||||
let manifestLoader: ManifestLoading
|
||||
|
||||
required convenience init(parser: ArgumentParser) {
|
||||
let manifestLoader = ManifestLoader()
|
||||
let manifestLinter = ManifestLinter()
|
||||
let modelLoader = GeneratorModelLoader(manifestLoader: manifestLoader,
|
||||
manifestLinter: manifestLinter)
|
||||
|
||||
let dotGraphGenerator = DotGraphGenerator(modelLoader: modelLoader)
|
||||
self.init(parser: parser,
|
||||
dotGraphGenerator: dotGraphGenerator,
|
||||
manifestLoader: manifestLoader)
|
||||
struct GraphCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "graph",
|
||||
abstract: "Generates a dot graph from the workspace or project in the current directory")
|
||||
}
|
||||
|
||||
init(parser: ArgumentParser,
|
||||
dotGraphGenerator: DotGraphGenerating,
|
||||
manifestLoader: ManifestLoading) {
|
||||
parser.add(subparser: GraphCommand.command, overview: GraphCommand.overview)
|
||||
self.dotGraphGenerator = dotGraphGenerator
|
||||
self.manifestLoader = manifestLoader
|
||||
}
|
||||
|
||||
func run(with _: ArgumentParser.Result) throws {
|
||||
let graph = try dotGraphGenerator.generate(at: FileHandler.shared.currentPath,
|
||||
manifestLoader: manifestLoader)
|
||||
|
||||
let path = FileHandler.shared.currentPath.appending(component: "graph.dot")
|
||||
if FileHandler.shared.exists(path) {
|
||||
logger.notice("Deleting existing graph at \(path.pathString)")
|
||||
try FileHandler.shared.delete(path)
|
||||
}
|
||||
|
||||
try FileHandler.shared.write(graph, path: path, atomically: true)
|
||||
logger.notice("Graph exported to \(path.pathString)", metadata: .success)
|
||||
func run() throws {
|
||||
try GraphService().run()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistGenerator
|
||||
import TuistLoader
|
||||
|
@ -10,263 +10,160 @@ import TuistSupport
|
|||
private typealias Platform = TuistCore.Platform
|
||||
private typealias Product = TuistCore.Product
|
||||
|
||||
enum InitCommandError: FatalError, Equatable {
|
||||
case ungettableProjectName(AbsolutePath)
|
||||
case nonEmptyDirectory(AbsolutePath)
|
||||
case templateNotFound(String)
|
||||
case templateNotProvided
|
||||
case attributeNotProvided(String)
|
||||
|
||||
var type: ErrorType {
|
||||
.abort
|
||||
struct InitCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "init",
|
||||
abstract: "Bootstraps a project")
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .templateNotFound(template):
|
||||
return "Could not find template \(template). Make sure it exists at Tuist/Templates/\(template)"
|
||||
case .templateNotProvided:
|
||||
return "You must provide template name"
|
||||
case let .ungettableProjectName(path):
|
||||
return "Couldn't infer the project name from path \(path.pathString)."
|
||||
case let .nonEmptyDirectory(path):
|
||||
return "Can't initialize a project in the non-empty directory at path \(path.pathString)."
|
||||
case let .attributeNotProvided(name):
|
||||
return "You must provide \(name) option. Add --\(name) desired_value to your command."
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The platform (ios, tvos or macos) the product will be for (Default: ios)"
|
||||
)
|
||||
var platform: String?
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path to the folder where the project will be generated (Default: Current directory)"
|
||||
)
|
||||
var path: String?
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The name of the project. If it's not passed (Default: Name of the directory)"
|
||||
)
|
||||
var name: String?
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The name of the template to use (you can list available templates with tuist scaffold list)"
|
||||
)
|
||||
var template: String?
|
||||
|
||||
var requiredTemplateOptions: [String: String] = [:]
|
||||
var optionalTemplateOptions: [String: String?] = [:]
|
||||
|
||||
init() {}
|
||||
|
||||
// Custom decoding to decode dynamic options
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
platform = try container.decodeIfPresent(Option<String>.self, forKey: .platform)?.wrappedValue
|
||||
name = try container.decodeIfPresent(Option<String>.self, forKey: .name)?.wrappedValue
|
||||
template = try container.decodeIfPresent(Option<String>.self, forKey: .template)?.wrappedValue
|
||||
path = try container.decodeIfPresent(Option<String>.self, forKey: .path)?.wrappedValue
|
||||
try InitCommand.requiredTemplateOptions.forEach { option in
|
||||
requiredTemplateOptions[option.name] = try container.decode(Option<String>.self,
|
||||
forKey: .required(option.name)).wrappedValue
|
||||
}
|
||||
try InitCommand.optionalTemplateOptions.forEach { option in
|
||||
optionalTemplateOptions[option.name] = try container.decode(Option<String?>.self,
|
||||
forKey: .optional(option.name)).wrappedValue
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: InitCommandError, rhs: InitCommandError) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.ungettableProjectName(lhsPath), .ungettableProjectName(rhsPath)):
|
||||
return lhsPath == rhsPath
|
||||
case let (.nonEmptyDirectory(lhsPath), .nonEmptyDirectory(rhsPath)):
|
||||
return lhsPath == rhsPath
|
||||
case let (.templateNotFound(lhsTemplate), .templateNotFound(rhsTemplate)):
|
||||
return lhsTemplate == rhsTemplate
|
||||
case (.templateNotProvided, .templateNotProvided):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
func run() throws {
|
||||
try InitService().run(name: name,
|
||||
platform: platform,
|
||||
path: path,
|
||||
templateName: template,
|
||||
requiredTemplateOptions: requiredTemplateOptions,
|
||||
optionalTemplateOptions: optionalTemplateOptions)
|
||||
}
|
||||
}
|
||||
|
||||
class InitCommand: NSObject, Command {
|
||||
// MARK: - Attributes
|
||||
// MARK: - Preprocessing
|
||||
|
||||
static let command = "init"
|
||||
static let overview = "Bootstraps a project."
|
||||
private let platformArgument: OptionArgument<String>
|
||||
private let pathArgument: OptionArgument<String>
|
||||
private let nameArgument: OptionArgument<String>
|
||||
private let templateArgument: OptionArgument<String>
|
||||
private var attributesArguments: [String: OptionArgument<String>] = [:]
|
||||
private let subParser: ArgumentParser
|
||||
private let templatesDirectoryLocator: TemplatesDirectoryLocating
|
||||
private let templateGenerator: TemplateGenerating
|
||||
private let templateLoader: TemplateLoading
|
||||
extension InitCommand {
|
||||
static var requiredTemplateOptions: [(name: String, option: Option<String>)] = []
|
||||
static var optionalTemplateOptions: [(name: String, option: Option<String?>)] = []
|
||||
|
||||
// MARK: - Init
|
||||
/// We do not know template's option in advance -> we need to dynamically add them
|
||||
static func preprocess(_ arguments: [String]? = nil) throws {
|
||||
guard
|
||||
let arguments = arguments,
|
||||
arguments.contains("--template")
|
||||
else { return }
|
||||
|
||||
public required convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser,
|
||||
templatesDirectoryLocator: TemplatesDirectoryLocator(),
|
||||
templateGenerator: TemplateGenerator(),
|
||||
templateLoader: TemplateLoader())
|
||||
}
|
||||
|
||||
init(parser: ArgumentParser,
|
||||
templatesDirectoryLocator: TemplatesDirectoryLocating,
|
||||
templateGenerator: TemplateGenerating,
|
||||
templateLoader: TemplateLoading) {
|
||||
subParser = parser.add(subparser: InitCommand.command, overview: InitCommand.overview)
|
||||
platformArgument = subParser.add(option: "--platform",
|
||||
shortName: nil,
|
||||
kind: String.self,
|
||||
usage: "The platform (ios, tvos or macos) the product will be for (Default: ios).",
|
||||
completion: ShellCompletion.values([
|
||||
(value: "ios", description: "iOS platform"),
|
||||
(value: "tvos", description: "tvOS platform"),
|
||||
(value: "macos", description: "macOS platform"),
|
||||
]))
|
||||
pathArgument = subParser.add(option: "--path",
|
||||
shortName: "-p",
|
||||
kind: String.self,
|
||||
usage: "The path to the folder where the project will be generated (Default: Current directory).",
|
||||
completion: .filename)
|
||||
nameArgument = subParser.add(option: "--name",
|
||||
shortName: "-n",
|
||||
kind: String.self,
|
||||
usage: "The name of the project. If it's not passed (Default: Name of the directory).",
|
||||
completion: nil)
|
||||
templateArgument = subParser.add(option: "--template",
|
||||
shortName: "-t",
|
||||
kind: String.self,
|
||||
usage: "The name of the template to use (you can list available templates with tuist scaffold --list).",
|
||||
completion: nil)
|
||||
self.templatesDirectoryLocator = templatesDirectoryLocator
|
||||
self.templateGenerator = templateGenerator
|
||||
self.templateLoader = templateLoader
|
||||
}
|
||||
|
||||
func parse(with parser: ArgumentParser, arguments: [String]) throws -> ArgumentParser.Result {
|
||||
guard arguments.contains("--template") else { return try parser.parse(arguments) }
|
||||
// Plucking out path and template argument
|
||||
let pairedArguments = stride(from: 1, to: arguments.count, by: 2).map {
|
||||
arguments[$0 ..< min($0 + 2, arguments.count)]
|
||||
// We want to parse only the name of template, not its arguments which will be dynamically added
|
||||
// Plucking out path argument
|
||||
let pairedArguments: [[String]] = stride(from: 1, to: arguments.count, by: 2).map {
|
||||
Array(arguments[$0 ..< min($0 + 2, arguments.count)])
|
||||
}
|
||||
let possibleValues = ["--path", "-p", "--template", "-t"]
|
||||
let filteredArguments = pairedArguments
|
||||
.filter {
|
||||
$0.first == "--path" || $0.first == "--template"
|
||||
possibleValues.contains($0.first ?? "")
|
||||
}
|
||||
.flatMap { Array($0) }
|
||||
// We want to parse only the name of template, not its arguments which will be dynamically added
|
||||
let resultArguments = try parser.parse(Array(arguments.prefix(1)) + filteredArguments)
|
||||
.flatMap { $0 }
|
||||
|
||||
guard let templateName = resultArguments.get(templateArgument) else { throw InitCommandError.templateNotProvided }
|
||||
|
||||
let path = self.path(arguments: resultArguments)
|
||||
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
|
||||
|
||||
let templateDirectory = try self.templateDirectory(templateDirectories: directories,
|
||||
template: templateName)
|
||||
|
||||
let template = try templateLoader.loadTemplate(at: templateDirectory)
|
||||
|
||||
// Dynamically add attributes from template to `subParser`
|
||||
attributesArguments = template.attributes.reduce([:]) {
|
||||
var mutableDictionary = $0
|
||||
mutableDictionary[$1.name] = subParser.add(option: "--\($1.name)",
|
||||
kind: String.self)
|
||||
return mutableDictionary
|
||||
}
|
||||
|
||||
return try parser.parse(arguments)
|
||||
}
|
||||
|
||||
func run(with arguments: ArgumentParser.Result) throws {
|
||||
let platform = try self.platform(arguments: arguments)
|
||||
let path = self.path(arguments: arguments)
|
||||
let name = try self.name(arguments: arguments, path: path)
|
||||
try verifyDirectoryIsEmpty(path: path)
|
||||
|
||||
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
|
||||
if let template = arguments.get(templateArgument) {
|
||||
guard
|
||||
let templateDirectory = directories.first(where: { $0.basename == template })
|
||||
else { throw InitCommandError.templateNotFound(template) }
|
||||
let template = try templateLoader.loadTemplate(at: templateDirectory)
|
||||
let parsedAttributes = try validateAttributes(attributesArguments,
|
||||
template: template,
|
||||
name: name,
|
||||
platform: platform,
|
||||
arguments: arguments)
|
||||
|
||||
try templateGenerator.generate(template: template,
|
||||
to: path,
|
||||
attributes: parsedAttributes)
|
||||
} else {
|
||||
guard
|
||||
let templateDirectory = directories.first(where: { $0.basename == "default" })
|
||||
else { throw InitCommandError.templateNotFound("default") }
|
||||
let template = try templateLoader.loadTemplate(at: templateDirectory)
|
||||
try templateGenerator.generate(template: template,
|
||||
to: path,
|
||||
attributes: ["name": name, "platform": platform.caseValue])
|
||||
}
|
||||
|
||||
logger.notice("Project generated at path \(path.pathString).", metadata: .success)
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
/// Checks if the given directory is empty, essentially that it doesn't contain any file or directory.
|
||||
///
|
||||
/// - Parameter path: Directory to be checked.
|
||||
/// - Throws: An InitCommandError.nonEmptyDirectory error when the directory is not empty.
|
||||
private func verifyDirectoryIsEmpty(path: AbsolutePath) throws {
|
||||
if !path.glob("*").isEmpty {
|
||||
throw InitCommandError.nonEmptyDirectory(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates if all `attributes` from `template` have been provided
|
||||
/// If those attributes are optional, they default to `default` if not provided
|
||||
/// - Returns: Array of parsed attributes
|
||||
private func validateAttributes(_ attributes: [String: OptionArgument<String>],
|
||||
template: Template,
|
||||
name: String,
|
||||
platform: Platform,
|
||||
arguments: ArgumentParser.Result) throws -> [String: String] {
|
||||
try template.attributes.reduce(into: [:]) { attributesDict, attribute in
|
||||
if attribute.name == "name" {
|
||||
attributesDict[attribute.name] = name
|
||||
return
|
||||
}
|
||||
if attribute.name == "platform" {
|
||||
attributesDict[attribute.name] = platform.caseValue
|
||||
return
|
||||
}
|
||||
switch attribute {
|
||||
case let .required(name):
|
||||
guard
|
||||
let argument = attributes[name],
|
||||
let value = arguments.get(argument)
|
||||
else { throw InitCommandError.attributeNotProvided(name) }
|
||||
attributesDict[name] = value
|
||||
case let .optional(name, default: defaultValue):
|
||||
guard
|
||||
let argument = attributes[name],
|
||||
let value: String = arguments.get(argument)
|
||||
else {
|
||||
attributesDict[name] = defaultValue
|
||||
return
|
||||
}
|
||||
attributesDict[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds template directory
|
||||
/// - Parameters:
|
||||
/// - templateDirectories: Paths of available templates
|
||||
/// - template: Name of template
|
||||
/// - Returns: `AbsolutePath` of template directory
|
||||
private func templateDirectory(templateDirectories: [AbsolutePath], template: String) throws -> AbsolutePath {
|
||||
guard
|
||||
let templateDirectory = templateDirectories.first(where: { $0.basename == template })
|
||||
else { throw InitCommandError.templateNotFound(template) }
|
||||
return templateDirectory
|
||||
}
|
||||
let command = try parseAsRoot(filteredArguments) as? InitCommand,
|
||||
let templateName = command.template,
|
||||
templateName != "default"
|
||||
else { return }
|
||||
|
||||
private func name(arguments: ArgumentParser.Result, path: AbsolutePath) throws -> String {
|
||||
if let name = arguments.get(nameArgument) {
|
||||
return name
|
||||
} else if let name = path.components.last {
|
||||
return name
|
||||
} else {
|
||||
throw InitCommandError.ungettableProjectName(AbsolutePath.current)
|
||||
let (required, optional) = try InitService().loadTemplateOptions(templateName: templateName,
|
||||
path: command.path)
|
||||
|
||||
InitCommand.requiredTemplateOptions = required.map {
|
||||
(name: $0, option: Option<String>(name: .shortAndLong))
|
||||
}
|
||||
}
|
||||
|
||||
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
|
||||
if let path = arguments.get(pathArgument) {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
}
|
||||
|
||||
private func platform(arguments: ArgumentParser.Result) throws -> Platform {
|
||||
if let platformString = arguments.get(platformArgument) {
|
||||
if let platform = Platform(rawValue: platformString) {
|
||||
return platform
|
||||
} else {
|
||||
throw ArgumentParserError.invalidValue(argument: "platform", error: .custom("Platform should be either ios, tvos, or macos"))
|
||||
}
|
||||
} else {
|
||||
return .iOS
|
||||
InitCommand.optionalTemplateOptions = optional.map {
|
||||
(name: $0, option: Option<String?>(name: .shortAndLong))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - InitCommand.CodingKeys
|
||||
|
||||
extension InitCommand {
|
||||
enum CodingKeys: CodingKey {
|
||||
case platform
|
||||
case name
|
||||
case template
|
||||
case path
|
||||
case required(String)
|
||||
case optional(String)
|
||||
|
||||
var stringValue: String {
|
||||
switch self {
|
||||
case .platform:
|
||||
return "platform"
|
||||
case .name:
|
||||
return "name"
|
||||
case .template:
|
||||
return "template"
|
||||
case .path:
|
||||
return "path"
|
||||
case let .required(required):
|
||||
return required
|
||||
case let .optional(optional):
|
||||
return optional
|
||||
}
|
||||
}
|
||||
|
||||
// Not used
|
||||
var intValue: Int? { nil }
|
||||
init?(intValue _: Int) { nil }
|
||||
init?(stringValue _: String) { nil }
|
||||
}
|
||||
}
|
||||
|
||||
/// ArgumentParser library gets the list of options from a mirror
|
||||
/// Since we do not declare template's options in advance, we need to rewrite the mirror implementation and add them ourselves
|
||||
extension InitCommand: CustomReflectable {
|
||||
var customMirror: Mirror {
|
||||
let requiredTemplateChildren = InitCommand.requiredTemplateOptions
|
||||
.map { Mirror.Child(label: $0.name, value: $0.option) }
|
||||
let optionalTemplateChildren = InitCommand.optionalTemplateOptions
|
||||
.map { Mirror.Child(label: $0.name, value: $0.option) }
|
||||
let children = [
|
||||
Mirror.Child(label: "platform", value: _platform),
|
||||
Mirror.Child(label: "name", value: _name),
|
||||
Mirror.Child(label: "template", value: _template),
|
||||
Mirror.Child(label: "path", value: _path),
|
||||
]
|
||||
return Mirror(InitCommand(), children: children + requiredTemplateChildren + optionalTemplateChildren)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,115 +1,21 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistGenerator
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
enum LintCommandError: FatalError, Equatable {
|
||||
/// Thrown when neither a workspace or a project is found in the given path.
|
||||
case manifestNotFound(AbsolutePath)
|
||||
|
||||
/// Error type.
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .manifestNotFound:
|
||||
return .abort
|
||||
}
|
||||
}
|
||||
|
||||
/// Description
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .manifestNotFound(path):
|
||||
return "Couldn't find Project.swift nor Workspace.swift at \(path.pathString)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command that builds a target from the project in the current directory.
|
||||
class LintCommand: NSObject, Command {
|
||||
/// Command name.
|
||||
static var command: String = "lint"
|
||||
|
||||
/// Command description.
|
||||
static var overview: String = "Lints a workspace or a project that check whether they are well configured."
|
||||
|
||||
/// Graph linter
|
||||
private let graphLinter: GraphLinting
|
||||
private let environmentLinter: EnvironmentLinting
|
||||
private let manifestLoading: ManifestLoading
|
||||
private let graphLoader: GraphLoading
|
||||
let pathArgument: OptionArgument<String>
|
||||
|
||||
/// Default constructor.
|
||||
public required convenience init(parser: ArgumentParser) {
|
||||
let manifestLoader = ManifestLoader()
|
||||
let generatorModelLoader = GeneratorModelLoader(manifestLoader: manifestLoader,
|
||||
manifestLinter: AnyManifestLinter())
|
||||
self.init(graphLinter: GraphLinter(),
|
||||
environmentLinter: EnvironmentLinter(),
|
||||
manifestLoading: manifestLoader,
|
||||
graphLoader: GraphLoader(modelLoader: generatorModelLoader),
|
||||
parser: parser)
|
||||
struct LintCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "lint",
|
||||
abstract: "Lints a workspace or a project that check whether they are well configured")
|
||||
}
|
||||
|
||||
init(graphLinter: GraphLinting,
|
||||
environmentLinter: EnvironmentLinting,
|
||||
manifestLoading: ManifestLoading,
|
||||
graphLoader: GraphLoading,
|
||||
parser: ArgumentParser) {
|
||||
let subParser = parser.add(subparser: LintCommand.command, overview: LintCommand.overview)
|
||||
self.graphLinter = graphLinter
|
||||
self.environmentLinter = environmentLinter
|
||||
self.manifestLoading = manifestLoading
|
||||
self.graphLoader = graphLoader
|
||||
pathArgument = subParser.add(option: "--path",
|
||||
shortName: "-p",
|
||||
kind: String.self,
|
||||
usage: "The path to the directory that contains the workspace or project to be linted",
|
||||
completion: .filename)
|
||||
}
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path to the directory that contains the workspace or project to be linted"
|
||||
)
|
||||
var path: String?
|
||||
|
||||
func run(with arguments: ArgumentParser.Result) throws {
|
||||
let path = self.path(arguments: arguments)
|
||||
|
||||
// Load graph
|
||||
let manifests = manifestLoading.manifests(at: path)
|
||||
var graph: Graph!
|
||||
|
||||
logger.notice("Loading the dependency graph")
|
||||
if manifests.contains(.workspace) {
|
||||
logger.notice("Loading workspace at \(path.pathString)")
|
||||
(graph, _) = try graphLoader.loadWorkspace(path: path)
|
||||
} else if manifests.contains(.project) {
|
||||
logger.notice("Loading project at \(path.pathString)")
|
||||
(graph, _) = try graphLoader.loadProject(path: path)
|
||||
} else {
|
||||
throw LintCommandError.manifestNotFound(path)
|
||||
}
|
||||
|
||||
logger.notice("Running linters")
|
||||
let config = try graphLoader.loadConfig(path: path)
|
||||
|
||||
var issues: [LintingIssue] = []
|
||||
logger.notice("Linting the environment")
|
||||
issues.append(contentsOf: try environmentLinter.lint(config: config))
|
||||
logger.notice("Linting the loaded dependency graph")
|
||||
issues.append(contentsOf: graphLinter.lint(graph: graph))
|
||||
|
||||
if issues.isEmpty {
|
||||
logger.notice("No linting issues found", metadata: .success)
|
||||
} else {
|
||||
try issues.printAndThrowIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
|
||||
if let path = arguments.get(pathArgument) {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
func run() throws {
|
||||
try LintService().run(path: path)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
struct ListCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "list",
|
||||
abstract: "Lists available scaffold templates",
|
||||
subcommands: [])
|
||||
}
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path where you want to list templates from"
|
||||
)
|
||||
var path: String?
|
||||
|
||||
func run() throws {
|
||||
try ListService().run(path: path)
|
||||
}
|
||||
}
|
|
@ -1,202 +1,157 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistLoader
|
||||
import TuistScaffold
|
||||
import TuistSupport
|
||||
|
||||
enum ScaffoldCommandError: FatalError, Equatable {
|
||||
var type: ErrorType { .abort }
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .templateNotProvided:
|
||||
return .abort
|
||||
}
|
||||
}
|
||||
|
||||
case templateNotFound(String)
|
||||
case templateNotProvided
|
||||
case nonEmptyDirectory(AbsolutePath)
|
||||
case attributeNotProvided(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .templateNotFound(template):
|
||||
return "Could not find template \(template). Make sure it exists at Tuist/Templates/\(template)"
|
||||
case .templateNotProvided:
|
||||
return "You must provide template name"
|
||||
case let .nonEmptyDirectory(path):
|
||||
return "Can't generate a template in the non-empty directory at path \(path.pathString)."
|
||||
case let .attributeNotProvided(name):
|
||||
return "You must provide \(name) option. Add --\(name) desired_value to your command."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ScaffoldCommand: NSObject, Command {
|
||||
// MARK: - Attributes
|
||||
|
||||
static let command = "scaffold"
|
||||
static let overview = "Generates new project based on template."
|
||||
private let listArgument: OptionArgument<Bool>
|
||||
private let pathArgument: OptionArgument<String>
|
||||
private let templateArgument: PositionalArgument<String>
|
||||
private var attributesArguments: [String: OptionArgument<String>] = [:]
|
||||
private let subParser: ArgumentParser
|
||||
|
||||
private let templateLoader: TemplateLoading
|
||||
private let templatesDirectoryLocator: TemplatesDirectoryLocating
|
||||
private let templateGenerator: TemplateGenerating
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public required convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser,
|
||||
templateLoader: TemplateLoader(),
|
||||
templatesDirectoryLocator: TemplatesDirectoryLocator(),
|
||||
templateGenerator: TemplateGenerator())
|
||||
struct ScaffoldCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "scaffold",
|
||||
abstract: "Generates new project based on template",
|
||||
subcommands: [ListCommand.self])
|
||||
}
|
||||
|
||||
init(parser: ArgumentParser,
|
||||
templateLoader: TemplateLoading,
|
||||
templatesDirectoryLocator: TemplatesDirectoryLocating,
|
||||
templateGenerator: TemplateGenerating) {
|
||||
subParser = parser.add(subparser: ScaffoldCommand.command, overview: ScaffoldCommand.overview)
|
||||
listArgument = subParser.add(option: "--list",
|
||||
shortName: "-l",
|
||||
kind: Bool.self,
|
||||
usage: "Lists available scaffold templates",
|
||||
completion: nil)
|
||||
templateArgument = subParser.add(positional: "template",
|
||||
kind: String.self,
|
||||
optional: true,
|
||||
usage: "Name of template you want to use",
|
||||
completion: nil)
|
||||
pathArgument = subParser.add(option: "--path",
|
||||
shortName: "-p",
|
||||
kind: String.self,
|
||||
usage: "The path to the folder where the template will be generated (Default: Current directory).",
|
||||
completion: .filename)
|
||||
self.templateLoader = templateLoader
|
||||
self.templatesDirectoryLocator = templatesDirectoryLocator
|
||||
self.templateGenerator = templateGenerator
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path to the folder where the template will be generated (Default: Current directory)"
|
||||
)
|
||||
var path: String?
|
||||
|
||||
@Argument(
|
||||
help: "Name of template you want to use"
|
||||
)
|
||||
var template: String
|
||||
|
||||
var requiredTemplateOptions: [String: String] = [:]
|
||||
var optionalTemplateOptions: [String: String?] = [:]
|
||||
|
||||
init() {}
|
||||
|
||||
// Custom decoding to decode dynamic options
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
template = try container.decode(Argument<String>.self, forKey: .template).wrappedValue
|
||||
path = try container.decodeIfPresent(Option<String>.self, forKey: .path)?.wrappedValue
|
||||
try ScaffoldCommand.requiredTemplateOptions.forEach { option in
|
||||
requiredTemplateOptions[option.name] = try container.decode(Option<String>.self,
|
||||
forKey: .required(option.name)).wrappedValue
|
||||
}
|
||||
try ScaffoldCommand.optionalTemplateOptions.forEach { option in
|
||||
optionalTemplateOptions[option.name] = try container.decode(Option<String?>.self,
|
||||
forKey: .optional(option.name)).wrappedValue
|
||||
}
|
||||
}
|
||||
|
||||
func parse(with parser: ArgumentParser, arguments: [String]) throws -> (ArgumentParser.Result, ArgumentParser) {
|
||||
guard arguments.count >= 2 else { throw ScaffoldCommandError.templateNotProvided }
|
||||
// We want to parse only the name of template, not its arguments which will be dynamically added
|
||||
let templateArguments = Array(arguments.prefix(2))
|
||||
// Plucking out path argument
|
||||
let filteredArguments = stride(from: 2, to: arguments.count, by: 2).map {
|
||||
arguments[$0 ..< min($0 + 2, arguments.count)]
|
||||
}
|
||||
.filter {
|
||||
$0.first == "--path"
|
||||
}
|
||||
.flatMap { Array($0) }
|
||||
// We want to parse only the name of template, not its arguments which will be dynamically added
|
||||
let resultArguments = try parser.parse(templateArguments + filteredArguments)
|
||||
|
||||
if resultArguments.get(listArgument) != nil {
|
||||
return (try parser.parse(arguments), parser)
|
||||
}
|
||||
|
||||
guard let templateName = resultArguments.get(templateArgument) else { throw ScaffoldCommandError.templateNotProvided }
|
||||
|
||||
let path = self.path(arguments: resultArguments)
|
||||
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
|
||||
|
||||
let templateDirectory = try self.templateDirectory(templateDirectories: directories,
|
||||
template: templateName)
|
||||
|
||||
let template = try templateLoader.loadTemplate(at: templateDirectory)
|
||||
|
||||
// Dynamically add attributes from template to `subParser`
|
||||
attributesArguments = template.attributes.reduce([:]) {
|
||||
var mutableDictionary = $0
|
||||
mutableDictionary[$1.name] = subParser.add(option: "--\($1.name)",
|
||||
kind: String.self)
|
||||
return mutableDictionary
|
||||
}
|
||||
|
||||
return (try parser.parse(arguments), parser)
|
||||
}
|
||||
|
||||
func run(with arguments: ArgumentParser.Result) throws {
|
||||
let path = self.path(arguments: arguments)
|
||||
|
||||
let templateDirectories = try templatesDirectoryLocator.templateDirectories(at: path)
|
||||
|
||||
let shouldList = arguments.get(listArgument) ?? false
|
||||
if shouldList {
|
||||
try templateDirectories.forEach {
|
||||
let template = try templateLoader.loadTemplate(at: $0)
|
||||
logger.info("\($0.basename): \(template.description)")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let templateName = arguments.get(templateArgument) else { throw ScaffoldCommandError.templateNotProvided }
|
||||
|
||||
let templateDirectory = try self.templateDirectory(templateDirectories: templateDirectories,
|
||||
template: templateName)
|
||||
|
||||
let template = try templateLoader.loadTemplate(at: templateDirectory)
|
||||
|
||||
let parsedAttributes = try validateAttributes(attributesArguments,
|
||||
template: template,
|
||||
arguments: arguments)
|
||||
|
||||
try templateGenerator.generate(template: template,
|
||||
to: path,
|
||||
attributes: parsedAttributes)
|
||||
|
||||
logger.notice("Template \(templateName) was successfully generated", metadata: .success)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
|
||||
if let path = arguments.get(pathArgument) {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
func run() throws {
|
||||
// Currently, @Argument and subcommand clashes, so we need to handle that ourselves
|
||||
if template == ListCommand.configuration.commandName {
|
||||
try ListService().run(path: path)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
try ScaffoldService().run(path: path,
|
||||
templateName: template,
|
||||
requiredTemplateOptions: requiredTemplateOptions,
|
||||
optionalTemplateOptions: optionalTemplateOptions)
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates if all `attributes` from `template` have been provided
|
||||
/// If those attributes are optional, they default to `default` if not provided
|
||||
/// - Returns: Array of parsed attributes
|
||||
private func validateAttributes(_ attributes: [String: OptionArgument<String>],
|
||||
template: Template,
|
||||
arguments: ArgumentParser.Result) throws -> [String: String] {
|
||||
try template.attributes.reduce([:]) {
|
||||
var mutableDict = $0
|
||||
switch $1 {
|
||||
case let .required(name):
|
||||
guard
|
||||
let argument = attributes[name],
|
||||
let value = arguments.get(argument)
|
||||
else { throw ScaffoldCommandError.attributeNotProvided(name) }
|
||||
mutableDict[name] = value
|
||||
case let .optional(name, default: defaultValue):
|
||||
guard
|
||||
let argument = attributes[name],
|
||||
let value: String = arguments.get(argument)
|
||||
else {
|
||||
mutableDict[name] = defaultValue
|
||||
return mutableDict
|
||||
}
|
||||
mutableDict[name] = value
|
||||
}
|
||||
return mutableDict
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds template directory
|
||||
/// - Parameters:
|
||||
/// - templateDirectories: Paths of available templates
|
||||
/// - template: Name of template
|
||||
/// - Returns: `AbsolutePath` of template directory
|
||||
private func templateDirectory(templateDirectories: [AbsolutePath], template: String) throws -> AbsolutePath {
|
||||
guard
|
||||
let templateDirectory = templateDirectories.first(where: { $0.basename == template })
|
||||
else { throw ScaffoldCommandError.templateNotFound(template) }
|
||||
return templateDirectory
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preprocessing
|
||||
|
||||
extension ScaffoldCommand {
|
||||
static var requiredTemplateOptions: [(name: String, option: Option<String>)] = []
|
||||
static var optionalTemplateOptions: [(name: String, option: Option<String?>)] = []
|
||||
|
||||
/// We do not know template's option in advance -> we need to dynamically add them
|
||||
static func preprocess(_ arguments: [String]? = nil) throws {
|
||||
guard
|
||||
let arguments = arguments,
|
||||
arguments.count >= 2
|
||||
else { throw ScaffoldCommandError.templateNotProvided }
|
||||
guard !configuration.subcommands.contains(where: { $0.configuration.commandName == arguments[1] }) else { return }
|
||||
// We want to parse only the name of template, not its arguments which will be dynamically added
|
||||
// Plucking out path argument
|
||||
let pairedArguments: [[String]] = stride(from: 2, to: arguments.count, by: 2).map {
|
||||
Array(arguments[$0 ..< min($0 + 2, arguments.count)])
|
||||
}
|
||||
let filteredArguments = pairedArguments
|
||||
.filter {
|
||||
$0.first == "--path" || $0.first == "-p"
|
||||
}
|
||||
.flatMap { $0 }
|
||||
|
||||
guard let command = try parseAsRoot([arguments[1]] + filteredArguments) as? ScaffoldCommand else { return }
|
||||
|
||||
let (required, optional) = try ScaffoldService().loadTemplateOptions(templateName: command.template,
|
||||
path: command.path)
|
||||
|
||||
ScaffoldCommand.requiredTemplateOptions = required.map {
|
||||
(name: $0, option: Option<String>(name: .shortAndLong))
|
||||
}
|
||||
ScaffoldCommand.optionalTemplateOptions = optional.map {
|
||||
(name: $0, option: Option<String?>(name: .shortAndLong))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ScaffoldCommand.CodingKeys
|
||||
|
||||
extension ScaffoldCommand {
|
||||
enum CodingKeys: CodingKey {
|
||||
case template
|
||||
case path
|
||||
case required(String)
|
||||
case optional(String)
|
||||
|
||||
var stringValue: String {
|
||||
switch self {
|
||||
case .template:
|
||||
return "template"
|
||||
case .path:
|
||||
return "path"
|
||||
case let .required(required):
|
||||
return required
|
||||
case let .optional(optional):
|
||||
return optional
|
||||
}
|
||||
}
|
||||
|
||||
// Not used
|
||||
var intValue: Int? { nil }
|
||||
init?(intValue _: Int) { nil }
|
||||
init?(stringValue _: String) { nil }
|
||||
}
|
||||
}
|
||||
|
||||
/// ArgumentParser library gets the list of options from a mirror
|
||||
/// Since we do not declare template's options in advance, we need to rewrite the mirror implementation and add them ourselves
|
||||
extension ScaffoldCommand: CustomReflectable {
|
||||
var customMirror: Mirror {
|
||||
let requiredTemplateChildren = ScaffoldCommand.requiredTemplateOptions
|
||||
.map { Mirror.Child(label: $0.name, value: $0.option) }
|
||||
let optionalTemplateChildren = ScaffoldCommand.optionalTemplateOptions
|
||||
.map { Mirror.Child(label: $0.name, value: $0.option) }
|
||||
let children = [
|
||||
Mirror.Child(label: "template", value: _template),
|
||||
Mirror.Child(label: "path", value: _path),
|
||||
]
|
||||
return Mirror(ScaffoldCommand(), children: children + requiredTemplateChildren + optionalTemplateChildren)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +1,23 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistSigning
|
||||
import TuistSupport
|
||||
|
||||
class DecryptCommand: NSObject, Command {
|
||||
// MARK: - Attributes
|
||||
|
||||
static let command = "decrypt"
|
||||
static let overview = "Decrypts all files in Tuist/Signing directory."
|
||||
private let pathArgument: OptionArgument<String>
|
||||
|
||||
private let signingCipher: SigningCiphering
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public required convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser, signingCipher: SigningCipher())
|
||||
struct DecryptCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "decrypt",
|
||||
abstract: "Decrypts all files in Tuist/Signing directory")
|
||||
}
|
||||
|
||||
init(parser: ArgumentParser,
|
||||
signingCipher: SigningCiphering) {
|
||||
let subParser = parser.add(subparser: DecryptCommand.command, overview: DecryptCommand.overview)
|
||||
pathArgument = subParser.add(option: "--path",
|
||||
shortName: "-p",
|
||||
kind: String.self,
|
||||
usage: "The path to the folder containing the encrypted certificates",
|
||||
completion: .filename)
|
||||
self.signingCipher = signingCipher
|
||||
}
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path to the folder containing the encrypted certificates"
|
||||
)
|
||||
var path: String?
|
||||
|
||||
func run(with arguments: ArgumentParser.Result) throws {
|
||||
let path = self.path(arguments: arguments)
|
||||
try signingCipher.decryptSigning(at: path)
|
||||
logger.notice("Successfully decrypted all signing files", metadata: .success)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
|
||||
if let path = arguments.get(pathArgument) {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
func run() throws {
|
||||
try DecryptService().run(path: path)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +1,20 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistSigning
|
||||
import TuistSupport
|
||||
|
||||
class EncryptCommand: NSObject, Command {
|
||||
// MARK: - Attributes
|
||||
|
||||
static let command = "encrypt"
|
||||
static let overview = "Encrypts all files in Tuist/Signing directory."
|
||||
private let pathArgument: OptionArgument<String>
|
||||
|
||||
private let signingCipher: SigningCiphering
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public required convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser, signingCipher: SigningCipher())
|
||||
struct EncryptCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "encrypt",
|
||||
abstract: "Encrypts all files in Tuist/Signing directory")
|
||||
}
|
||||
|
||||
init(parser: ArgumentParser,
|
||||
signingCipher: SigningCiphering) {
|
||||
let subParser = parser.add(subparser: EncryptCommand.command, overview: EncryptCommand.overview)
|
||||
pathArgument = subParser.add(option: "--path",
|
||||
shortName: "-p",
|
||||
kind: String.self,
|
||||
usage: "The path to the folder containing the certificates you would like to encrypt",
|
||||
completion: .filename)
|
||||
self.signingCipher = signingCipher
|
||||
}
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path to the folder containing the certificates you would like to encrypt"
|
||||
)
|
||||
var path: String?
|
||||
|
||||
func run(with arguments: ArgumentParser.Result) throws {
|
||||
let path = self.path(arguments: arguments)
|
||||
try signingCipher.encryptSigning(at: path)
|
||||
|
||||
logger.notice("Successfully encrypted all signing files", metadata: .success)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
|
||||
if let path = arguments.get(pathArgument) {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
func run() throws {
|
||||
try EncryptService().run(path: path)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,14 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistSigning
|
||||
import TuistSupport
|
||||
|
||||
class SigningCommand: NSObject, Command {
|
||||
// MARK: - Attributes
|
||||
|
||||
static let command = "signing"
|
||||
static let overview = "A set of commands for signing-related operations. "
|
||||
let subcommands: [Command]
|
||||
|
||||
private let argumentParser: ArgumentParser
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public required init(parser: ArgumentParser) {
|
||||
_ = parser.add(subparser: SigningCommand.command, overview: SigningCommand.overview)
|
||||
let argumentParser = ArgumentParser(commandName: Self.command, usage: "tuist signing <command> <options>", overview: Self.overview)
|
||||
let subcommands: [Command.Type] = [EncryptCommand.self, DecryptCommand.self]
|
||||
self.subcommands = subcommands.map { $0.init(parser: argumentParser) }
|
||||
self.argumentParser = argumentParser
|
||||
}
|
||||
|
||||
func parse(with _: ArgumentParser, arguments: [String]) throws -> (ArgumentParser.Result, ArgumentParser) {
|
||||
return (try argumentParser.parse(Array(arguments.dropFirst())), argumentParser)
|
||||
}
|
||||
|
||||
func run(with _: ArgumentParser.Result) throws {
|
||||
argumentParser.printUsage(on: stdoutStream)
|
||||
struct SigningCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "signing",
|
||||
abstract: "A set of commands for signing-related operations",
|
||||
subcommands: [
|
||||
EncryptCommand.self,
|
||||
DecryptCommand.self,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
public struct TuistCommand: ParsableCommand {
|
||||
public init() {}
|
||||
|
||||
public static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "tuist",
|
||||
abstract: "Generate, build and test your Xcode projects.",
|
||||
subcommands: [
|
||||
GenerateCommand.self,
|
||||
UpCommand.self,
|
||||
FocusCommand.self,
|
||||
EditCommand.self,
|
||||
DumpCommand.self,
|
||||
GraphCommand.self,
|
||||
LintCommand.self,
|
||||
VersionCommand.self,
|
||||
BuildCommand.self,
|
||||
CacheCommand.self,
|
||||
CreateIssueCommand.self,
|
||||
ScaffoldCommand.self,
|
||||
InitCommand.self,
|
||||
CloudCommand.self,
|
||||
SigningCommand.self,
|
||||
])
|
||||
}
|
||||
|
||||
public static func main(_ arguments: [String]? = nil) -> Never {
|
||||
let errorHandler = ErrorHandler()
|
||||
let command: ParsableCommand
|
||||
do {
|
||||
let processedArguments = Array(processArguments(arguments)?.dropFirst() ?? [])
|
||||
if processedArguments.first == ScaffoldCommand.configuration.commandName {
|
||||
try ScaffoldCommand.preprocess(processedArguments)
|
||||
}
|
||||
if processedArguments.first == InitCommand.configuration.commandName {
|
||||
try InitCommand.preprocess(processedArguments)
|
||||
}
|
||||
command = try parseAsRoot(processedArguments)
|
||||
} catch {
|
||||
logger.error("\(fullMessage(for: error))")
|
||||
_exit(exitCode(for: error).rawValue)
|
||||
}
|
||||
do {
|
||||
try command.run()
|
||||
exit()
|
||||
} catch let error as CleanExit {
|
||||
_exit(exitCode(for: error).rawValue)
|
||||
} catch let error as FatalError {
|
||||
errorHandler.fatal(error: error)
|
||||
_exit(exitCode(for: error).rawValue)
|
||||
} catch {
|
||||
errorHandler.fatal(error: UnhandledError(error: error))
|
||||
_exit(exitCode(for: error).rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
static func processArguments(_ arguments: [String]? = nil) -> [String]? {
|
||||
let arguments = arguments ?? Array(ProcessInfo.processInfo.arguments)
|
||||
return arguments.filter { $0 != "--verbose" }
|
||||
}
|
||||
}
|
|
@ -1,67 +1,23 @@
|
|||
import Basic
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
/// Command that configures the environment to work on the project.
|
||||
class UpCommand: NSObject, Command {
|
||||
// MARK: - Attributes
|
||||
|
||||
/// Name of the command.
|
||||
static let command = "up"
|
||||
|
||||
/// Description of the command.
|
||||
static let overview = "Configures the environment for the project."
|
||||
|
||||
/// Path to the project directory.
|
||||
let pathArgument: OptionArgument<String>
|
||||
|
||||
/// Instance to load the setup manifest and perform the project setup.
|
||||
private let setupLoader: SetupLoading
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
/// Initializes the command with the CLI parser.
|
||||
///
|
||||
/// - Parameter parser: CLI parser where the command should register itself.
|
||||
public required convenience init(parser: ArgumentParser) {
|
||||
self.init(parser: parser,
|
||||
setupLoader: SetupLoader())
|
||||
struct UpCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(
|
||||
commandName: "up",
|
||||
abstract: "Configures the environment for the project.",
|
||||
subcommands: []
|
||||
)
|
||||
}
|
||||
|
||||
/// Initializes the command with its arguments.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - parser: CLI parser where the command should register itself.
|
||||
/// - setupLoader: Instance to load the setup manifest and perform the project setup.
|
||||
init(parser: ArgumentParser,
|
||||
setupLoader: SetupLoading) {
|
||||
let subParser = parser.add(subparser: UpCommand.command, overview: UpCommand.overview)
|
||||
pathArgument = subParser.add(option: "--path",
|
||||
shortName: "-p",
|
||||
kind: String.self,
|
||||
usage: "The path to the directory that contains the project.",
|
||||
completion: .filename)
|
||||
self.setupLoader = setupLoader
|
||||
}
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path to the directory that contains the project."
|
||||
)
|
||||
var path: String?
|
||||
|
||||
/// Runs the command using the result from parsing the command line arguments.
|
||||
///
|
||||
/// - Throws: An error if the the configuration of the environment fails.
|
||||
func run(with arguments: ArgumentParser.Result) throws {
|
||||
try setupLoader.meet(at: path(arguments: arguments))
|
||||
}
|
||||
|
||||
/// Parses the arguments and returns the path to the directory where
|
||||
/// the up command should be ran.
|
||||
///
|
||||
/// - Parameter arguments: Result from parsing the command line arguments.
|
||||
/// - Returns: Path to be used for the up command.
|
||||
private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
|
||||
guard let path = arguments.get(pathArgument) else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
func run() throws {
|
||||
try UpService().run(path: path)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,14 @@
|
|||
import ArgumentParser
|
||||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
class VersionCommand: NSObject, Command {
|
||||
// MARK: - Command
|
||||
|
||||
static let command = "version"
|
||||
static let overview = "Outputs the current version of tuist."
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
required init(parser: ArgumentParser) {
|
||||
parser.add(subparser: VersionCommand.command, overview: VersionCommand.overview)
|
||||
struct VersionCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "version",
|
||||
abstract: "Outputs the current version of tuist")
|
||||
}
|
||||
|
||||
// MARK: - Command
|
||||
|
||||
func run(with _: ArgumentParser.Result) {
|
||||
logger.notice("\(Constants.version)")
|
||||
func run() throws {
|
||||
try VersionService().run()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ final class ProjectEditor: ProjectEditing {
|
|||
}
|
||||
|
||||
// To be sure that we are using the same binary of Tuist that invoked `edit`
|
||||
let tuistPath = AbsolutePath(CommandRegistry.processArguments().first!)
|
||||
let tuistPath = AbsolutePath(TuistCommand.processArguments()!.first!)
|
||||
|
||||
let (project, graph) = projectEditorMapper.map(tuistPath: tuistPath,
|
||||
sourceRootPath: at,
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
enum BuildServiceError: FatalError {
|
||||
// Error description
|
||||
var description: String {
|
||||
""
|
||||
}
|
||||
|
||||
// Error type
|
||||
var type: ErrorType { .abort }
|
||||
}
|
||||
|
||||
final class BuildService {
|
||||
func run() throws {
|
||||
logger.notice("Command not available yet")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
final class CacheService {
|
||||
/// Cache controller.
|
||||
private let cacheController: CacheControlling
|
||||
|
||||
init(cacheController: CacheControlling = CacheController()) {
|
||||
self.cacheController = cacheController
|
||||
}
|
||||
|
||||
func run(path: String?) throws {
|
||||
let path = self.path(path)
|
||||
try cacheController.cache(path: path)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func path(_ path: String?) -> AbsolutePath {
|
||||
if let path = path {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
|
||||
final class CreateIssueService {
|
||||
static let createIssueUrl: String = "https://github.com/tuist/tuist/issues/new"
|
||||
|
||||
func run() throws {
|
||||
try System.shared.run("/usr/bin/open", CreateIssueService.createIssueUrl)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import TuistCore
|
||||
import TuistSigning
|
||||
import TuistSupport
|
||||
|
||||
final class DecryptService {
|
||||
private let signingCipher: SigningCiphering
|
||||
|
||||
init(signingCipher: SigningCiphering = SigningCipher()) {
|
||||
self.signingCipher = signingCipher
|
||||
}
|
||||
|
||||
func run(path: String?) throws {
|
||||
let path = self.path(path)
|
||||
try signingCipher.decryptSigning(at: path)
|
||||
logger.notice("Successfully decrypted all signing files", metadata: .success)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func path(_ path: String?) -> AbsolutePath {
|
||||
if let path = path {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
final class DumpService {
|
||||
private let manifestLoader: ManifestLoading
|
||||
|
||||
init(manifestLoader: ManifestLoading = ManifestLoader()) {
|
||||
self.manifestLoader = manifestLoader
|
||||
}
|
||||
|
||||
func run(path: String?) throws {
|
||||
let projectPath: AbsolutePath
|
||||
if let path = path {
|
||||
projectPath = AbsolutePath(path, relativeTo: AbsolutePath.current)
|
||||
} else {
|
||||
projectPath = AbsolutePath.current
|
||||
}
|
||||
let project = try manifestLoader.loadProject(at: projectPath)
|
||||
let json: JSON = try project.toJSON()
|
||||
logger.notice("\(json.toString(prettyPrint: true))")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import Signals
|
||||
import TuistGenerator
|
||||
import TuistSupport
|
||||
|
||||
final class EditService {
|
||||
private let projectEditor: ProjectEditing
|
||||
private let opener: Opening
|
||||
|
||||
init(projectEditor: ProjectEditing = ProjectEditor(),
|
||||
opener: Opening = Opener()) {
|
||||
self.projectEditor = projectEditor
|
||||
self.opener = opener
|
||||
}
|
||||
|
||||
func run(path: String?,
|
||||
permanent: Bool) throws {
|
||||
let path = self.path(path)
|
||||
let generationDirectory = permanent ? path : EditService.temporaryDirectory.path
|
||||
let xcodeprojPath = try projectEditor.edit(at: path, in: generationDirectory)
|
||||
|
||||
if !permanent {
|
||||
Signals.trap(signals: [.int, .abrt]) { _ in
|
||||
// swiftlint:disable:next force_try
|
||||
try! FileHandler.shared.delete(EditService.temporaryDirectory.path)
|
||||
exit(0)
|
||||
}
|
||||
logger.pretty("Opening Xcode to edit the project. Press \(.keystroke("CTRL + C")) once you are done editing")
|
||||
try opener.open(path: xcodeprojPath)
|
||||
} else {
|
||||
logger.notice("Xcode project generated at \(xcodeprojPath.pathString)", metadata: .success)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func path(_ path: String?) -> AbsolutePath {
|
||||
if let path = path {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
}
|
||||
|
||||
private static var _temporaryDirectory: TemporaryDirectory?
|
||||
private static var temporaryDirectory: TemporaryDirectory {
|
||||
// swiftlint:disable:next identifier_name
|
||||
if let _temporaryDirectory = _temporaryDirectory { return _temporaryDirectory }
|
||||
// swiftlint:disable:next force_try
|
||||
_temporaryDirectory = try! TemporaryDirectory(removeTreeOnDeinit: true)
|
||||
return _temporaryDirectory!
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import TuistCore
|
||||
import TuistSigning
|
||||
import TuistSupport
|
||||
|
||||
final class EncryptService {
|
||||
private let signingCipher: SigningCiphering
|
||||
|
||||
init(signingCipher: SigningCiphering = SigningCipher()) {
|
||||
self.signingCipher = signingCipher
|
||||
}
|
||||
|
||||
func run(path: String?) throws {
|
||||
let path = self.path(path)
|
||||
try signingCipher.encryptSigning(at: path)
|
||||
|
||||
logger.notice("Successfully encrypted all signing files", metadata: .success)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func path(_ path: String?) -> AbsolutePath {
|
||||
if let path = path {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import RxBlocking
|
||||
import RxSwift
|
||||
import TuistCache
|
||||
import TuistCore
|
||||
import TuistGenerator
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
final class FocusService {
|
||||
/// Generator instance to generate the project workspace.
|
||||
private let generator: ProjectGenerating
|
||||
|
||||
/// Opener instance to run open in the system.
|
||||
private let opener: Opening
|
||||
|
||||
init(generator: ProjectGenerating = ProjectGenerator(),
|
||||
opener: Opening = Opener()) {
|
||||
self.generator = generator
|
||||
self.opener = opener
|
||||
}
|
||||
|
||||
func run() throws {
|
||||
let path = FileHandler.shared.currentPath
|
||||
|
||||
let workspacePath = try generator.generate(path: path,
|
||||
projectOnly: false)
|
||||
|
||||
try opener.open(path: workspacePath)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import Basic
|
||||
import TuistGenerator
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
final class GenerateService {
|
||||
// MARK: - Attributes
|
||||
|
||||
private let clock: Clock
|
||||
private let generator: ProjectGenerating
|
||||
|
||||
init(generator: ProjectGenerating = ProjectGenerator(),
|
||||
clock: Clock = WallClock()) {
|
||||
self.generator = generator
|
||||
self.clock = clock
|
||||
}
|
||||
|
||||
func run(path: String?,
|
||||
projectOnly: Bool) throws {
|
||||
let timer = clock.startTimer()
|
||||
let path = self.path(path)
|
||||
|
||||
try generator.generate(path: path, projectOnly: projectOnly)
|
||||
|
||||
let time = String(format: "%.3f", timer.stop())
|
||||
|
||||
logger.notice("Project generated.", metadata: .success)
|
||||
logger.notice("Total time taken: \(time)s")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func path(_ path: String?) -> AbsolutePath {
|
||||
if let path = path {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import TuistGenerator
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
final class GraphService {
|
||||
/// Dot graph generator.
|
||||
private let dotGraphGenerator: DotGraphGenerating
|
||||
|
||||
/// Manifest loader.
|
||||
private let manifestLoader: ManifestLoading
|
||||
|
||||
init(dotGraphGenerator: DotGraphGenerating = DotGraphGenerator(modelLoader: GeneratorModelLoader(manifestLoader: ManifestLoader(),
|
||||
manifestLinter: ManifestLinter())),
|
||||
manifestLoader: ManifestLoading = ManifestLoader()) {
|
||||
self.dotGraphGenerator = dotGraphGenerator
|
||||
self.manifestLoader = manifestLoader
|
||||
}
|
||||
|
||||
func run() throws {
|
||||
let graph = try dotGraphGenerator.generate(at: FileHandler.shared.currentPath,
|
||||
manifestLoader: manifestLoader)
|
||||
|
||||
let path = FileHandler.shared.currentPath.appending(component: "graph.dot")
|
||||
if FileHandler.shared.exists(path) {
|
||||
logger.notice("Deleting existing graph at \(path.pathString)")
|
||||
try FileHandler.shared.delete(path)
|
||||
}
|
||||
|
||||
try FileHandler.shared.write(graph, path: path, atomically: true)
|
||||
logger.notice("Graph exported to \(path.pathString)", metadata: .success)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
import Basic
|
||||
import TuistCore
|
||||
import TuistLoader
|
||||
import TuistScaffold
|
||||
import TuistSupport
|
||||
|
||||
enum InitServiceError: FatalError, Equatable {
|
||||
case ungettableProjectName(AbsolutePath)
|
||||
case nonEmptyDirectory(AbsolutePath)
|
||||
case templateNotFound(String)
|
||||
case templateNotProvided
|
||||
case attributeNotProvided(String)
|
||||
case invalidValue(argument: String, error: String)
|
||||
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .ungettableProjectName, .nonEmptyDirectory, .templateNotFound, .templateNotProvided, .attributeNotProvided, .invalidValue:
|
||||
return .abort
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .templateNotFound(template):
|
||||
return "Could not find template \(template). Make sure it exists at Tuist/Templates/\(template)"
|
||||
case .templateNotProvided:
|
||||
return "You must provide template name"
|
||||
case let .ungettableProjectName(path):
|
||||
return "Couldn't infer the project name from path \(path.pathString)."
|
||||
case let .nonEmptyDirectory(path):
|
||||
return "Can't initialize a project in the non-empty directory at path \(path.pathString)."
|
||||
case let .attributeNotProvided(name):
|
||||
return "You must provide \(name) option. Add --\(name) desired_value to your command."
|
||||
case let .invalidValue(argument: argument, error: error):
|
||||
return "\(error) for argument \(argument); use --help to print usage"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InitService {
|
||||
private let templateLoader: TemplateLoading
|
||||
private let templatesDirectoryLocator: TemplatesDirectoryLocating
|
||||
private let templateGenerator: TemplateGenerating
|
||||
|
||||
init(templateLoader: TemplateLoading = TemplateLoader(),
|
||||
templatesDirectoryLocator: TemplatesDirectoryLocating = TemplatesDirectoryLocator(),
|
||||
templateGenerator: TemplateGenerating = TemplateGenerator()) {
|
||||
self.templateLoader = templateLoader
|
||||
self.templatesDirectoryLocator = templatesDirectoryLocator
|
||||
self.templateGenerator = templateGenerator
|
||||
}
|
||||
|
||||
func loadTemplateOptions(templateName: String,
|
||||
path: String?) throws -> (required: [String],
|
||||
optional: [String]) {
|
||||
let path = self.path(path)
|
||||
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
|
||||
|
||||
let templateDirectory = try self.templateDirectory(templateDirectories: directories,
|
||||
template: templateName)
|
||||
|
||||
let template = try templateLoader.loadTemplate(at: templateDirectory)
|
||||
|
||||
return template.attributes.reduce(into: (required: [], optional: [])) { currentValue, attribute in
|
||||
switch attribute {
|
||||
case let .optional(name, default: _):
|
||||
currentValue.optional.append(name)
|
||||
case let .required(name):
|
||||
currentValue.required.append(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run(name: String?,
|
||||
platform: String?,
|
||||
path: String?,
|
||||
templateName: String?,
|
||||
requiredTemplateOptions: [String: String],
|
||||
optionalTemplateOptions: [String: String?]) throws {
|
||||
let platform = try self.platform(platform)
|
||||
let path = self.path(path)
|
||||
let name = try self.name(name, path: path)
|
||||
try verifyDirectoryIsEmpty(path: path)
|
||||
|
||||
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
|
||||
if let templateName = templateName {
|
||||
guard
|
||||
let templateDirectory = directories.first(where: { $0.basename == templateName })
|
||||
else { throw InitServiceError.templateNotFound(templateName) }
|
||||
let template = try templateLoader.loadTemplate(at: templateDirectory)
|
||||
let parsedAttributes = try parseAttributes(name: name,
|
||||
platform: platform,
|
||||
requiredTemplateOptions: requiredTemplateOptions,
|
||||
optionalTemplateOptions: optionalTemplateOptions,
|
||||
template: template)
|
||||
|
||||
try templateGenerator.generate(template: template,
|
||||
to: path,
|
||||
attributes: parsedAttributes)
|
||||
} else {
|
||||
guard
|
||||
let templateDirectory = directories.first(where: { $0.basename == "default" })
|
||||
else { throw InitServiceError.templateNotFound("default") }
|
||||
let template = try templateLoader.loadTemplate(at: templateDirectory)
|
||||
try templateGenerator.generate(template: template,
|
||||
to: path,
|
||||
attributes: ["name": name, "platform": platform.caseValue])
|
||||
}
|
||||
|
||||
logger.notice("Project generated at path \(path.pathString).", metadata: .success)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
/// Checks if the given directory is empty, essentially that it doesn't contain any file or directory.
|
||||
///
|
||||
/// - Parameter path: Directory to be checked.
|
||||
/// - Throws: An InitServiceError.nonEmptyDirectory error when the directory is not empty.
|
||||
private func verifyDirectoryIsEmpty(path: AbsolutePath) throws {
|
||||
if !path.glob("*").isEmpty {
|
||||
throw InitServiceError.nonEmptyDirectory(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses all `attributes` from `template`
|
||||
/// If those attributes are optional, they default to `default` if not provided
|
||||
/// - Returns: Array of parsed attributes
|
||||
private func parseAttributes(name: String,
|
||||
platform: Platform,
|
||||
requiredTemplateOptions: [String: String],
|
||||
optionalTemplateOptions: [String: String?],
|
||||
template: Template) throws -> [String: String] {
|
||||
try template.attributes.reduce(into: [:]) { attributesDictionary, attribute in
|
||||
if attribute.name == "name" {
|
||||
attributesDictionary[attribute.name] = name
|
||||
return
|
||||
}
|
||||
if attribute.name == "platform" {
|
||||
attributesDictionary[attribute.name] = platform.caseValue
|
||||
return
|
||||
}
|
||||
switch attribute {
|
||||
case let .required(name):
|
||||
guard
|
||||
let option = requiredTemplateOptions[name]
|
||||
else { throw ScaffoldServiceError.attributeNotProvided(name) }
|
||||
attributesDictionary[name] = option
|
||||
case let .optional(name, default: defaultValue):
|
||||
guard
|
||||
let unwrappedOption = optionalTemplateOptions[name],
|
||||
let option = unwrappedOption
|
||||
else {
|
||||
attributesDictionary[name] = defaultValue
|
||||
return
|
||||
}
|
||||
attributesDictionary[name] = option
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds template directory
|
||||
/// - Parameters:
|
||||
/// - templateDirectories: Paths of available templates
|
||||
/// - template: Name of template
|
||||
/// - Returns: `AbsolutePath` of template directory
|
||||
private func templateDirectory(templateDirectories: [AbsolutePath], template: String) throws -> AbsolutePath {
|
||||
guard
|
||||
let templateDirectory = templateDirectories.first(where: { $0.basename == template })
|
||||
else { throw InitServiceError.templateNotFound(template) }
|
||||
return templateDirectory
|
||||
}
|
||||
|
||||
private func name(_ name: String?, path: AbsolutePath) throws -> String {
|
||||
if let name = name {
|
||||
return name
|
||||
} else if let name = path.components.last {
|
||||
return name
|
||||
} else {
|
||||
throw InitServiceError.ungettableProjectName(AbsolutePath.current)
|
||||
}
|
||||
}
|
||||
|
||||
private func path(_ path: String?) -> AbsolutePath {
|
||||
if let path = path {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
}
|
||||
|
||||
private func platform(_ platform: String?) throws -> Platform {
|
||||
if let platformString = platform {
|
||||
if let platform = Platform(rawValue: platformString) {
|
||||
return platform
|
||||
} else {
|
||||
throw InitServiceError.invalidValue(argument: "platform", error: "Platform should be either ios, tvos, or macos")
|
||||
}
|
||||
} else {
|
||||
return .iOS
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import TuistCore
|
||||
import TuistGenerator
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
enum LintServiceError: FatalError, Equatable {
|
||||
/// Thrown when neither a workspace or a project is found in the given path.
|
||||
case manifestNotFound(AbsolutePath)
|
||||
|
||||
/// Error type.
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .manifestNotFound:
|
||||
return .abort
|
||||
}
|
||||
}
|
||||
|
||||
/// Description
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .manifestNotFound(path):
|
||||
return "Couldn't find Project.swift nor Workspace.swift at \(path.pathString)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class LintService {
|
||||
/// Graph linter
|
||||
private let graphLinter: GraphLinting
|
||||
private let environmentLinter: EnvironmentLinting
|
||||
private let manifestLoading: ManifestLoading
|
||||
private let graphLoader: GraphLoading
|
||||
|
||||
init(graphLinter: GraphLinting = GraphLinter(),
|
||||
environmentLinter: EnvironmentLinting = EnvironmentLinter(),
|
||||
manifestLoading: ManifestLoading = ManifestLoader(),
|
||||
graphLoader: GraphLoading = GraphLoader(modelLoader: GeneratorModelLoader(manifestLoader: ManifestLoader(),
|
||||
manifestLinter: AnyManifestLinter()))) {
|
||||
self.graphLinter = graphLinter
|
||||
self.environmentLinter = environmentLinter
|
||||
self.manifestLoading = manifestLoading
|
||||
self.graphLoader = graphLoader
|
||||
}
|
||||
|
||||
func run(path: String?) throws {
|
||||
let path = self.path(path)
|
||||
|
||||
// Load graph
|
||||
let manifests = manifestLoading.manifests(at: path)
|
||||
var graph: Graph!
|
||||
|
||||
logger.notice("Loading the dependency graph")
|
||||
if manifests.contains(.workspace) {
|
||||
logger.notice("Loading workspace at \(path.pathString)")
|
||||
(graph, _) = try graphLoader.loadWorkspace(path: path)
|
||||
} else if manifests.contains(.project) {
|
||||
logger.notice("Loading project at \(path.pathString)")
|
||||
(graph, _) = try graphLoader.loadProject(path: path)
|
||||
} else {
|
||||
throw LintServiceError.manifestNotFound(path)
|
||||
}
|
||||
|
||||
logger.notice("Running linters")
|
||||
let config = try graphLoader.loadConfig(path: path)
|
||||
|
||||
var issues: [LintingIssue] = []
|
||||
logger.notice("Linting the environment")
|
||||
issues.append(contentsOf: try environmentLinter.lint(config: config))
|
||||
logger.notice("Linting the loaded dependency graph")
|
||||
issues.append(contentsOf: graphLinter.lint(graph: graph))
|
||||
|
||||
if issues.isEmpty {
|
||||
logger.notice("No linting issues found", metadata: .success)
|
||||
} else {
|
||||
try issues.printAndThrowIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func path(_ path: String?) -> AbsolutePath {
|
||||
if let path = path {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import TuistLoader
|
||||
import TuistScaffold
|
||||
import TuistSupport
|
||||
|
||||
class ListService {
|
||||
private let templatesDirectoryLocator: TemplatesDirectoryLocating
|
||||
private let templateLoader: TemplateLoading
|
||||
|
||||
init(templatesDirectoryLocator: TemplatesDirectoryLocating = TemplatesDirectoryLocator(),
|
||||
templateLoader: TemplateLoading = TemplateLoader()) {
|
||||
self.templatesDirectoryLocator = templatesDirectoryLocator
|
||||
self.templateLoader = templateLoader
|
||||
}
|
||||
|
||||
func run(path: String?) throws {
|
||||
let path = self.path(path)
|
||||
|
||||
let templateDirectories = try templatesDirectoryLocator.templateDirectories(at: path)
|
||||
|
||||
try templateDirectories.forEach {
|
||||
let template = try templateLoader.loadTemplate(at: $0)
|
||||
logger.info("\($0.basename): \(template.description)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func path(_ path: String?) -> AbsolutePath {
|
||||
if let path = path {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
import Basic
|
||||
import TuistCore
|
||||
import TuistLoader
|
||||
import TuistScaffold
|
||||
import TuistSupport
|
||||
|
||||
enum ScaffoldServiceError: FatalError, Equatable {
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .templateNotFound, .nonEmptyDirectory, .attributeNotProvided:
|
||||
return .abort
|
||||
}
|
||||
}
|
||||
|
||||
case templateNotFound(String)
|
||||
case nonEmptyDirectory(AbsolutePath)
|
||||
case attributeNotProvided(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .templateNotFound(template):
|
||||
return "Could not find template \(template). Make sure it exists at Tuist/Templates/\(template)"
|
||||
case let .nonEmptyDirectory(path):
|
||||
return "Can't generate a template in the non-empty directory at path \(path.pathString)."
|
||||
case let .attributeNotProvided(name):
|
||||
return "You must provide \(name) option. Add --\(name) desired_value to your command."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ScaffoldService {
|
||||
private let templateLoader: TemplateLoading
|
||||
private let templatesDirectoryLocator: TemplatesDirectoryLocating
|
||||
private let templateGenerator: TemplateGenerating
|
||||
|
||||
init(templateLoader: TemplateLoading = TemplateLoader(),
|
||||
templatesDirectoryLocator: TemplatesDirectoryLocating = TemplatesDirectoryLocator(),
|
||||
templateGenerator: TemplateGenerating = TemplateGenerator()) {
|
||||
self.templateLoader = templateLoader
|
||||
self.templatesDirectoryLocator = templatesDirectoryLocator
|
||||
self.templateGenerator = templateGenerator
|
||||
}
|
||||
|
||||
func loadTemplateOptions(templateName: String,
|
||||
path: String?) throws -> (required: [String],
|
||||
optional: [String]) {
|
||||
let path = self.path(path)
|
||||
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
|
||||
|
||||
let templateDirectory = try self.templateDirectory(templateDirectories: directories,
|
||||
template: templateName)
|
||||
|
||||
let template = try templateLoader.loadTemplate(at: templateDirectory)
|
||||
|
||||
return template.attributes.reduce(into: (required: [], optional: [])) { currentValue, attribute in
|
||||
switch attribute {
|
||||
case let .optional(name, default: _):
|
||||
currentValue.optional.append(name)
|
||||
case let .required(name):
|
||||
currentValue.required.append(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run(path: String?,
|
||||
templateName: String,
|
||||
requiredTemplateOptions: [String: String],
|
||||
optionalTemplateOptions: [String: String?]) throws {
|
||||
let path = self.path(path)
|
||||
|
||||
let templateDirectories = try templatesDirectoryLocator.templateDirectories(at: path)
|
||||
|
||||
let templateDirectory = try self.templateDirectory(templateDirectories: templateDirectories,
|
||||
template: templateName)
|
||||
|
||||
let template = try templateLoader.loadTemplate(at: templateDirectory)
|
||||
|
||||
let parsedAttributes = try parseAttributes(requiredTemplateOptions: requiredTemplateOptions,
|
||||
optionalTemplateOptions: optionalTemplateOptions,
|
||||
template: template)
|
||||
|
||||
try templateGenerator.generate(template: template,
|
||||
to: path,
|
||||
attributes: parsedAttributes)
|
||||
|
||||
logger.notice("Template \(templateName) was successfully generated", metadata: .success)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func path(_ path: String?) -> AbsolutePath {
|
||||
if let path = path {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses all `attributes` from `template`
|
||||
/// If those attributes are optional, they default to `default` if not provided
|
||||
/// - Returns: Array of parsed attributes
|
||||
private func parseAttributes(requiredTemplateOptions: [String: String],
|
||||
optionalTemplateOptions: [String: String?],
|
||||
template: Template) throws -> [String: String] {
|
||||
try template.attributes.reduce(into: [:]) { attributesDictionary, attribute in
|
||||
switch attribute {
|
||||
case let .required(name):
|
||||
guard
|
||||
let option = requiredTemplateOptions[name]
|
||||
else { throw ScaffoldServiceError.attributeNotProvided(name) }
|
||||
attributesDictionary[name] = option
|
||||
case let .optional(name, default: defaultValue):
|
||||
guard
|
||||
let unwrappedOption = optionalTemplateOptions[name],
|
||||
let option = unwrappedOption
|
||||
else {
|
||||
attributesDictionary[name] = defaultValue
|
||||
return
|
||||
}
|
||||
attributesDictionary[name] = option
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds template directory
|
||||
/// - Parameters:
|
||||
/// - templateDirectories: Paths of available templates
|
||||
/// - template: Name of template
|
||||
/// - Returns: `AbsolutePath` of template directory
|
||||
private func templateDirectory(templateDirectories: [AbsolutePath], template: String) throws -> AbsolutePath {
|
||||
guard
|
||||
let templateDirectory = templateDirectories.first(where: { $0.basename == template })
|
||||
else { throw ScaffoldServiceError.templateNotFound(template) }
|
||||
return templateDirectory
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import Basic
|
||||
import TuistGenerator
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
||||
final class UpService {
|
||||
// MARK: - Attributes
|
||||
|
||||
/// Instance to load the setup manifest and perform the project setup.
|
||||
private let setupLoader: SetupLoading
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(setupLoader: SetupLoading = SetupLoader()) {
|
||||
self.setupLoader = setupLoader
|
||||
}
|
||||
|
||||
func run(path: String?) throws {
|
||||
let path = self.path(path)
|
||||
try setupLoader.meet(at: path)
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
/// Parses the arguments and returns the path to the directory where
|
||||
/// the up command should be ran.
|
||||
///
|
||||
/// - Parameter path: The path from parsing the command line arguments.
|
||||
/// - Returns: Path to be used for the up command.
|
||||
private func path(_ path: String?) -> AbsolutePath {
|
||||
guard let path = path else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
final class VersionService {
|
||||
func run() throws {
|
||||
logger.notice("\(Constants.version)")
|
||||
}
|
||||
}
|
|
@ -12,28 +12,10 @@ public protocol ErrorHandling: AnyObject {
|
|||
|
||||
/// The default implementation of the ErrorHandling protocol
|
||||
public final class ErrorHandler: ErrorHandling {
|
||||
// MARK: - Attributes
|
||||
|
||||
/// Function to exit the execution of the program.
|
||||
var exiter: (Int32) -> Void
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
/// Default error handler initializer.
|
||||
public convenience init() {
|
||||
self.init(exiter: { exit($0) })
|
||||
}
|
||||
|
||||
/// Default error handler initializer.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - exiter: Closure to exit the execution.
|
||||
init(exiter: @escaping (Int32) -> Void) {
|
||||
self.exiter = exiter
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
public init() {}
|
||||
|
||||
/// When called, this method delegates the error handling
|
||||
/// to the entity that conforms this protocol.
|
||||
///
|
||||
|
@ -49,6 +31,5 @@ public final class ErrorHandler: ErrorHandling {
|
|||
"""
|
||||
logger.error("\(message)")
|
||||
}
|
||||
exiter(1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,5 +7,4 @@ if CommandLine.arguments.contains("--verbose") { try? ProcessEnv.setVar("TUIST_V
|
|||
LogOutput.bootstrap()
|
||||
|
||||
import TuistKit
|
||||
var registry = CommandRegistry()
|
||||
registry.run()
|
||||
TuistCommand.main()
|
||||
|
|
|
@ -4,5 +4,4 @@ import enum TuistSupport.LogOutput
|
|||
|
||||
LogOutput.bootstrap()
|
||||
|
||||
var registry = CommandRegistry()
|
||||
registry.run()
|
||||
TuistCommand.main()
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import Foundation
|
||||
import TuistSupport
|
||||
import XCTest
|
||||
@testable import TuistEnvKit
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class CommandRegistryTests: XCTestCase {
|
||||
var subject: CommandRegistry!
|
||||
var errorHandler: MockErrorHandler!
|
||||
var commandRunner: MockCommandRunner!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
errorHandler = MockErrorHandler()
|
||||
commandRunner = MockCommandRunner()
|
||||
}
|
||||
|
||||
func test_run_calls_the_runner_when_the_command_is_not_found() {
|
||||
setupSubject(arguments: ["tuist", "command"], commands: [])
|
||||
subject.run()
|
||||
XCTAssertEqual(commandRunner.runCallCount, 1)
|
||||
}
|
||||
|
||||
func test_run_calls_the_right_command() {
|
||||
setupSubject(arguments: ["tuist", MockCommand.command], commands: [MockCommand.self])
|
||||
subject.run()
|
||||
XCTAssertEqual((subject.commands.first! as! MockCommand).runCallCount, 1)
|
||||
}
|
||||
|
||||
func test_run_reports_fatal_errors() {
|
||||
commandRunner.runStub = MockFatalError()
|
||||
setupSubject(arguments: ["tuist", "command"], commands: [])
|
||||
subject.run()
|
||||
XCTAssertEqual(errorHandler.fatalErrorArgs.count, 1)
|
||||
}
|
||||
|
||||
func test_run_reports_unhandled_errors() {
|
||||
commandRunner.runStub = NSError(domain: "test", code: 1, userInfo: nil)
|
||||
setupSubject(arguments: ["tuist", "command"], commands: [])
|
||||
subject.run()
|
||||
XCTAssertEqual(errorHandler.fatalErrorArgs.count, 1)
|
||||
XCTAssertTrue(type(of: errorHandler.fatalErrorArgs.first!) == UnhandledError.self)
|
||||
}
|
||||
|
||||
private func setupSubject(arguments: [String], commands: [Command.Type]) {
|
||||
subject = CommandRegistry(processArguments: { arguments },
|
||||
errorHandler: errorHandler,
|
||||
commandRunner: commandRunner,
|
||||
commands: commands)
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import Foundation
|
||||
import TuistSupport
|
||||
import XCTest
|
||||
|
||||
@testable import SPMUtility
|
||||
@testable import TuistEnvKit
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class UpdateCommandTests: TuistUnitTestCase {
|
||||
var parser: ArgumentParser!
|
||||
var subject: UpdateCommand!
|
||||
var updater: MockUpdater!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
parser = ArgumentParser(usage: "test", overview: "overview")
|
||||
updater = MockUpdater()
|
||||
subject = UpdateCommand(parser: parser,
|
||||
updater: updater)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
parser = nil
|
||||
updater = nil
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_command() {
|
||||
XCTAssertEqual(UpdateCommand.command, "update")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(UpdateCommand.overview, "Installs the latest version if it's not already installed")
|
||||
}
|
||||
|
||||
func test_init_registers_the_command() {
|
||||
XCTAssertEqual(parser.subparsers.count, 1)
|
||||
XCTAssertEqual(parser.subparsers.first?.key, UpdateCommand.command)
|
||||
XCTAssertEqual(parser.subparsers.first?.value.overview, UpdateCommand.overview)
|
||||
}
|
||||
|
||||
func test_run() throws {
|
||||
let result = try parser.parse(["update", "-f"])
|
||||
|
||||
var updateCalls: [Bool] = []
|
||||
updater.updateStub = { force in
|
||||
updateCalls.append(force)
|
||||
}
|
||||
|
||||
try subject.run(with: result)
|
||||
|
||||
XCTAssertPrinterOutputContains("Checking for updates...")
|
||||
XCTAssertEqual(updateCalls, [true])
|
||||
}
|
||||
}
|
|
@ -1,43 +1,38 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import SPMUtility
|
||||
@testable import TuistEnvKit
|
||||
@testable import TuistSupport
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class BundleCommandErrorTests: XCTestCase {
|
||||
final class BundleServiceErrorTests: XCTestCase {
|
||||
func test_type() {
|
||||
let path = AbsolutePath("/test")
|
||||
XCTAssertEqual(BundleCommandError.missingVersionFile(path).type, .abort)
|
||||
XCTAssertEqual(BundleServiceError.missingVersionFile(path).type, .abort)
|
||||
}
|
||||
|
||||
func test_description() {
|
||||
let path = AbsolutePath("/test")
|
||||
XCTAssertEqual(BundleCommandError.missingVersionFile(path).description, "Couldn't find a .tuist-version file in the directory \(path.pathString)")
|
||||
XCTAssertEqual(BundleServiceError.missingVersionFile(path).description, "Couldn't find a .tuist-version file in the directory \(path.pathString)")
|
||||
}
|
||||
}
|
||||
|
||||
final class BundleCommandTests: TuistUnitTestCase {
|
||||
var parser: ArgumentParser!
|
||||
final class BundleServiceTests: TuistUnitTestCase {
|
||||
var versionsController: MockVersionsController!
|
||||
var installer: MockInstaller!
|
||||
var subject: BundleCommand!
|
||||
var subject: BundleService!
|
||||
var tmpDir: TemporaryDirectory!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
parser = ArgumentParser(usage: "test", overview: "overview")
|
||||
versionsController = try! MockVersionsController()
|
||||
installer = MockInstaller()
|
||||
tmpDir = try! TemporaryDirectory(removeTreeOnDeinit: true)
|
||||
subject = BundleCommand(parser: parser,
|
||||
versionsController: versionsController,
|
||||
subject = BundleService(versionsController: versionsController,
|
||||
installer: installer)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
parser = nil
|
||||
versionsController = nil
|
||||
installer = nil
|
||||
subject = nil
|
||||
|
@ -45,29 +40,13 @@ final class BundleCommandTests: TuistUnitTestCase {
|
|||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_init_registers_the_command() {
|
||||
XCTAssertEqual(parser.subparsers.count, 1)
|
||||
XCTAssertEqual(parser.subparsers.first?.key, BundleCommand.command)
|
||||
XCTAssertEqual(parser.subparsers.first?.value.overview, BundleCommand.overview)
|
||||
}
|
||||
|
||||
func test_command() {
|
||||
XCTAssertEqual(BundleCommand.command, "bundle")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(BundleCommand.overview, "Bundles the version specified in the .tuist-version file into the .tuist-bin directory")
|
||||
}
|
||||
|
||||
func test_run_throws_when_there_is_no_xmp_version_in_the_directory() throws {
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let result = try parser.parse([])
|
||||
XCTAssertThrowsSpecific(try subject.run(with: result), BundleCommandError.missingVersionFile(temporaryPath))
|
||||
XCTAssertThrowsSpecific(try subject.run(), BundleServiceError.missingVersionFile(temporaryPath))
|
||||
}
|
||||
|
||||
func test_run_installs_the_app_if_it_doesnt_exist() throws {
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let result = try parser.parse([])
|
||||
let tuistVersionPath = temporaryPath.appending(component: Constants.versionFileName)
|
||||
try "3.2.1".write(to: tuistVersionPath.url, atomically: true, encoding: .utf8)
|
||||
|
||||
|
@ -77,7 +56,7 @@ final class BundleCommandTests: TuistUnitTestCase {
|
|||
try Data().write(to: versionPath.appending(component: "test").url)
|
||||
}
|
||||
|
||||
try subject.run(with: result)
|
||||
try subject.run()
|
||||
|
||||
let bundledTestFilePath = temporaryPath
|
||||
.appending(component: Constants.binFolderName)
|
||||
|
@ -89,19 +68,17 @@ final class BundleCommandTests: TuistUnitTestCase {
|
|||
func test_run_doesnt_install_the_app_if_it_already_exists() throws {
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
|
||||
let result = try parser.parse([])
|
||||
let tuistVersionPath = temporaryPath.appending(component: Constants.versionFileName)
|
||||
try "3.2.1".write(to: tuistVersionPath.url, atomically: true, encoding: .utf8)
|
||||
let versionPath = versionsController.path(version: "3.2.1")
|
||||
try FileHandler.shared.createFolder(versionPath)
|
||||
|
||||
try subject.run(with: result)
|
||||
try subject.run()
|
||||
|
||||
XCTAssertEqual(installer.installCallCount, 0)
|
||||
}
|
||||
|
||||
func test_run_prints_the_right_messages() throws {
|
||||
let result = try parser.parse([])
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let tuistVersionPath = temporaryPath.appending(component: Constants.versionFileName)
|
||||
let binPath = temporaryPath.appending(component: Constants.binFolderName)
|
||||
|
@ -114,7 +91,7 @@ final class BundleCommandTests: TuistUnitTestCase {
|
|||
try Data().write(to: versionPath.appending(component: "test").url)
|
||||
}
|
||||
|
||||
try subject.run(with: result)
|
||||
try subject.run()
|
||||
|
||||
XCTAssertPrinterOutputContains("""
|
||||
Bundling the version 3.2.1 in the directory \(binPath.pathString)
|
|
@ -2,29 +2,24 @@ import Basic
|
|||
import Foundation
|
||||
import TuistSupport
|
||||
import XCTest
|
||||
@testable import SPMUtility
|
||||
@testable import TuistEnvKit
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class InstallCommandTests: TuistUnitTestCase {
|
||||
var parser: ArgumentParser!
|
||||
final class InstallServiceTests: TuistUnitTestCase {
|
||||
var versionsController: MockVersionsController!
|
||||
var installer: MockInstaller!
|
||||
var subject: InstallCommand!
|
||||
var subject: InstallService!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
parser = ArgumentParser(usage: "test", overview: "overview")
|
||||
versionsController = try! MockVersionsController()
|
||||
installer = MockInstaller()
|
||||
subject = InstallCommand(parser: parser,
|
||||
versionsController: versionsController,
|
||||
subject = InstallService(versionsController: versionsController,
|
||||
installer: installer)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
parser = nil
|
||||
versionsController = nil
|
||||
installer = nil
|
||||
subject = nil
|
||||
|
@ -32,39 +27,21 @@ final class InstallCommandTests: TuistUnitTestCase {
|
|||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_command() {
|
||||
XCTAssertEqual(InstallCommand.command, "install")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(InstallCommand.overview, "Installs a version of tuist")
|
||||
}
|
||||
|
||||
func test_init_registers_the_command() {
|
||||
XCTAssertEqual(parser.subparsers.count, 1)
|
||||
XCTAssertEqual(parser.subparsers.first?.key, InstallCommand.command)
|
||||
XCTAssertEqual(parser.subparsers.first?.value.overview, InstallCommand.overview)
|
||||
}
|
||||
|
||||
func test_run_when_version_is_already_installed() throws {
|
||||
let result = try parser.parse(["install", "3.2.1"])
|
||||
|
||||
versionsController.versionsStub = [InstalledVersion.reference("3.2.1")]
|
||||
|
||||
try subject.run(with: result)
|
||||
try subject.run(version: "3.2.1", force: false)
|
||||
|
||||
XCTAssertPrinterOutputContains("Version 3.2.1 already installed, skipping")
|
||||
}
|
||||
|
||||
func test_run() throws {
|
||||
let result = try parser.parse(["install", "3.2.1"])
|
||||
|
||||
versionsController.versionsStub = []
|
||||
|
||||
var installArgs: [(version: String, force: Bool)] = []
|
||||
installer.installStub = { version, force in installArgs.append((version: version, force: force)) }
|
||||
|
||||
try subject.run(with: result)
|
||||
try subject.run(version: "3.2.1", force: false)
|
||||
|
||||
XCTAssertEqual(installArgs.count, 1)
|
||||
XCTAssertEqual(installArgs.first?.version, "3.2.1")
|
||||
|
@ -72,14 +49,12 @@ final class InstallCommandTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
func test_run_when_force() throws {
|
||||
let result = try parser.parse(["install", "3.2.1", "-f"])
|
||||
|
||||
versionsController.versionsStub = []
|
||||
|
||||
var installArgs: [(version: String, force: Bool)] = []
|
||||
installer.installStub = { version, force in installArgs.append((version: version, force: force)) }
|
||||
|
||||
try subject.run(with: result)
|
||||
try subject.run(version: "3.2.1", force: true)
|
||||
|
||||
XCTAssertEqual(installArgs.count, 1)
|
||||
XCTAssertEqual(installArgs.first?.version, "3.2.1")
|
|
@ -6,56 +6,44 @@ import XCTest
|
|||
@testable import TuistEnvKit
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class LocalCommandTests: TuistUnitTestCase {
|
||||
var argumentParser: ArgumentParser!
|
||||
var subject: LocalCommand!
|
||||
final class LocalServiceTests: TuistUnitTestCase {
|
||||
var subject: LocalService!
|
||||
var versionController: MockVersionsController!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
argumentParser = ArgumentParser(usage: "test", overview: "overview")
|
||||
versionController = try! MockVersionsController()
|
||||
subject = LocalCommand(parser: argumentParser, versionController: versionController)
|
||||
subject = LocalService(versionController: versionController)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
argumentParser = nil
|
||||
subject = nil
|
||||
versionController = nil
|
||||
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_command() {
|
||||
XCTAssertEqual(LocalCommand.command, "local")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(LocalCommand.overview, "Creates a .tuist-version file to pin the tuist version that should be used in the current directory. If the version is not specified, it prints the local versions")
|
||||
}
|
||||
|
||||
func test_init_registers_the_command() {
|
||||
XCTAssertEqual(argumentParser.subparsers.count, 1)
|
||||
XCTAssertEqual(argumentParser.subparsers.first?.key, LocalCommand.command)
|
||||
XCTAssertEqual(argumentParser.subparsers.first?.value.overview, LocalCommand.overview)
|
||||
}
|
||||
|
||||
func test_run_when_version_argument_is_passed() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let result = try argumentParser.parse(["local", "3.2.1"])
|
||||
try subject.run(with: result)
|
||||
|
||||
// When
|
||||
try subject.run(version: "3.2.1")
|
||||
|
||||
// Then
|
||||
let versionPath = temporaryPath.appending(component: Constants.versionFileName)
|
||||
|
||||
XCTAssertEqual(try String(contentsOf: versionPath.url), "3.2.1")
|
||||
}
|
||||
|
||||
func test_run_prints_when_version_argument_is_passed() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let result = try argumentParser.parse(["local", "3.2.1"])
|
||||
try subject.run(with: result)
|
||||
|
||||
// When
|
||||
try subject.run(version: "3.2.1")
|
||||
|
||||
// Then
|
||||
let versionPath = temporaryPath.appending(component: Constants.versionFileName)
|
||||
|
||||
XCTAssertPrinterOutputContains("""
|
||||
|
@ -65,10 +53,13 @@ final class LocalCommandTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
func test_run_prints_when_no_argument_is_passed() throws {
|
||||
let result = try argumentParser.parse(["local"])
|
||||
// Given
|
||||
versionController.semverVersionsStub = [Version(string: "1.2.3")!, Version(string: "3.2.1")!]
|
||||
try subject.run(with: result)
|
||||
|
||||
// When
|
||||
try subject.run(version: nil)
|
||||
|
||||
// Then
|
||||
XCTAssertPrinterOutputContains("""
|
||||
The following versions are available in the local environment:
|
||||
- 3.2.1
|
|
@ -2,80 +2,55 @@ import Basic
|
|||
import Foundation
|
||||
import TuistSupport
|
||||
import XCTest
|
||||
@testable import SPMUtility
|
||||
@testable import TuistEnvKit
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class UninstallCommandTests: TuistUnitTestCase {
|
||||
var parser: ArgumentParser!
|
||||
final class UninstallServiceTests: TuistUnitTestCase {
|
||||
var versionsController: MockVersionsController!
|
||||
var installer: MockInstaller!
|
||||
var subject: UninstallCommand!
|
||||
var subject: UninstallService!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
parser = ArgumentParser(usage: "test", overview: "overview")
|
||||
versionsController = try! MockVersionsController()
|
||||
installer = MockInstaller()
|
||||
subject = UninstallCommand(parser: parser,
|
||||
versionsController: versionsController,
|
||||
subject = UninstallService(versionsController: versionsController,
|
||||
installer: installer)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
parser = nil
|
||||
versionsController = nil
|
||||
installer = nil
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_command() {
|
||||
XCTAssertEqual(UninstallCommand.command, "uninstall")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(UninstallCommand.overview, "Uninstalls a version of tuist")
|
||||
}
|
||||
|
||||
func test_init_registers_the_command() {
|
||||
XCTAssertEqual(parser.subparsers.count, 1)
|
||||
XCTAssertEqual(parser.subparsers.first?.key, UninstallCommand.command)
|
||||
XCTAssertEqual(parser.subparsers.first?.value.overview, UninstallCommand.overview)
|
||||
}
|
||||
|
||||
func test_run_when_version_is_installed() throws {
|
||||
let result = try parser.parse(["uninstall", "3.2.1"])
|
||||
|
||||
versionsController.versionsStub = [InstalledVersion.reference("3.2.1")]
|
||||
var uninstalledVersion: String?
|
||||
versionsController.uninstallStub = { uninstalledVersion = $0 }
|
||||
|
||||
try subject.run(with: result)
|
||||
try subject.run(version: "3.2.1")
|
||||
|
||||
XCTAssertPrinterOutputContains("Version 3.2.1 uninstalled")
|
||||
XCTAssertEqual(uninstalledVersion, "3.2.1")
|
||||
}
|
||||
|
||||
func test_run_when_version_is_installed_and_throws() throws {
|
||||
let result = try parser.parse(["uninstall", "3.2.1"])
|
||||
|
||||
versionsController.versionsStub = [InstalledVersion.reference("3.2.1")]
|
||||
|
||||
let error = NSError.test()
|
||||
versionsController.uninstallStub = { _ in throw error }
|
||||
|
||||
XCTAssertThrowsError(try subject.run(with: result)) {
|
||||
XCTAssertThrowsError(try subject.run(version: "3.2.1")) {
|
||||
XCTAssertEqual($0 as NSError, error)
|
||||
}
|
||||
}
|
||||
|
||||
func test_run_when_version_is_not_installed() throws {
|
||||
let result = try parser.parse(["uninstall", "3.2.1"])
|
||||
|
||||
versionsController.versionsStub = []
|
||||
|
||||
try subject.run(with: result)
|
||||
try subject.run(version: "3.2.1")
|
||||
|
||||
XCTAssertPrinterOutputContains("Version 3.2.1 cannot be uninstalled because it's not installed")
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import Foundation
|
||||
import TuistSupport
|
||||
import XCTest
|
||||
|
||||
@testable import TuistEnvKit
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class UpdateServiceTests: TuistUnitTestCase {
|
||||
var subject: UpdateService!
|
||||
var updater: MockUpdater!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
updater = MockUpdater()
|
||||
subject = UpdateService(updater: updater)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
updater = nil
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_run() throws {
|
||||
var updateCalls: [Bool] = []
|
||||
updater.updateStub = { force in
|
||||
updateCalls.append(force)
|
||||
}
|
||||
|
||||
try subject.run(force: true)
|
||||
|
||||
XCTAssertPrinterOutputContains("Checking for updates...")
|
||||
XCTAssertEqual(updateCalls, [true])
|
||||
}
|
||||
}
|
|
@ -8,24 +8,20 @@ import XCTest
|
|||
@testable import TuistLoader
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class DumpCommandTests: TuistTestCase {
|
||||
final class DumpServiceTests: TuistTestCase {
|
||||
var errorHandler: MockErrorHandler!
|
||||
var subject: DumpCommand!
|
||||
var parser: ArgumentParser!
|
||||
var subject: DumpService!
|
||||
var manifestLoading: ManifestLoading!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
errorHandler = MockErrorHandler()
|
||||
parser = ArgumentParser.test()
|
||||
manifestLoading = ManifestLoader()
|
||||
subject = DumpCommand(manifestLoader: manifestLoading,
|
||||
parser: parser)
|
||||
subject = DumpService(manifestLoader: manifestLoading)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
errorHandler = nil
|
||||
parser = nil
|
||||
manifestLoading = nil
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
|
@ -44,8 +40,7 @@ final class DumpCommandTests: TuistTestCase {
|
|||
try config.write(toFile: tmpDir.path.appending(component: "Project.swift").pathString,
|
||||
atomically: true,
|
||||
encoding: .utf8)
|
||||
let result = try parser.parse([DumpCommand.command, "-p", tmpDir.path.pathString])
|
||||
try subject.run(with: result)
|
||||
try subject.run(path: tmpDir.path.pathString)
|
||||
let expected = "{\n \"additionalFiles\": [\n\n ],\n \"name\": \"tuist\",\n \"organizationName\": \"tuist\",\n \"packages\": [\n\n ],\n \"schemes\": [\n\n ],\n \"targets\": [\n\n ]\n}\n"
|
||||
|
||||
XCTAssertPrinterOutputContains(expected)
|
|
@ -1,14 +0,0 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import TuistKit
|
||||
|
||||
final class BuildCommandTests: XCTestCase {
|
||||
func test_command() {
|
||||
XCTAssertEqual(BuildCommand.command, "build")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(BuildCommand.overview, "Builds a project target.")
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
import XCTest
|
||||
|
||||
@testable import TuistKit
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class CacheCommandTests: TuistUnitTestCase {
|
||||
var subject: CacheCommand!
|
||||
var parser: ArgumentParser!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
parser = ArgumentParser.test()
|
||||
subject = CacheCommand(parser: parser)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
parser = nil
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_name() {
|
||||
XCTAssertEqual(CacheCommand.command, "cache")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(CacheCommand.overview, "Cache frameworks as .xcframeworks to speed up build times in generated projects")
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import Foundation
|
||||
import SPMUtility
|
||||
import XCTest
|
||||
@testable import TuistKit
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class CreateIssueCommandTests: TuistUnitTestCase {
|
||||
var subject: CreateIssueCommand!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
let parser = ArgumentParser.test()
|
||||
|
||||
subject = CreateIssueCommand(parser: parser)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_command() {
|
||||
XCTAssertEqual(CreateIssueCommand.command, "create-issue")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(CreateIssueCommand.overview, "Opens the GitHub page to create a new issue.")
|
||||
}
|
||||
|
||||
func test_run() throws {
|
||||
system.succeedCommand("/usr/bin/open", CreateIssueCommand.createIssueUrl)
|
||||
try subject.run(with: ArgumentParser.Result.test())
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import XcodeProj
|
||||
import XCTest
|
||||
@testable import TuistKit
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class EditCommandTests: TuistUnitTestCase {
|
||||
func test_command() {
|
||||
XCTAssertEqual(EditCommand.command, "edit")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(EditCommand.overview, "Generates a temporary project to edit the project in the current directory")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import Foundation
|
||||
import SPMUtility
|
||||
import XCTest
|
||||
@testable import TuistKit
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class CreateIssueServiceTests: TuistUnitTestCase {
|
||||
var subject: CreateIssueService!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
subject = CreateIssueService()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_run() throws {
|
||||
system.succeedCommand("/usr/bin/open", CreateIssueService.createIssueUrl)
|
||||
try subject.run()
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
import XCTest
|
||||
|
@ -9,41 +8,28 @@ import XCTest
|
|||
@testable import TuistLoaderTesting
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class DumpCommandTests: TuistUnitTestCase {
|
||||
final class DumpServiceTests: TuistUnitTestCase {
|
||||
var errorHandler: MockErrorHandler!
|
||||
var subject: DumpCommand!
|
||||
var parser: ArgumentParser!
|
||||
var subject: DumpService!
|
||||
var manifestLoading: ManifestLoading!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
errorHandler = MockErrorHandler()
|
||||
parser = ArgumentParser.test()
|
||||
manifestLoading = ManifestLoader()
|
||||
subject = DumpCommand(manifestLoader: manifestLoading,
|
||||
parser: parser)
|
||||
subject = DumpService(manifestLoader: manifestLoading)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
errorHandler = nil
|
||||
parser = nil
|
||||
manifestLoading = nil
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_name() {
|
||||
XCTAssertEqual(DumpCommand.command, "dump")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(DumpCommand.overview, "Outputs the project manifest as a JSON")
|
||||
}
|
||||
|
||||
func test_run_throws_when_file_doesnt_exist() throws {
|
||||
let tmpDir = try TemporaryDirectory(removeTreeOnDeinit: true)
|
||||
let result = try parser.parse([DumpCommand.command, "-p", tmpDir.path.pathString])
|
||||
XCTAssertThrowsSpecific(try subject.run(with: result),
|
||||
XCTAssertThrowsSpecific(try subject.run(path: tmpDir.path.pathString),
|
||||
ManifestLoaderError.manifestNotFound(.project, tmpDir.path))
|
||||
}
|
||||
|
||||
|
@ -52,7 +38,6 @@ final class DumpCommandTests: TuistUnitTestCase {
|
|||
try "invalid config".write(toFile: tmpDir.path.appending(component: "Project.swift").pathString,
|
||||
atomically: true,
|
||||
encoding: .utf8)
|
||||
let result = try parser.parse([DumpCommand.command, "-p", tmpDir.path.pathString])
|
||||
XCTAssertThrowsError(try subject.run(with: result))
|
||||
XCTAssertThrowsError(try subject.run(path: tmpDir.path.pathString))
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistLoader
|
||||
import XcodeProj
|
||||
|
@ -10,57 +9,43 @@ import XCTest
|
|||
@testable import TuistLoaderTesting
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class FocusCommandTests: TuistUnitTestCase {
|
||||
var subject: FocusCommand!
|
||||
var parser: ArgumentParser!
|
||||
final class FocusServiceTests: TuistUnitTestCase {
|
||||
var subject: FocusService!
|
||||
var opener: MockOpener!
|
||||
var generator: MockProjectGenerator!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
parser = ArgumentParser.test()
|
||||
opener = MockOpener()
|
||||
generator = MockProjectGenerator()
|
||||
|
||||
subject = FocusCommand(parser: parser,
|
||||
generator: generator,
|
||||
subject = FocusService(generator: generator,
|
||||
opener: opener)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
parser = nil
|
||||
opener = nil
|
||||
generator = nil
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_command() {
|
||||
XCTAssertEqual(FocusCommand.command, "focus")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(FocusCommand.overview, "Opens Xcode ready to focus on the project in the current directory.")
|
||||
}
|
||||
|
||||
func test_run_fatalErrors_when_theworkspaceGenerationFails() throws {
|
||||
let result = try parser.parse([FocusCommand.command])
|
||||
let error = NSError.test()
|
||||
generator.generateStub = { _, _ in
|
||||
throw error
|
||||
}
|
||||
XCTAssertThrowsError(try subject.run(with: result)) {
|
||||
XCTAssertThrowsError(try subject.run()) {
|
||||
XCTAssertEqual($0 as NSError?, error)
|
||||
}
|
||||
}
|
||||
|
||||
func test_run() throws {
|
||||
let result = try parser.parse([FocusCommand.command])
|
||||
let workspacePath = AbsolutePath("/test.xcworkspace")
|
||||
generator.generateStub = { _, _ in
|
||||
workspacePath
|
||||
}
|
||||
try subject.run(with: result)
|
||||
try subject.run()
|
||||
|
||||
XCTAssertEqual(opener.openArgs.last?.0, workspacePath.pathString)
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
|
@ -11,48 +10,33 @@ import XCTest
|
|||
@testable import TuistLoaderTesting
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class GenerateCommandTests: TuistUnitTestCase {
|
||||
var subject: GenerateCommand!
|
||||
final class GenerateServiceTests: TuistUnitTestCase {
|
||||
var subject: GenerateService!
|
||||
var generator: MockProjectGenerator!
|
||||
var parser: ArgumentParser!
|
||||
var clock: StubClock!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
generator = MockProjectGenerator()
|
||||
parser = ArgumentParser.test()
|
||||
clock = StubClock()
|
||||
generator.generateStub = { _, _ in
|
||||
AbsolutePath("/Test")
|
||||
}
|
||||
|
||||
subject = GenerateCommand(parser: parser,
|
||||
generator: generator,
|
||||
subject = GenerateService(generator: generator,
|
||||
clock: clock)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
generator = nil
|
||||
parser = nil
|
||||
clock = nil
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_command() {
|
||||
XCTAssertEqual(GenerateCommand.command, "generate")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(GenerateCommand.overview, "Generates an Xcode workspace to start working on the project.")
|
||||
}
|
||||
|
||||
func test_run() throws {
|
||||
// Given
|
||||
let result = try parser.parse([GenerateCommand.command])
|
||||
|
||||
// When
|
||||
try subject.run(with: result)
|
||||
try subject.testRun()
|
||||
|
||||
// Then
|
||||
XCTAssertPrinterOutputContains("Project generated.")
|
||||
|
@ -60,14 +44,13 @@ final class GenerateCommandTests: TuistUnitTestCase {
|
|||
|
||||
func test_run_timeIsPrinted() throws {
|
||||
// Given
|
||||
let result = try parser.parse([GenerateCommand.command])
|
||||
clock.assertOnUnexpectedCalls = true
|
||||
clock.primedTimers = [
|
||||
0.234,
|
||||
]
|
||||
|
||||
// When
|
||||
try subject.run(with: result)
|
||||
try subject.testRun()
|
||||
|
||||
// Then
|
||||
XCTAssertPrinterOutputContains("Total time taken: 0.234s")
|
||||
|
@ -76,7 +59,6 @@ final class GenerateCommandTests: TuistUnitTestCase {
|
|||
func test_run_withRelativePathParameter() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let result = try parser.parse([GenerateCommand.command, "--path", "subpath"])
|
||||
var generationPath: AbsolutePath?
|
||||
generator.generateStub = { path, _ in
|
||||
generationPath = path
|
||||
|
@ -84,7 +66,7 @@ final class GenerateCommandTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
// When
|
||||
try subject.run(with: result)
|
||||
try subject.testRun(path: "subpath")
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(generationPath, AbsolutePath("subpath", relativeTo: temporaryPath))
|
||||
|
@ -92,7 +74,6 @@ final class GenerateCommandTests: TuistUnitTestCase {
|
|||
|
||||
func test_run_withAbsoultePathParameter() throws {
|
||||
// Given
|
||||
let result = try parser.parse([GenerateCommand.command, "--path", "/some/path"])
|
||||
var generationPath: AbsolutePath?
|
||||
generator.generateStub = { path, _ in
|
||||
generationPath = path
|
||||
|
@ -100,7 +81,7 @@ final class GenerateCommandTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
// When
|
||||
try subject.run(with: result)
|
||||
try subject.testRun(path: "/some/path")
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(generationPath, AbsolutePath("/some/path"))
|
||||
|
@ -109,7 +90,6 @@ final class GenerateCommandTests: TuistUnitTestCase {
|
|||
func test_run_withoutPathParameter() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let result = try parser.parse([GenerateCommand.command])
|
||||
var generationPath: AbsolutePath?
|
||||
generator.generateStub = { path, _ in
|
||||
generationPath = path
|
||||
|
@ -117,7 +97,7 @@ final class GenerateCommandTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
// When
|
||||
try subject.run(with: result)
|
||||
try subject.testRun()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(generationPath, temporaryPath)
|
||||
|
@ -125,15 +105,11 @@ final class GenerateCommandTests: TuistUnitTestCase {
|
|||
|
||||
func test_run_withProjectOnlyParameter() throws {
|
||||
// Given
|
||||
let arguments = [
|
||||
[GenerateCommand.command, "--project-only"],
|
||||
[GenerateCommand.command],
|
||||
]
|
||||
let projectOnlyValues = [true, false]
|
||||
|
||||
// When
|
||||
try arguments.forEach {
|
||||
let result = try parser.parse($0)
|
||||
try subject.run(with: result)
|
||||
try projectOnlyValues.forEach {
|
||||
try subject.testRun(projectOnly: $0)
|
||||
}
|
||||
|
||||
// Then
|
||||
|
@ -145,15 +121,22 @@ final class GenerateCommandTests: TuistUnitTestCase {
|
|||
|
||||
func test_run_fatalErrors_when_theworkspaceGenerationFails() throws {
|
||||
// Given
|
||||
let result = try parser.parse([GenerateCommand.command])
|
||||
let error = NSError.test()
|
||||
generator.generateStub = { _, _ in
|
||||
throw error
|
||||
}
|
||||
|
||||
// When / Then
|
||||
XCTAssertThrowsError(try subject.run(with: result)) {
|
||||
XCTAssertThrowsError(try subject.testRun()) {
|
||||
XCTAssertEqual($0 as NSError, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension GenerateService {
|
||||
func testRun(path: String? = nil,
|
||||
projectOnly: Bool = false) throws {
|
||||
try run(path: path,
|
||||
projectOnly: projectOnly)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistSupport
|
||||
import XcodeProj
|
||||
import XCTest
|
||||
|
@ -9,38 +8,26 @@ import XCTest
|
|||
@testable import TuistLoaderTesting
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class GraphCommandTests: TuistUnitTestCase {
|
||||
var subject: GraphCommand!
|
||||
final class GraphServiceTests: TuistUnitTestCase {
|
||||
var subject: GraphService!
|
||||
var dotGraphGenerator: MockDotGraphGenerator!
|
||||
var manifestLoader: MockManifestLoader!
|
||||
var parser: ArgumentParser!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
dotGraphGenerator = MockDotGraphGenerator()
|
||||
manifestLoader = MockManifestLoader()
|
||||
parser = ArgumentParser.test()
|
||||
subject = GraphCommand(parser: parser,
|
||||
dotGraphGenerator: dotGraphGenerator,
|
||||
subject = GraphService(dotGraphGenerator: dotGraphGenerator,
|
||||
manifestLoader: manifestLoader)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
dotGraphGenerator = nil
|
||||
manifestLoader = nil
|
||||
parser = nil
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_command() {
|
||||
XCTAssertEqual(GraphCommand.command, "graph")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(GraphCommand.overview, "Generates a dot graph from the workspace or project in the current directory.")
|
||||
}
|
||||
|
||||
func test_run() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
|
@ -59,8 +46,7 @@ final class GraphCommandTests: TuistUnitTestCase {
|
|||
dotGraphGenerator.generateProjectStub = graph
|
||||
|
||||
// When
|
||||
let result = try parser.parse([GraphCommand.command])
|
||||
try subject.run(with: result)
|
||||
try subject.run()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(try FileHandler.shared.readTextFile(graphPath), graph)
|
|
@ -1,6 +1,5 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistScaffold
|
||||
import TuistSupport
|
||||
import XCTest
|
||||
|
@ -10,27 +9,23 @@ import XCTest
|
|||
@testable import TuistScaffoldTesting
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class InitCommandTests: TuistUnitTestCase {
|
||||
var subject: InitCommand!
|
||||
var parser: ArgumentParser!
|
||||
final class InitServiceTests: TuistUnitTestCase {
|
||||
var subject: InitService!
|
||||
var templatesDirectoryLocator: MockTemplatesDirectoryLocator!
|
||||
var templateGenerator: MockTemplateGenerator!
|
||||
var templateLoader: MockTemplateLoader!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
parser = ArgumentParser.test()
|
||||
templatesDirectoryLocator = MockTemplatesDirectoryLocator()
|
||||
templateGenerator = MockTemplateGenerator()
|
||||
templateLoader = MockTemplateLoader()
|
||||
subject = InitCommand(parser: parser,
|
||||
subject = InitService(templateLoader: templateLoader,
|
||||
templatesDirectoryLocator: templatesDirectoryLocator,
|
||||
templateGenerator: templateGenerator,
|
||||
templateLoader: templateLoader)
|
||||
templateGenerator: templateGenerator)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
parser = nil
|
||||
subject = nil
|
||||
templatesDirectoryLocator = nil
|
||||
templateGenerator = nil
|
||||
|
@ -38,29 +33,18 @@ final class InitCommandTests: TuistUnitTestCase {
|
|||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_name() {
|
||||
XCTAssertEqual(InitCommand.command, "init")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(InitCommand.overview, "Bootstraps a project.")
|
||||
}
|
||||
|
||||
func test_fails_when_directory_not_empty() throws {
|
||||
// Given
|
||||
let path = FileHandler.shared.currentPath
|
||||
try FileHandler.shared.touch(path.appending(component: "dummy"))
|
||||
|
||||
let result = try parser.parse([InitCommand.command])
|
||||
|
||||
// Then
|
||||
XCTAssertThrowsSpecific(try subject.run(with: result), InitCommandError.nonEmptyDirectory(path))
|
||||
XCTAssertThrowsSpecific(try subject.testRun(), InitServiceError.nonEmptyDirectory(path))
|
||||
}
|
||||
|
||||
func test_init_fails_when_template_not_found() throws {
|
||||
let templateName = "template"
|
||||
let result = try parser.parse([InitCommand.command, "--template", templateName])
|
||||
XCTAssertThrowsSpecific(try subject.run(with: result), InitCommandError.templateNotFound(templateName))
|
||||
XCTAssertThrowsSpecific(try subject.testRun(templateName: templateName), InitServiceError.templateNotFound(templateName))
|
||||
}
|
||||
|
||||
func test_init_default_when_no_template() throws {
|
||||
|
@ -70,35 +54,50 @@ final class InitCommandTests: TuistUnitTestCase {
|
|||
[defaultTemplatePath]
|
||||
}
|
||||
let expectedAttributes = ["name": "name", "platform": "macOS"]
|
||||
let result = try parser.parse([InitCommand.command, "--name", "name", "--platform", "macos"])
|
||||
var generatorAttributes: [String: String] = [:]
|
||||
templateGenerator.generateStub = { _, _, attributes in
|
||||
generatorAttributes = attributes
|
||||
}
|
||||
|
||||
// When
|
||||
try subject.run(with: result)
|
||||
try subject.testRun(name: "name", platform: "macos")
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(expectedAttributes, generatorAttributes)
|
||||
}
|
||||
|
||||
func test_init_default_platform() throws {
|
||||
// Given
|
||||
let defaultTemplatePath = try temporaryPath().appending(component: "default")
|
||||
templatesDirectoryLocator.templateDirectoriesStub = { _ in
|
||||
[defaultTemplatePath]
|
||||
}
|
||||
let expectedAttributes = ["name": "name", "platform": "iOS"]
|
||||
let result = try parser.parse([InitCommand.command, "--name", "name"])
|
||||
var generatorAttributes: [String: String] = [:]
|
||||
templateGenerator.generateStub = { _, _, attributes in
|
||||
generatorAttributes = attributes
|
||||
}
|
||||
|
||||
// When
|
||||
try subject.run(with: result)
|
||||
try subject.testRun(name: "name")
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(expectedAttributes, generatorAttributes)
|
||||
}
|
||||
}
|
||||
|
||||
extension InitService {
|
||||
func testRun(name: String? = nil,
|
||||
platform: String? = nil,
|
||||
path: String? = nil,
|
||||
templateName: String? = nil,
|
||||
requiredTemplateOptions: [String: String] = [:],
|
||||
optionalTemplateOptions: [String: String?] = [:]) throws {
|
||||
try run(name: name,
|
||||
platform: platform,
|
||||
path: path,
|
||||
templateName: templateName,
|
||||
requiredTemplateOptions: requiredTemplateOptions,
|
||||
optionalTemplateOptions: optionalTemplateOptions)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import XCTest
|
||||
|
||||
|
@ -10,25 +9,22 @@ import XCTest
|
|||
@testable import TuistLoaderTesting
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class LintCommandTests: TuistUnitTestCase {
|
||||
var parser: ArgumentParser!
|
||||
final class LintServiceTests: TuistUnitTestCase {
|
||||
var graphLinter: MockGraphLinter!
|
||||
var environmentLinter: MockEnvironmentLinter!
|
||||
var manifestLoader: MockManifestLoader!
|
||||
var graphLoader: MockGraphLoader!
|
||||
var subject: LintCommand!
|
||||
var subject: LintService!
|
||||
|
||||
override func setUp() {
|
||||
parser = ArgumentParser.test()
|
||||
graphLinter = MockGraphLinter()
|
||||
environmentLinter = MockEnvironmentLinter()
|
||||
manifestLoader = MockManifestLoader()
|
||||
graphLoader = MockGraphLoader()
|
||||
subject = LintCommand(graphLinter: graphLinter,
|
||||
subject = LintService(graphLinter: graphLinter,
|
||||
environmentLinter: environmentLinter,
|
||||
manifestLoading: manifestLoader,
|
||||
graphLoader: graphLoader,
|
||||
parser: parser)
|
||||
graphLoader: graphLoader)
|
||||
super.setUp()
|
||||
}
|
||||
|
||||
|
@ -41,32 +37,22 @@ final class LintCommandTests: TuistUnitTestCase {
|
|||
subject = nil
|
||||
}
|
||||
|
||||
func test_command() {
|
||||
XCTAssertEqual(LintCommand.command, "lint")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(LintCommand.overview, "Lints a workspace or a project that check whether they are well configured.")
|
||||
}
|
||||
|
||||
func test_run_throws_an_error_when_no_manifests_exist() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
manifestLoader.manifestsAtStub = { _ in Set() }
|
||||
let result = try parser.parse([LintCommand.command, "--path", path.pathString])
|
||||
|
||||
// When
|
||||
XCTAssertThrowsSpecific(try subject.run(with: result), LintCommandError.manifestNotFound(path))
|
||||
XCTAssertThrowsSpecific(try subject.run(path: path.pathString), LintServiceError.manifestNotFound(path))
|
||||
}
|
||||
|
||||
func test_run_when_there_are_no_issues_and_project_manifest() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
manifestLoader.manifestsAtStub = { _ in Set([.project]) }
|
||||
let result = try parser.parse([LintCommand.command, "--path", path.pathString])
|
||||
|
||||
// When
|
||||
try subject.run(with: result)
|
||||
try subject.run(path: path.pathString)
|
||||
|
||||
// Then
|
||||
XCTAssertPrinterOutputContains("""
|
||||
|
@ -83,10 +69,9 @@ final class LintCommandTests: TuistUnitTestCase {
|
|||
// Given
|
||||
let path = try temporaryPath()
|
||||
manifestLoader.manifestsAtStub = { _ in Set([.workspace]) }
|
||||
let result = try parser.parse([LintCommand.command, "--path", path.pathString])
|
||||
|
||||
// When
|
||||
try subject.run(with: result)
|
||||
try subject.run(path: path.pathString)
|
||||
|
||||
// Then
|
||||
XCTAssertPrinterOutputContains("""
|
||||
|
@ -105,10 +90,9 @@ final class LintCommandTests: TuistUnitTestCase {
|
|||
manifestLoader.manifestsAtStub = { _ in Set([.workspace]) }
|
||||
environmentLinter.lintStub = [LintingIssue(reason: "environment", severity: .error)]
|
||||
graphLinter.lintStub = [LintingIssue(reason: "graph", severity: .error)]
|
||||
let result = try parser.parse([LintCommand.command, "--path", path.pathString])
|
||||
|
||||
// Then
|
||||
XCTAssertThrowsSpecific(try subject.run(with: result), LintingError())
|
||||
XCTAssertThrowsSpecific(try subject.run(path: path.pathString), LintingError())
|
||||
XCTAssertPrinterOutputContains("""
|
||||
Loading the dependency graph
|
||||
Loading workspace at \(path.pathString)
|
|
@ -0,0 +1,51 @@
|
|||
import Basic
|
||||
import XCTest
|
||||
|
||||
@testable import TuistCore
|
||||
@testable import TuistKit
|
||||
@testable import TuistLoaderTesting
|
||||
@testable import TuistScaffoldTesting
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class ListServiceTests: TuistUnitTestCase {
|
||||
var subject: ListService!
|
||||
var templateLoader: MockTemplateLoader!
|
||||
var templatesDirectoryLocator: MockTemplatesDirectoryLocator!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
templateLoader = MockTemplateLoader()
|
||||
templatesDirectoryLocator = MockTemplatesDirectoryLocator()
|
||||
subject = ListService(templatesDirectoryLocator: templatesDirectoryLocator,
|
||||
templateLoader: templateLoader)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
subject = nil
|
||||
templateLoader = nil
|
||||
templatesDirectoryLocator = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_lists_available_templates() throws {
|
||||
// Given
|
||||
let expectedTemplates = ["template", "customTemplate"]
|
||||
let expectedOutput = expectedTemplates.map { $0 + ": description" }
|
||||
|
||||
templatesDirectoryLocator.templateDirectoriesStub = { _ in
|
||||
try expectedTemplates.map(self.temporaryPath().appending)
|
||||
}
|
||||
|
||||
templateLoader.loadTemplateStub = { _ in
|
||||
Template(description: "description")
|
||||
}
|
||||
|
||||
// When
|
||||
try subject.run(path: nil)
|
||||
|
||||
// Then
|
||||
expectedOutput.forEach {
|
||||
XCTAssertPrinterContains($0, at: .info, ==)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,27 +12,23 @@ import XCTest
|
|||
@testable import TuistScaffoldTesting
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class ScaffoldCommandTests: TuistUnitTestCase {
|
||||
var subject: ScaffoldCommand!
|
||||
var parser: ArgumentParser!
|
||||
final class ScaffoldServiceTests: TuistUnitTestCase {
|
||||
var subject: ScaffoldService!
|
||||
var templateLoader: MockTemplateLoader!
|
||||
var templatesDirectoryLocator: MockTemplatesDirectoryLocator!
|
||||
var templateGenerator: MockTemplateGenerator!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
parser = ArgumentParser.test()
|
||||
templateLoader = MockTemplateLoader()
|
||||
templatesDirectoryLocator = MockTemplatesDirectoryLocator()
|
||||
templateGenerator = MockTemplateGenerator()
|
||||
subject = ScaffoldCommand(parser: parser,
|
||||
templateLoader: templateLoader,
|
||||
subject = ScaffoldService(templateLoader: templateLoader,
|
||||
templatesDirectoryLocator: templatesDirectoryLocator,
|
||||
templateGenerator: templateGenerator)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
parser = nil
|
||||
subject = nil
|
||||
templateLoader = nil
|
||||
templatesDirectoryLocator = nil
|
||||
|
@ -40,52 +36,35 @@ final class ScaffoldCommandTests: TuistUnitTestCase {
|
|||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_name() {
|
||||
XCTAssertEqual(ScaffoldCommand.command, "scaffold")
|
||||
}
|
||||
func test_load_template_options() throws {
|
||||
// Given
|
||||
templateLoader.loadTemplateStub = { _ in
|
||||
Template(description: "test",
|
||||
attributes: [
|
||||
.required("required"),
|
||||
.optional("optional", default: ""),
|
||||
])
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(ScaffoldCommand.overview, "Generates new project based on template.")
|
||||
templatesDirectoryLocator.templateDirectoriesStub = { _ in
|
||||
[try self.temporaryPath().appending(component: "template")]
|
||||
}
|
||||
|
||||
let expectedOptions: (required: [String], optional: [String]) = (required: ["required"], optional: ["optional"])
|
||||
|
||||
// When
|
||||
let options = try subject.loadTemplateOptions(templateName: "template",
|
||||
path: nil)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(options.required, expectedOptions.required)
|
||||
XCTAssertEqual(options.optional, expectedOptions.optional)
|
||||
}
|
||||
|
||||
func test_fails_when_template_not_found() throws {
|
||||
let templateName = "template"
|
||||
let result = try parser.parse([ScaffoldCommand.command, templateName])
|
||||
XCTAssertThrowsSpecific(try subject.run(with: result), ScaffoldCommandError.templateNotFound(templateName))
|
||||
}
|
||||
|
||||
func test_adds_attributes_when_parsing() throws {
|
||||
// Given
|
||||
templateLoader.loadTemplateStub = { _ in
|
||||
Template(description: "test",
|
||||
attributes: [.required("name")])
|
||||
}
|
||||
|
||||
templatesDirectoryLocator.templateDirectoriesStub = { _ in
|
||||
[try self.temporaryPath().appending(component: "template")]
|
||||
}
|
||||
|
||||
// When
|
||||
let result = try subject.parse(with: parser,
|
||||
arguments: [ScaffoldCommand.command, "template", "--name", "test"])
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(try result.0.get("--name"), "test")
|
||||
}
|
||||
|
||||
func test_fails_when_attributes_not_added() throws {
|
||||
// Given
|
||||
templateLoader.loadTemplateStub = { _ in
|
||||
Template(description: "test")
|
||||
}
|
||||
|
||||
templatesDirectoryLocator.templateDirectoriesStub = { _ in
|
||||
[try self.temporaryPath().appending(component: "template")]
|
||||
}
|
||||
|
||||
// Then
|
||||
XCTAssertThrowsError(try subject.parse(with: parser,
|
||||
arguments: [ScaffoldCommand.command, "template", "--name", "Test"]))
|
||||
XCTAssertThrowsSpecific(try subject.testRun(templateName: templateName),
|
||||
ScaffoldServiceError.templateNotFound(templateName))
|
||||
}
|
||||
|
||||
func test_fails_when_required_attribute_not_provided() throws {
|
||||
|
@ -98,15 +77,9 @@ final class ScaffoldCommandTests: TuistUnitTestCase {
|
|||
[try self.temporaryPath().appending(component: "template")]
|
||||
}
|
||||
|
||||
let arguments = [ScaffoldCommand.command, "template"]
|
||||
_ = try subject.parse(with: parser, arguments: arguments)
|
||||
|
||||
// When
|
||||
let result = try parser.parse(arguments)
|
||||
|
||||
// Then
|
||||
XCTAssertThrowsSpecific(try subject.run(with: result),
|
||||
ScaffoldCommandError.attributeNotProvided("required"))
|
||||
XCTAssertThrowsSpecific(try subject.testRun(),
|
||||
ScaffoldServiceError.attributeNotProvided("required"))
|
||||
}
|
||||
|
||||
func test_optional_attribute_is_taken_from_template() throws {
|
||||
|
@ -124,12 +97,8 @@ final class ScaffoldCommandTests: TuistUnitTestCase {
|
|||
generateAttributes = attributes
|
||||
}
|
||||
|
||||
let arguments = [ScaffoldCommand.command, "template"]
|
||||
_ = try subject.parse(with: parser, arguments: arguments)
|
||||
let result = try parser.parse(arguments)
|
||||
|
||||
// When
|
||||
try subject.run(with: result)
|
||||
try subject.testRun()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(["optional": "optionalValue"],
|
||||
|
@ -152,12 +121,9 @@ final class ScaffoldCommandTests: TuistUnitTestCase {
|
|||
generateAttributes = attributes
|
||||
}
|
||||
|
||||
let arguments = [ScaffoldCommand.command, "template", "--optional", "optionalValue", "--required", "requiredValue"]
|
||||
_ = try subject.parse(with: parser, arguments: arguments)
|
||||
let result = try parser.parse(arguments)
|
||||
|
||||
// When
|
||||
try subject.run(with: result)
|
||||
try subject.testRun(requiredTemplateOptions: ["required": "requiredValue"],
|
||||
optionalTemplateOptions: ["optional": "optionalValue"])
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(["optional": "optionalValue",
|
||||
|
@ -165,3 +131,15 @@ final class ScaffoldCommandTests: TuistUnitTestCase {
|
|||
generateAttributes)
|
||||
}
|
||||
}
|
||||
|
||||
extension ScaffoldService {
|
||||
func testRun(path: String? = nil,
|
||||
templateName: String = "template",
|
||||
requiredTemplateOptions: [String: String] = [:],
|
||||
optionalTemplateOptions: [String: String] = [:]) throws {
|
||||
try run(path: path,
|
||||
templateName: templateName,
|
||||
requiredTemplateOptions: requiredTemplateOptions,
|
||||
optionalTemplateOptions: optionalTemplateOptions)
|
||||
}
|
||||
}
|
|
@ -1,52 +1,41 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import SPMUtility
|
||||
import TuistCore
|
||||
import TuistLoader
|
||||
import TuistSupport
|
||||
import XcodeProj
|
||||
import XCTest
|
||||
|
||||
@testable import TuistCoreTesting
|
||||
@testable import TuistKit
|
||||
@testable import TuistLoaderTesting
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class UpCommandTests: TuistUnitTestCase {
|
||||
var subject: UpCommand!
|
||||
var parser: ArgumentParser!
|
||||
final class UpServiceTests: TuistUnitTestCase {
|
||||
var subject: UpService!
|
||||
var setupLoader: MockSetupLoader!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
parser = ArgumentParser.test()
|
||||
setupLoader = MockSetupLoader()
|
||||
subject = UpCommand(parser: parser,
|
||||
setupLoader: setupLoader)
|
||||
subject = UpService(setupLoader: setupLoader)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
subject = nil
|
||||
parser = nil
|
||||
setupLoader = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_command() {
|
||||
XCTAssertEqual(UpCommand.command, "up")
|
||||
}
|
||||
|
||||
func test_overview() {
|
||||
XCTAssertEqual(UpCommand.overview, "Configures the environment for the project.")
|
||||
}
|
||||
|
||||
func test_run_configures_the_environment() throws {
|
||||
// given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
let result = try parser.parse([UpCommand.command])
|
||||
var receivedPaths = [String]()
|
||||
setupLoader.meetStub = { path in
|
||||
receivedPaths.append(path.pathString)
|
||||
}
|
||||
|
||||
// when
|
||||
try subject.run(with: result)
|
||||
try subject.run(path: temporaryPath.pathString)
|
||||
|
||||
// then
|
||||
XCTAssertEqual(receivedPaths, [temporaryPath.pathString])
|
||||
|
@ -56,14 +45,13 @@ final class UpCommandTests: TuistUnitTestCase {
|
|||
func test_run_uses_the_given_path() throws {
|
||||
// given
|
||||
let path = AbsolutePath("/path")
|
||||
let result = try parser.parse([UpCommand.command, "-p", path.pathString])
|
||||
var receivedPaths = [String]()
|
||||
setupLoader.meetStub = { path in
|
||||
receivedPaths.append(path.pathString)
|
||||
}
|
||||
|
||||
// when
|
||||
try subject.run(with: result)
|
||||
try subject.run(path: path.pathString)
|
||||
|
||||
// then
|
||||
XCTAssertEqual(receivedPaths, ["/path"])
|
|
@ -10,12 +10,11 @@ private struct TestError: FatalError {
|
|||
|
||||
final class ErrorHandlerTests: TuistUnitTestCase {
|
||||
var subject: ErrorHandler!
|
||||
var exited: Int32?
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
subject = ErrorHandler { self.exited = $0 }
|
||||
subject = ErrorHandler()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
|
@ -29,12 +28,6 @@ final class ErrorHandlerTests: TuistUnitTestCase {
|
|||
XCTAssertPrinterErrorContains(error.description)
|
||||
}
|
||||
|
||||
func test_fatalError_exitsWith1() {
|
||||
let error = TestError(type: .abort)
|
||||
subject.fatal(error: error)
|
||||
XCTAssertEqual(exited, 1)
|
||||
}
|
||||
|
||||
func test_fatalError_prints_whenItsSilent() {
|
||||
let error = TestError(type: .bugSilent)
|
||||
subject.fatal(error: error)
|
||||
|
|
Loading…
Reference in New Issue