Tasks (#2816)
* Create Tasks model * Run custom task * Create tasks fixture test * Add task options * Rename load arguments for Tasks * Foprmat code * WIP: Change tasks to work with multiple files * Run task without options * Parse options with regex * Remove optional and required options * Skip building project description helpers for tasks * Move Task model to ProjectAutomation * WIP: Editing tasks * Editing tasks * Add task documentation * WIP tests * Add tests * Format code * Limit file named in step definitions * Fix ruby code format * Rename task command to exec * Trigger CI workflows Co-authored-by: Pedro Piñera <pedro@ppinera.es>
This commit is contained in:
parent
8d9dc8c7f5
commit
1f776e728f
|
@ -25,6 +25,11 @@ let package = Package(
|
|||
type: .dynamic,
|
||||
targets: ["ProjectDescription"]
|
||||
),
|
||||
.library(
|
||||
name: "ProjectAutomation",
|
||||
type: .dynamic,
|
||||
targets: ["ProjectAutomation"]
|
||||
),
|
||||
.library(
|
||||
name: "TuistGraph",
|
||||
targets: ["TuistGraph"]
|
||||
|
@ -138,6 +143,7 @@ let package = Package(
|
|||
"TuistCache",
|
||||
"TuistAutomation",
|
||||
"ProjectDescription",
|
||||
"ProjectAutomation",
|
||||
signalsDependency,
|
||||
rxSwiftDependency,
|
||||
rxBlockingDependency,
|
||||
|
@ -155,6 +161,7 @@ let package = Package(
|
|||
"TuistAnalytics",
|
||||
"TuistPlugin",
|
||||
"TuistGraph",
|
||||
"TuistTasks",
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
|
@ -165,6 +172,7 @@ let package = Package(
|
|||
"TuistSupportTesting",
|
||||
"TuistCoreTesting",
|
||||
"ProjectDescription",
|
||||
"ProjectAutomation",
|
||||
rxBlockingDependency,
|
||||
"TuistLoaderTesting",
|
||||
"TuistCacheTesting",
|
||||
|
@ -181,6 +189,7 @@ let package = Package(
|
|||
"TuistGraphTesting",
|
||||
"TuistPlugin",
|
||||
"TuistPluginTesting",
|
||||
"TuistTasksTesting",
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
|
@ -190,6 +199,7 @@ let package = Package(
|
|||
"TuistCoreTesting",
|
||||
"TuistSupportTesting",
|
||||
"ProjectDescription",
|
||||
"ProjectAutomation",
|
||||
rxBlockingDependency,
|
||||
"TuistLoaderTesting",
|
||||
"TuistCloudTesting",
|
||||
|
@ -201,6 +211,7 @@ let package = Package(
|
|||
dependencies: [
|
||||
"TuistKit",
|
||||
"ProjectDescription",
|
||||
"ProjectAutomation",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
|
@ -237,6 +248,10 @@ let package = Package(
|
|||
"TuistSupportTesting",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "ProjectAutomation",
|
||||
dependencies: []
|
||||
),
|
||||
.target(
|
||||
name: "TuistSupport",
|
||||
dependencies: [
|
||||
|
@ -396,6 +411,29 @@ let package = Package(
|
|||
"TuistGraphTesting",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "TuistTasks",
|
||||
dependencies: [
|
||||
swiftToolsSupportDependency,
|
||||
"TuistCore",
|
||||
"TuistSupport",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "TuistTasksTesting",
|
||||
dependencies: [
|
||||
"TuistTasks",
|
||||
"TuistGraphTesting",
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "TuistTasksIntegrationTests",
|
||||
dependencies: [
|
||||
"TuistTasks",
|
||||
"TuistSupportTesting",
|
||||
"TuistGraphTesting",
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "TuistScaffold",
|
||||
dependencies: [
|
||||
|
|
13
Rakefile
13
Rakefile
|
@ -68,6 +68,15 @@ def package
|
|||
"-Xswiftc", "-emit-module-interface-path",
|
||||
"-Xswiftc", ".build/release/ProjectDescription.swiftinterface"
|
||||
)
|
||||
system(
|
||||
"swift", "build",
|
||||
"--product", "ProjectAutomation",
|
||||
"--configuration", "release",
|
||||
"-Xswiftc", "-enable-library-evolution",
|
||||
"-Xswiftc", "-emit-module-interface",
|
||||
"-Xswiftc", "-emit-module-interface-path",
|
||||
"-Xswiftc", ".build/release/ProjectAutomation.swiftinterface"
|
||||
)
|
||||
system("swift", "build", "--product", "tuistenv", "--configuration", "release")
|
||||
|
||||
build_templates_path = File.join(__dir__, ".build/release/Templates")
|
||||
|
@ -91,6 +100,10 @@ def package
|
|||
"ProjectDescription.swiftdoc",
|
||||
"libProjectDescription.dylib",
|
||||
"ProjectDescription.swiftinterface",
|
||||
"ProjectAutomation.swiftmodule",
|
||||
"ProjectAutomation.swiftdoc",
|
||||
"libProjectAutomation.dylib",
|
||||
"ProjectAutomation.swiftinterface",
|
||||
"Templates",
|
||||
"vendor",
|
||||
"script"
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import Foundation
|
||||
|
||||
public struct Task {
|
||||
public let options: [Option]
|
||||
public let task: ([String: String]) throws -> Void
|
||||
|
||||
public enum Option: Equatable {
|
||||
case option(String)
|
||||
}
|
||||
|
||||
public init(
|
||||
options: [Option],
|
||||
task: @escaping ([String: String]) throws -> Void
|
||||
) {
|
||||
self.options = options
|
||||
self.task = task
|
||||
|
||||
runIfNeeded()
|
||||
}
|
||||
|
||||
private func runIfNeeded() {
|
||||
guard
|
||||
let taskCommandLineIndex = CommandLine.arguments.firstIndex(of: "--tuist-task"),
|
||||
CommandLine.argc > taskCommandLineIndex
|
||||
else { return }
|
||||
let attributesString = CommandLine.arguments[taskCommandLineIndex + 1]
|
||||
// swiftlint:disable force_try
|
||||
let attributes: [String: String] = try! JSONDecoder().decode(
|
||||
[String: String].self,
|
||||
from: attributesString.data(using: .utf8)!
|
||||
)
|
||||
try! task(attributes)
|
||||
// swiftlint:enable force_try
|
||||
}
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
import Foundation
|
||||
|
||||
func dumpIfNeeded<E: Encodable>(_ entity: E) {
|
||||
if CommandLine.argc > 0 {
|
||||
if CommandLine.arguments.contains("--tuist-dump") {
|
||||
let encoder = JSONEncoder()
|
||||
// swiftlint:disable:next force_try
|
||||
let data = try! encoder.encode(entity)
|
||||
let manifest = String(data: data, encoding: .utf8)!
|
||||
print("TUIST_MANIFEST_START")
|
||||
print(manifest)
|
||||
print("TUIST_MANIFEST_END")
|
||||
}
|
||||
}
|
||||
guard
|
||||
CommandLine.argc > 0,
|
||||
CommandLine.arguments.contains("--tuist-dump")
|
||||
else { return }
|
||||
let encoder = JSONEncoder()
|
||||
// swiftlint:disable:next force_try
|
||||
let data = try! encoder.encode(entity)
|
||||
let manifest = String(data: data, encoding: .utf8)!
|
||||
print("TUIST_MANIFEST_START")
|
||||
print(manifest)
|
||||
print("TUIST_MANIFEST_END")
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import TSCBasic
|
|||
|
||||
extension Template {
|
||||
public static func test(description: String = "Template",
|
||||
attributes: [Template.Attribute] = [],
|
||||
attributes: [Attribute] = [],
|
||||
files: [Template.File] = []) -> Template
|
||||
{
|
||||
Template(
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
enum ExecCommandError: FatalError, Equatable {
|
||||
case taskNotProvided
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .taskNotProvided:
|
||||
return "You must provide a task name."
|
||||
}
|
||||
}
|
||||
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .taskNotProvided:
|
||||
return .bug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ExecCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(
|
||||
commandName: "exec",
|
||||
abstract: "Runs a task defined in Tuist/Tasks directory."
|
||||
)
|
||||
}
|
||||
|
||||
@Argument(
|
||||
help: "Name of a task you want to run."
|
||||
)
|
||||
var task: String
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path to the directory where the tasks are run from.",
|
||||
completion: .directory
|
||||
)
|
||||
var path: String?
|
||||
|
||||
func run() throws {
|
||||
try ExecService().run(
|
||||
task,
|
||||
options: taskOptions,
|
||||
path: path
|
||||
)
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
||||
var taskOptions: [String: String] = [:]
|
||||
|
||||
// Custom decoding to decode dynamic options
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
task = try container.decode(Argument<String>.self, forKey: .task).wrappedValue
|
||||
path = try container.decodeIfPresent(Option<String>.self, forKey: .path)?.wrappedValue
|
||||
try ExecCommand.options.forEach { option in
|
||||
guard let value = try container.decode(
|
||||
Option<String?>.self,
|
||||
forKey: .option(option.name)
|
||||
)
|
||||
.wrappedValue
|
||||
else { return }
|
||||
taskOptions[option.name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preprocessing
|
||||
|
||||
extension ExecCommand {
|
||||
static var options: [(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 ExecCommandError.taskNotProvided }
|
||||
guard !configuration.subcommands.contains(where: { $0.configuration.commandName == arguments[1] }) else { return }
|
||||
// We want to parse only the name of a task, not its arguments which will be dynamically added
|
||||
// Plucking out path arguments
|
||||
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? ExecCommand else { return }
|
||||
|
||||
ExecCommand.options = try ExecService().loadTaskOptions(
|
||||
taskName: command.task,
|
||||
path: command.path
|
||||
)
|
||||
.map {
|
||||
(name: $0, option: Option<String?>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TaskCommand.CodingKeys
|
||||
|
||||
extension ExecCommand {
|
||||
enum CodingKeys: CodingKey {
|
||||
case task
|
||||
case path
|
||||
case option(String)
|
||||
|
||||
var stringValue: String {
|
||||
switch self {
|
||||
case .task:
|
||||
return "task"
|
||||
case .path:
|
||||
return "path"
|
||||
case let .option(option):
|
||||
return option
|
||||
}
|
||||
}
|
||||
|
||||
init?(stringValue: String) {
|
||||
switch stringValue {
|
||||
case "task":
|
||||
self = .task
|
||||
case "path":
|
||||
self = .path
|
||||
default:
|
||||
if ExecCommand.options.map(\.name).contains(stringValue) {
|
||||
self = .option(stringValue)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not used
|
||||
var intValue: Int? { nil }
|
||||
init?(intValue _: Int) { nil }
|
||||
}
|
||||
}
|
||||
|
||||
/// ArgumentParser library gets the list of options from a mirror.
|
||||
/// Since we do not declare task's options in advance, we need to rewrite the mirror implementation and add them ourselves.
|
||||
extension ExecCommand: CustomReflectable {
|
||||
var customMirror: Mirror {
|
||||
let optionsChildren = ExecCommand.options
|
||||
.map { Mirror.Child(label: $0.name, value: $0.option) }
|
||||
let children = [
|
||||
Mirror.Child(label: "task", value: _task),
|
||||
Mirror.Child(label: "path", value: _path),
|
||||
]
|
||||
.filter {
|
||||
// Prefer attributes defined in a template if it clashes with predefined ones
|
||||
$0.label.map { label in
|
||||
!ExecCommand.options.map(\.name)
|
||||
.contains(label)
|
||||
} ?? true
|
||||
}
|
||||
return Mirror(ExecCommand(), children: children + optionsChildren)
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ public struct TuistCommand: ParsableCommand {
|
|||
CleanCommand.self,
|
||||
DocCommand.self,
|
||||
DependenciesCommand.self,
|
||||
ExecCommand.self,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
@ -53,6 +54,9 @@ public struct TuistCommand: ParsableCommand {
|
|||
if processedArguments.first == InitCommand.configuration.commandName {
|
||||
try InitCommand.preprocess(processedArguments)
|
||||
}
|
||||
if processedArguments.first == ExecCommand.configuration.commandName {
|
||||
try ExecCommand.preprocess(processedArguments)
|
||||
}
|
||||
command = try parseAsRoot(processedArguments)
|
||||
} catch {
|
||||
let exitCode = self.exitCode(for: error).rawValue
|
||||
|
|
|
@ -6,6 +6,7 @@ import TuistGraph
|
|||
import TuistLoader
|
||||
import TuistScaffold
|
||||
import TuistSupport
|
||||
import TuistTasks
|
||||
|
||||
enum ProjectEditorError: FatalError, Equatable {
|
||||
/// This error is thrown when we try to edit in a project in a directory that has no editable files.
|
||||
|
@ -60,8 +61,8 @@ final class ProjectEditor: ProjectEditing {
|
|||
let templatesDirectoryLocator: TemplatesDirectoryLocating
|
||||
|
||||
private let cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring
|
||||
|
||||
private let projectDescriptionHelpersBuilderFactory: ProjectDescriptionHelpersBuilderFactoring
|
||||
private let tasksLocator: TasksLocating
|
||||
|
||||
/// Xcode Project writer
|
||||
private let writer: XcodeProjWriting
|
||||
|
@ -75,7 +76,8 @@ final class ProjectEditor: ProjectEditing {
|
|||
writer: XcodeProjWriting = XcodeProjWriter(),
|
||||
templatesDirectoryLocator: TemplatesDirectoryLocating = TemplatesDirectoryLocator(),
|
||||
cacheDirectoryProviderFactory: CacheDirectoriesProviderFactoring = CacheDirectoriesProviderFactory(),
|
||||
projectDescriptionHelpersBuilderFactory: ProjectDescriptionHelpersBuilderFactoring = ProjectDescriptionHelpersBuilderFactory()
|
||||
projectDescriptionHelpersBuilderFactory: ProjectDescriptionHelpersBuilderFactoring = ProjectDescriptionHelpersBuilderFactory(),
|
||||
tasksLocator: TasksLocating = TasksLocator()
|
||||
) {
|
||||
self.generator = generator
|
||||
self.projectEditorMapper = projectEditorMapper
|
||||
|
@ -86,6 +88,7 @@ final class ProjectEditor: ProjectEditing {
|
|||
self.templatesDirectoryLocator = templatesDirectoryLocator
|
||||
self.cacheDirectoryProviderFactory = cacheDirectoryProviderFactory
|
||||
self.projectDescriptionHelpersBuilderFactory = projectDescriptionHelpersBuilderFactory
|
||||
self.tasksLocator = tasksLocator
|
||||
}
|
||||
|
||||
func edit(
|
||||
|
@ -110,6 +113,8 @@ final class ProjectEditor: ProjectEditing {
|
|||
FileHandler.shared.glob($0, glob: "**/*.swift") + FileHandler.shared.glob($0, glob: "**/*.stencil")
|
||||
} ?? []
|
||||
|
||||
let tasks = try tasksLocator.locateTasks(at: editingPath)
|
||||
|
||||
let editablePluginManifests = locateEditablePluginManifests(at: editingPath, plugins: plugins)
|
||||
let builtPluginHelperModules = try buildRemotePluginModules(
|
||||
in: editingPath,
|
||||
|
@ -140,7 +145,9 @@ final class ProjectEditor: ProjectEditing {
|
|||
pluginProjectDescriptionHelpersModule: builtPluginHelperModules,
|
||||
helpers: helpers,
|
||||
templates: templates,
|
||||
projectDescriptionPath: projectDescriptionPath
|
||||
tasks: tasks,
|
||||
projectDescriptionPath: projectDescriptionPath,
|
||||
projectAutomationPath: try resourceLocator.projectAutomation()
|
||||
)
|
||||
|
||||
let graphTraverser = ValueGraphTraverser(graph: graph)
|
||||
|
|
|
@ -19,7 +19,9 @@ protocol ProjectEditorMapping: AnyObject {
|
|||
pluginProjectDescriptionHelpersModule: [ProjectDescriptionHelpersModule],
|
||||
helpers: [AbsolutePath],
|
||||
templates: [AbsolutePath],
|
||||
projectDescriptionPath: AbsolutePath
|
||||
tasks: [AbsolutePath],
|
||||
projectDescriptionPath: AbsolutePath,
|
||||
projectAutomationPath: AbsolutePath
|
||||
) throws -> ValueGraph
|
||||
}
|
||||
|
||||
|
@ -39,7 +41,9 @@ final class ProjectEditorMapper: ProjectEditorMapping {
|
|||
pluginProjectDescriptionHelpersModule: [ProjectDescriptionHelpersModule],
|
||||
helpers: [AbsolutePath],
|
||||
templates: [AbsolutePath],
|
||||
projectDescriptionPath: AbsolutePath
|
||||
tasks: [AbsolutePath],
|
||||
projectDescriptionPath: AbsolutePath,
|
||||
projectAutomationPath: AbsolutePath
|
||||
) throws -> ValueGraph {
|
||||
let swiftVersion = try System.shared.swiftVersion()
|
||||
|
||||
|
@ -55,12 +59,14 @@ final class ProjectEditorMapper: ProjectEditorMapping {
|
|||
let manifestsProject = mapManifestsProject(
|
||||
projectManifests: projectManifests,
|
||||
projectDescriptionPath: projectDescriptionPath,
|
||||
projectAutomationPath: projectAutomationPath,
|
||||
swiftVersion: swiftVersion,
|
||||
sourceRootPath: sourceRootPath,
|
||||
destinationDirectory: destinationDirectory,
|
||||
tuistPath: tuistPath,
|
||||
helpers: helpers,
|
||||
templates: templates,
|
||||
tasks: tasks,
|
||||
setupPath: setupPath,
|
||||
configPath: configPath,
|
||||
dependenciesPath: dependenciesPath,
|
||||
|
@ -122,12 +128,14 @@ final class ProjectEditorMapper: ProjectEditorMapping {
|
|||
private func mapManifestsProject(
|
||||
projectManifests: [AbsolutePath],
|
||||
projectDescriptionPath: AbsolutePath,
|
||||
projectAutomationPath: AbsolutePath,
|
||||
swiftVersion: String,
|
||||
sourceRootPath: AbsolutePath,
|
||||
destinationDirectory: AbsolutePath,
|
||||
tuistPath: AbsolutePath,
|
||||
helpers: [AbsolutePath],
|
||||
templates: [AbsolutePath],
|
||||
tasks: [AbsolutePath],
|
||||
setupPath: AbsolutePath?,
|
||||
configPath: AbsolutePath?,
|
||||
dependenciesPath: AbsolutePath?,
|
||||
|
@ -165,6 +173,19 @@ final class ProjectEditorMapper: ProjectEditorMapping {
|
|||
)
|
||||
}()
|
||||
|
||||
let tasksTargets = tasks.map { taskPath in
|
||||
editorHelperTarget(
|
||||
name: taskPath.basenameWithoutExt,
|
||||
filesGroup: manifestsFilesGroup,
|
||||
targetSettings: Settings(
|
||||
base: targetBaseSettings(for: [projectAutomationPath], swiftVersion: swiftVersion),
|
||||
configurations: Settings.default.configurations,
|
||||
defaultSettings: .recommended
|
||||
),
|
||||
sourcePaths: [taskPath]
|
||||
)
|
||||
}
|
||||
|
||||
let setupTarget: Target? = {
|
||||
guard let setupPath = setupPath else { return nil }
|
||||
return editorHelperTarget(
|
||||
|
@ -221,7 +242,10 @@ final class ProjectEditorMapper: ProjectEditorMapping {
|
|||
setupTarget,
|
||||
configTarget,
|
||||
dependenciesTarget,
|
||||
].compactMap { $0 } + manifestsTargets
|
||||
]
|
||||
.compactMap { $0 }
|
||||
+ manifestsTargets
|
||||
+ tasksTargets
|
||||
|
||||
let buildAction = BuildAction(targets: targets.map { TargetReference(projectPath: projectPath, name: $0.name) })
|
||||
let arguments = Arguments(launchArguments: [LaunchArgument(name: "generate --path \(sourceRootPath)", isEnabled: true)])
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import Foundation
|
||||
import ProjectDescription
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistGraph
|
||||
import TuistLoader
|
||||
import TuistPlugin
|
||||
import TuistSupport
|
||||
import TuistTasks
|
||||
|
||||
enum ExecError: FatalError, Equatable {
|
||||
case taskNotFound(String, [String])
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .taskNotFound(task, tasks):
|
||||
return "Task \(task) not found. Available tasks are: \(tasks.joined(separator: ", "))"
|
||||
}
|
||||
}
|
||||
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .taskNotFound:
|
||||
return .abort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ExecService {
|
||||
private let manifestLoader: ManifestLoading
|
||||
private let tasksLocator: TasksLocating
|
||||
|
||||
init(
|
||||
manifestLoader: ManifestLoading = ManifestLoader(),
|
||||
tasksLocator: TasksLocating = TasksLocator()
|
||||
) {
|
||||
self.manifestLoader = manifestLoader
|
||||
self.tasksLocator = tasksLocator
|
||||
}
|
||||
|
||||
func run(
|
||||
_ taskName: String,
|
||||
options: [String: String],
|
||||
path: String?
|
||||
) throws {
|
||||
let path = self.path(path)
|
||||
let taskPath = try task(with: taskName, path: path)
|
||||
let runArguments = try manifestLoader.taskLoadArguments(at: taskPath)
|
||||
+ [
|
||||
"--tuist-task",
|
||||
String(data: try JSONEncoder().encode(options), encoding: .utf8)!,
|
||||
]
|
||||
try ProcessEnv.chdir(path)
|
||||
try System.shared.runAndPrint(
|
||||
runArguments,
|
||||
verbose: false,
|
||||
environment: Environment.shared.manifestLoadingVariables
|
||||
)
|
||||
}
|
||||
|
||||
func loadTaskOptions(
|
||||
taskName: String,
|
||||
path: String?
|
||||
) throws -> [String] {
|
||||
let path = self.path(path)
|
||||
let taskPath = try task(with: taskName, path: path)
|
||||
let taskContents = try FileHandler.shared.readTextFile(taskPath)
|
||||
let optionsRegex = try NSRegularExpression(pattern: "\\.option\\(\"([^\"]*)\"\\),?", options: [])
|
||||
var options: [String] = []
|
||||
optionsRegex.enumerateMatches(
|
||||
in: taskContents,
|
||||
options: [],
|
||||
range: NSRange(location: 0, length: taskContents.count)
|
||||
) { match, _, _ in
|
||||
guard
|
||||
let match = match,
|
||||
match.numberOfRanges == 2,
|
||||
let range = Range(match.range(at: 1), in: taskContents)
|
||||
else { return }
|
||||
options.append(
|
||||
String(taskContents[range])
|
||||
)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func task(with name: String, path: AbsolutePath) throws -> AbsolutePath {
|
||||
let tasks: [String: AbsolutePath] = try tasksLocator.locateTasks(at: path)
|
||||
.reduce(into: [:]) { acc, current in
|
||||
acc[current.basenameWithoutExt.camelCaseToKebabCase()] = current
|
||||
}
|
||||
|
||||
guard let task = tasks[name] else { throw ExecError.taskNotFound(name, tasks.map(\.key).sorted()) }
|
||||
return task
|
||||
}
|
||||
|
||||
private func path(_ path: String?) -> AbsolutePath {
|
||||
if let path = path {
|
||||
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
|
||||
} else {
|
||||
return FileHandler.shared.currentPath
|
||||
}
|
||||
}
|
||||
}
|
|
@ -96,6 +96,10 @@ public class CachedManifestLoader: ManifestLoading {
|
|||
try manifestLoader.loadDependencies(at: path)
|
||||
}
|
||||
|
||||
public func taskLoadArguments(at path: AbsolutePath) throws -> [String] {
|
||||
try manifestLoader.taskLoadArguments(at: path)
|
||||
}
|
||||
|
||||
public func manifests(at path: AbsolutePath) -> Set<Manifest> {
|
||||
manifestLoader.manifests(at: path)
|
||||
}
|
||||
|
|
|
@ -88,6 +88,10 @@ public protocol ManifestLoading {
|
|||
/// - path: Path to the directory that contains Dependencies.swift
|
||||
func loadDependencies(at path: AbsolutePath) throws -> ProjectDescription.Dependencies
|
||||
|
||||
/// Returns arguments for loading `Tasks.swift`
|
||||
/// You can append this list to insert your own custom flag
|
||||
func taskLoadArguments(at path: AbsolutePath) throws -> [String]
|
||||
|
||||
/// Loads the Plugin.swift in the given directory.
|
||||
/// - Parameter path: Path to the directory that contains Plugin.swift
|
||||
func loadPlugin(at path: AbsolutePath) throws -> ProjectDescription.Plugin
|
||||
|
@ -199,6 +203,10 @@ public class ManifestLoader: ManifestLoading {
|
|||
return try decoder.decode(Dependencies.self, from: dependenciesData)
|
||||
}
|
||||
|
||||
public func taskLoadArguments(at path: AbsolutePath) throws -> [String] {
|
||||
try buildArguments(.task, at: path)
|
||||
}
|
||||
|
||||
public func loadPlugin(at path: AbsolutePath) throws -> ProjectDescription.Plugin {
|
||||
try loadManifest(.plugin, at: path)
|
||||
}
|
||||
|
@ -213,6 +221,22 @@ public class ManifestLoader: ManifestLoading {
|
|||
_ manifest: Manifest,
|
||||
at path: AbsolutePath
|
||||
) throws -> T {
|
||||
let manifestPath = try self.manifestPath(
|
||||
manifest,
|
||||
at: path
|
||||
)
|
||||
let data = try loadDataForManifest(manifest, at: manifestPath)
|
||||
if Environment.shared.isVerbose {
|
||||
let string = String(data: data, encoding: .utf8)
|
||||
logger.debug("Trying to load the manifest represented by the following JSON representation:\n\(string ?? "")")
|
||||
}
|
||||
return try decoder.decode(T.self, from: data)
|
||||
}
|
||||
|
||||
private func manifestPath(
|
||||
_ manifest: Manifest,
|
||||
at path: AbsolutePath
|
||||
) throws -> AbsolutePath {
|
||||
var fileNames = [manifest.fileName(path)]
|
||||
if let deprecatedFileName = manifest.deprecatedFileName {
|
||||
fileNames.insert(deprecatedFileName, at: 0)
|
||||
|
@ -221,12 +245,7 @@ public class ManifestLoader: ManifestLoading {
|
|||
for fileName in fileNames {
|
||||
let manifestPath = path.appending(component: fileName)
|
||||
if !FileHandler.shared.exists(manifestPath) { continue }
|
||||
let data = try loadDataForManifest(manifest, at: manifestPath)
|
||||
if Environment.shared.isVerbose {
|
||||
let string = String(data: data, encoding: .utf8)
|
||||
logger.debug("Trying to load the manifest represented by the following JSON representation:\n\(string ?? "")")
|
||||
}
|
||||
return try decoder.decode(T.self, from: data)
|
||||
return manifestPath
|
||||
}
|
||||
|
||||
throw ManifestLoaderError.manifestNotFound(manifest, path)
|
||||
|
@ -237,52 +256,10 @@ public class ManifestLoader: ManifestLoading {
|
|||
_ manifest: Manifest,
|
||||
at path: AbsolutePath
|
||||
) throws -> Data {
|
||||
let projectDescriptionPath = try resourceLocator.projectDescription()
|
||||
let searchPaths = ProjectDescriptionSearchPaths.paths(for: projectDescriptionPath)
|
||||
var arguments = [
|
||||
"/usr/bin/xcrun",
|
||||
"swiftc",
|
||||
"--driver-mode=swift",
|
||||
"-suppress-warnings",
|
||||
"-I", searchPaths.includeSearchPath.pathString,
|
||||
"-L", searchPaths.librarySearchPath.pathString,
|
||||
"-F", searchPaths.frameworkSearchPath.pathString,
|
||||
"-lProjectDescription",
|
||||
"-framework", "ProjectDescription",
|
||||
]
|
||||
let projectDescriptionHelpersCacheDirectory = try cacheDirectoryProviderFactory
|
||||
.cacheDirectories(config: nil)
|
||||
.projectDescriptionHelpersCacheDirectory
|
||||
|
||||
let projectDescriptionHelperArguments: [String] = try {
|
||||
switch manifest {
|
||||
case .config,
|
||||
.plugin:
|
||||
return []
|
||||
case .dependencies,
|
||||
.galaxy,
|
||||
.project,
|
||||
.setup,
|
||||
.template,
|
||||
.workspace:
|
||||
return try projectDescriptionHelpersBuilderFactory
|
||||
.projectDescriptionHelpersBuilder(cacheDirectory: projectDescriptionHelpersCacheDirectory)
|
||||
.build(
|
||||
at: path,
|
||||
projectDescriptionSearchPaths: searchPaths,
|
||||
projectDescriptionHelperPlugins: plugins.projectDescriptionHelpers
|
||||
).flatMap { [
|
||||
"-I", $0.path.parentDirectory.pathString,
|
||||
"-L", $0.path.parentDirectory.pathString,
|
||||
"-F", $0.path.parentDirectory.pathString,
|
||||
"-l\($0.name)",
|
||||
] }
|
||||
}
|
||||
}()
|
||||
|
||||
arguments.append(contentsOf: projectDescriptionHelperArguments)
|
||||
arguments.append(path.pathString)
|
||||
arguments.append("--tuist-dump")
|
||||
let arguments = try buildArguments(
|
||||
manifest,
|
||||
at: path
|
||||
) + ["--tuist-dump"]
|
||||
|
||||
let result = System.shared
|
||||
.observable(arguments, verbose: false, environment: environment.manifestLoadingVariables)
|
||||
|
@ -312,6 +289,75 @@ public class ManifestLoader: ManifestLoading {
|
|||
}
|
||||
}
|
||||
|
||||
private func buildArguments(
|
||||
_ manifest: Manifest,
|
||||
at path: AbsolutePath
|
||||
) throws -> [String] {
|
||||
let projectDescriptionPath = try resourceLocator.projectDescription()
|
||||
let searchPaths = ProjectDescriptionSearchPaths.paths(for: projectDescriptionPath)
|
||||
let frameworkName: String
|
||||
switch manifest {
|
||||
case .task:
|
||||
frameworkName = "ProjectAutomation"
|
||||
case .config,
|
||||
.plugin,
|
||||
.dependencies,
|
||||
.galaxy,
|
||||
.project,
|
||||
.setup,
|
||||
.template,
|
||||
.workspace:
|
||||
frameworkName = "ProjectDescription"
|
||||
}
|
||||
var arguments = [
|
||||
"/usr/bin/xcrun",
|
||||
"swiftc",
|
||||
"--driver-mode=swift",
|
||||
"-suppress-warnings",
|
||||
"-I", searchPaths.includeSearchPath.pathString,
|
||||
"-L", searchPaths.librarySearchPath.pathString,
|
||||
"-F", searchPaths.frameworkSearchPath.pathString,
|
||||
"-l\(frameworkName)",
|
||||
"-framework", frameworkName,
|
||||
]
|
||||
let projectDescriptionHelpersCacheDirectory = try cacheDirectoryProviderFactory
|
||||
.cacheDirectories(config: nil)
|
||||
.projectDescriptionHelpersCacheDirectory
|
||||
|
||||
let projectDescriptionHelperArguments: [String] = try {
|
||||
switch manifest {
|
||||
case .config,
|
||||
.plugin,
|
||||
.task:
|
||||
return []
|
||||
case .dependencies,
|
||||
.galaxy,
|
||||
.project,
|
||||
.setup,
|
||||
.template,
|
||||
.workspace:
|
||||
return try projectDescriptionHelpersBuilderFactory.projectDescriptionHelpersBuilder(
|
||||
cacheDirectory: projectDescriptionHelpersCacheDirectory
|
||||
)
|
||||
.build(
|
||||
at: path,
|
||||
projectDescriptionSearchPaths: searchPaths,
|
||||
projectDescriptionHelperPlugins: plugins.projectDescriptionHelpers
|
||||
).flatMap { [
|
||||
"-I", $0.path.parentDirectory.pathString,
|
||||
"-L", $0.path.parentDirectory.pathString,
|
||||
"-F", $0.path.parentDirectory.pathString,
|
||||
"-l\($0.name)",
|
||||
] }
|
||||
}
|
||||
}()
|
||||
|
||||
arguments.append(contentsOf: projectDescriptionHelperArguments)
|
||||
arguments.append(path.pathString)
|
||||
|
||||
return arguments
|
||||
}
|
||||
|
||||
private func logUnexpectedImportErrorIfNeeded(in path: AbsolutePath, error: Error, manifest: Manifest) {
|
||||
guard case let TuistSupport.SystemError.terminated(command, _, standardError) = error,
|
||||
manifest == .config || manifest == .plugin,
|
||||
|
|
|
@ -10,6 +10,7 @@ public enum Manifest: CaseIterable {
|
|||
case galaxy
|
||||
case dependencies
|
||||
case plugin
|
||||
case task
|
||||
|
||||
/// This was introduced to rename a file name without breaking existing projects.
|
||||
public var deprecatedFileName: String? {
|
||||
|
@ -44,6 +45,8 @@ public enum Manifest: CaseIterable {
|
|||
return "Dependencies.swift"
|
||||
case .plugin:
|
||||
return "Plugin.swift"
|
||||
case .task:
|
||||
return "\(path.basenameWithoutExt).swift"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import TSCBasic
|
|||
import TuistSupport
|
||||
|
||||
public protocol ResourceLocating: AnyObject {
|
||||
func projectAutomation() throws -> AbsolutePath
|
||||
func projectDescription() throws -> AbsolutePath
|
||||
func cliPath() throws -> AbsolutePath
|
||||
}
|
||||
|
@ -37,6 +38,10 @@ public final class ResourceLocator: ResourceLocating {
|
|||
|
||||
// MARK: - ResourceLocating
|
||||
|
||||
public func projectAutomation() throws -> AbsolutePath {
|
||||
try frameworkPath("ProjectAutomation")
|
||||
}
|
||||
|
||||
public func projectDescription() throws -> AbsolutePath {
|
||||
try frameworkPath("ProjectDescription")
|
||||
}
|
||||
|
|
|
@ -79,5 +79,10 @@ public final class MockManifestLoader: ManifestLoading {
|
|||
return try loadPluginStub?(path) ?? Plugin.test()
|
||||
}
|
||||
|
||||
public var taskLoadArgumentsStub: ((AbsolutePath) throws -> [String])?
|
||||
public func taskLoadArguments(at path: AbsolutePath) throws -> [String] {
|
||||
try taskLoadArgumentsStub?(path) ?? []
|
||||
}
|
||||
|
||||
public func register(plugins _: Plugins) {}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ extension Config {
|
|||
|
||||
extension Template {
|
||||
public static func test(description: String = "Template",
|
||||
attributes: [Template.Attribute] = [],
|
||||
attributes: [Attribute] = [],
|
||||
files: [Template.File] = []) -> Template
|
||||
{
|
||||
Template(
|
||||
|
|
|
@ -10,6 +10,11 @@ public final class MockResourceLocator: ResourceLocating {
|
|||
public var embedPathCount: UInt = 0
|
||||
public var embedPathStub: (() throws -> AbsolutePath)?
|
||||
|
||||
public var projectAutomationStub: (() throws -> AbsolutePath)?
|
||||
public func projectAutomation() throws -> AbsolutePath {
|
||||
try projectAutomationStub?() ?? AbsolutePath("/")
|
||||
}
|
||||
|
||||
public func projectDescription() throws -> AbsolutePath {
|
||||
projectDescriptionCount += 1
|
||||
return try projectDescriptionStub?() ?? AbsolutePath("/")
|
||||
|
|
|
@ -13,6 +13,7 @@ public enum Constants {
|
|||
|
||||
public static let helpersDirectoryName: String = "ProjectDescriptionHelpers"
|
||||
public static let signingDirectoryName: String = "Signing"
|
||||
public static let tasksDirectoryName: String = "Tasks"
|
||||
|
||||
public static let masterKey = "master.key"
|
||||
public static let encryptedExtension = "encrypted"
|
||||
|
|
|
@ -122,17 +122,28 @@ extension String {
|
|||
return ([first] + rest).joined(separator: "")
|
||||
}
|
||||
|
||||
public func camelCaseToSnakeCase() -> String {
|
||||
let acronymPattern = "([A-Z]+)([A-Z][a-z]|[0-9])"
|
||||
let normalPattern = "([a-z0-9])([A-Z])"
|
||||
return processCamalCaseRegex(pattern: acronymPattern)?
|
||||
.processCamalCaseRegex(pattern: normalPattern)?.lowercased() ?? lowercased()
|
||||
public func camelCaseToKebabCase() -> String {
|
||||
convertCamelCase(separator: "-")
|
||||
}
|
||||
|
||||
fileprivate func processCamalCaseRegex(pattern: String) -> String? {
|
||||
public func camelCaseToSnakeCase() -> String {
|
||||
convertCamelCase(separator: "_")
|
||||
}
|
||||
|
||||
private func convertCamelCase(separator: String) -> String {
|
||||
let acronymPattern = "([A-Z]+)([A-Z][a-z]|[0-9])"
|
||||
let normalPattern = "([a-z0-9])([A-Z])"
|
||||
return processCamelCaseRegex(pattern: acronymPattern, separator: separator)?
|
||||
.processCamelCaseRegex(pattern: normalPattern, separator: separator)?.lowercased() ?? lowercased()
|
||||
}
|
||||
|
||||
private func processCamelCaseRegex(
|
||||
pattern: String,
|
||||
separator: String
|
||||
) -> String? {
|
||||
let regex = try? NSRegularExpression(pattern: pattern, options: [])
|
||||
let range = NSRange(location: 0, length: count)
|
||||
return regex?.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "$1_$2")
|
||||
return regex?.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "$1\(separator)$2")
|
||||
}
|
||||
|
||||
// MARK: - Shell
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistSupport
|
||||
|
||||
/// Finds tasks.
|
||||
public protocol TasksLocating {
|
||||
/// Returns paths to user-defined tasks.
|
||||
func locateTasks(at path: AbsolutePath) throws -> [AbsolutePath]
|
||||
}
|
||||
|
||||
public final class TasksLocator: TasksLocating {
|
||||
private let rootDirectoryLocator: RootDirectoryLocating
|
||||
|
||||
/// Default constructor.
|
||||
public init(rootDirectoryLocator: RootDirectoryLocating = RootDirectoryLocator()) {
|
||||
self.rootDirectoryLocator = rootDirectoryLocator
|
||||
}
|
||||
|
||||
public func locateTasks(at path: AbsolutePath) throws -> [AbsolutePath] {
|
||||
guard let rootDirectory = rootDirectoryLocator.locate(from: path) else { return [] }
|
||||
let tasksDirectory = rootDirectory.appending(
|
||||
components: Constants.tuistDirectoryName, Constants.tasksDirectoryName
|
||||
)
|
||||
guard FileHandler.shared.exists(tasksDirectory) else { return [] }
|
||||
return try FileHandler.shared.contentsOfDirectory(tasksDirectory)
|
||||
.filter { $0.extension == "swift" }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import TSCBasic
|
||||
import TuistTasks
|
||||
|
||||
public final class MockTasksLocator: TasksLocating {
|
||||
public init() {}
|
||||
|
||||
public var locateTasksStub: ((AbsolutePath) throws -> [AbsolutePath])?
|
||||
public func locateTasks(at path: AbsolutePath) throws -> [AbsolutePath] {
|
||||
try locateTasksStub?(path) ?? []
|
||||
}
|
||||
}
|
|
@ -23,7 +23,9 @@ final class MockProjectEditorMapper: ProjectEditorMapping {
|
|||
pluginProjectDescriptionHelpersModule: [ProjectDescriptionHelpersModule],
|
||||
helpers: [AbsolutePath],
|
||||
templates: [AbsolutePath],
|
||||
projectDescriptionPath: AbsolutePath
|
||||
tasks: [AbsolutePath],
|
||||
projectDescriptionPath: AbsolutePath,
|
||||
projectAutomationPath: AbsolutePath
|
||||
)] = []
|
||||
|
||||
func map(
|
||||
|
@ -39,7 +41,9 @@ final class MockProjectEditorMapper: ProjectEditorMapping {
|
|||
pluginProjectDescriptionHelpersModule: [ProjectDescriptionHelpersModule],
|
||||
helpers: [AbsolutePath],
|
||||
templates: [AbsolutePath],
|
||||
projectDescriptionPath: AbsolutePath
|
||||
tasks: [AbsolutePath],
|
||||
projectDescriptionPath: AbsolutePath,
|
||||
projectAutomationPath: AbsolutePath
|
||||
) throws -> ValueGraph {
|
||||
mapArgs.append((
|
||||
name: name,
|
||||
|
@ -54,7 +58,9 @@ final class MockProjectEditorMapper: ProjectEditorMapping {
|
|||
pluginProjectDescriptionHelpersModule: pluginProjectDescriptionHelpersModule,
|
||||
helpers: helpers,
|
||||
templates: templates,
|
||||
projectDescriptionPath: projectDescriptionPath
|
||||
tasks: tasks,
|
||||
projectDescriptionPath: projectDescriptionPath,
|
||||
projectAutomationPath: projectAutomationPath
|
||||
))
|
||||
|
||||
if let mapStub = mapStub { return mapStub }
|
||||
|
|
|
@ -23,7 +23,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
|
|||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_edit_when_there_are_helpers_and_setup_and_config_and_dependencies() throws {
|
||||
func test_edit_when_there_are_helpers_and_setup_and_config_and_dependencies_and_tasks() throws {
|
||||
// Given
|
||||
let sourceRootPath = try temporaryPath()
|
||||
let projectManifestPaths = [sourceRootPath].map { $0.appending(component: "Project.swift") }
|
||||
|
@ -36,6 +36,10 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
|
|||
let tuistPath = AbsolutePath("/usr/bin/foo/bar/tuist")
|
||||
let projectName = "Manifests"
|
||||
let projectsGroup = ProjectGroup.group(name: projectName)
|
||||
let tasksPaths = [
|
||||
sourceRootPath.appending(component: "TaskOne.swift"),
|
||||
sourceRootPath.appending(component: "TaskTwo.swift"),
|
||||
]
|
||||
|
||||
// When
|
||||
let graph = try subject.map(
|
||||
|
@ -51,7 +55,9 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
|
|||
pluginProjectDescriptionHelpersModule: [],
|
||||
helpers: helperPaths,
|
||||
templates: templates,
|
||||
projectDescriptionPath: projectDescriptionPath
|
||||
tasks: tasksPaths,
|
||||
projectDescriptionPath: projectDescriptionPath,
|
||||
projectAutomationPath: sourceRootPath.appending(component: "ProjectAutomation.framework")
|
||||
)
|
||||
|
||||
let project = try XCTUnwrap(graph.projects.values.first)
|
||||
|
@ -63,7 +69,7 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
|
|||
// Then
|
||||
XCTAssertEqual(graph.name, "TestManifests")
|
||||
|
||||
XCTAssertEqual(targets.count, 6)
|
||||
XCTAssertEqual(targets.count, 8)
|
||||
XCTAssertEqual(project.targets.sorted { $0.name < $1.name }, targets)
|
||||
|
||||
// Generated Manifests target
|
||||
|
@ -137,6 +143,30 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(dependenciesTarget.filesGroup, projectsGroup)
|
||||
XCTAssertEmpty(dependenciesTarget.dependencies)
|
||||
|
||||
// Generated TaskOne target
|
||||
let taskOneTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "TaskOne" }))
|
||||
XCTAssertTrue(targets.contains(taskOneTarget))
|
||||
|
||||
XCTAssertEqual(taskOneTarget.name, "TaskOne")
|
||||
XCTAssertEqual(taskOneTarget.platform, .macOS)
|
||||
XCTAssertEqual(taskOneTarget.product, .staticFramework)
|
||||
XCTAssertEqual(taskOneTarget.settings, expectedSettings(includePaths: [sourceRootPath]))
|
||||
XCTAssertEqual(taskOneTarget.sources.map(\.path), [tasksPaths[0]])
|
||||
XCTAssertEqual(taskOneTarget.filesGroup, projectsGroup)
|
||||
XCTAssertEmpty(taskOneTarget.dependencies)
|
||||
|
||||
// Generated TaskTwo target
|
||||
let taskTwoTarget = try XCTUnwrap(project.targets.last(where: { $0.name == "TaskTwo" }))
|
||||
XCTAssertTrue(targets.contains(taskTwoTarget))
|
||||
|
||||
XCTAssertEqual(taskTwoTarget.name, "TaskTwo")
|
||||
XCTAssertEqual(taskTwoTarget.platform, .macOS)
|
||||
XCTAssertEqual(taskTwoTarget.product, .staticFramework)
|
||||
XCTAssertEqual(taskTwoTarget.settings, expectedSettings(includePaths: [sourceRootPath]))
|
||||
XCTAssertEqual(taskTwoTarget.sources.map(\.path), [tasksPaths[1]])
|
||||
XCTAssertEqual(taskTwoTarget.filesGroup, projectsGroup)
|
||||
XCTAssertEmpty(taskTwoTarget.dependencies)
|
||||
|
||||
// Generated Project
|
||||
XCTAssertEqual(project.path, sourceRootPath.appending(component: projectName))
|
||||
XCTAssertEqual(project.name, projectName)
|
||||
|
@ -187,7 +217,9 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
|
|||
pluginProjectDescriptionHelpersModule: [],
|
||||
helpers: helperPaths,
|
||||
templates: templates,
|
||||
projectDescriptionPath: projectDescriptionPath
|
||||
tasks: [],
|
||||
projectDescriptionPath: projectDescriptionPath,
|
||||
projectAutomationPath: sourceRootPath.appending(component: "ProjectAutomation.framework")
|
||||
)
|
||||
|
||||
let project = try XCTUnwrap(graph.projects.values.first)
|
||||
|
@ -266,7 +298,9 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
|
|||
pluginProjectDescriptionHelpersModule: [],
|
||||
helpers: helperPaths,
|
||||
templates: templates,
|
||||
projectDescriptionPath: projectDescriptionPath
|
||||
tasks: [],
|
||||
projectDescriptionPath: projectDescriptionPath,
|
||||
projectAutomationPath: sourceRootPath.appending(component: "ProjectAutomation.framework")
|
||||
)
|
||||
|
||||
let project = try XCTUnwrap(graph.projects.values.first)
|
||||
|
@ -375,7 +409,9 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
|
|||
pluginProjectDescriptionHelpersModule: [],
|
||||
helpers: helperPaths,
|
||||
templates: templates,
|
||||
projectDescriptionPath: projectDescriptionPath
|
||||
tasks: [],
|
||||
projectDescriptionPath: projectDescriptionPath,
|
||||
projectAutomationPath: sourceRootPath.appending(component: "ProjectAutomation.framework")
|
||||
)
|
||||
|
||||
let project = try XCTUnwrap(graph.projects.values.first)
|
||||
|
@ -451,7 +487,9 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
|
|||
pluginProjectDescriptionHelpersModule: [],
|
||||
helpers: helperPaths,
|
||||
templates: templates,
|
||||
projectDescriptionPath: projectDescriptionPath
|
||||
tasks: [],
|
||||
projectDescriptionPath: projectDescriptionPath,
|
||||
projectAutomationPath: sourceRootPath.appending(component: "ProjectAutomation.framework")
|
||||
)
|
||||
|
||||
let project = try XCTUnwrap(graph.projects.values.first)
|
||||
|
@ -558,7 +596,9 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
|
|||
pluginProjectDescriptionHelpersModule: [],
|
||||
helpers: helperPaths,
|
||||
templates: templates,
|
||||
projectDescriptionPath: projectDescriptionPath
|
||||
tasks: [],
|
||||
projectDescriptionPath: projectDescriptionPath,
|
||||
projectAutomationPath: sourceRootPath.appending(component: "ProjectAutomation.framework")
|
||||
)
|
||||
|
||||
// Then
|
||||
|
@ -597,7 +637,9 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
|
|||
pluginProjectDescriptionHelpersModule: [.init(name: "Plugin", path: pluginHelpersPath)],
|
||||
helpers: helperPaths,
|
||||
templates: templates,
|
||||
projectDescriptionPath: projectDescriptionPath
|
||||
tasks: [],
|
||||
projectDescriptionPath: projectDescriptionPath,
|
||||
projectAutomationPath: sourceRootPath.appending(component: "ProjectAutomation.framework")
|
||||
)
|
||||
|
||||
let project = try XCTUnwrap(graph.projects.values.first)
|
||||
|
|
|
@ -9,6 +9,7 @@ import TuistPluginTesting
|
|||
import TuistSupport
|
||||
import XCTest
|
||||
|
||||
import TuistTasksTesting
|
||||
@testable import TuistCoreTesting
|
||||
@testable import TuistGeneratorTesting
|
||||
@testable import TuistKit
|
||||
|
@ -27,16 +28,17 @@ final class ProjectEditorErrorTests: TuistUnitTestCase {
|
|||
}
|
||||
|
||||
final class ProjectEditorTests: TuistUnitTestCase {
|
||||
var generator: MockDescriptorGenerator!
|
||||
var projectEditorMapper: MockProjectEditorMapper!
|
||||
var resourceLocator: MockResourceLocator!
|
||||
var manifestFilesLocator: MockManifestFilesLocator!
|
||||
var helpersDirectoryLocator: MockHelpersDirectoryLocator!
|
||||
var writer: MockXcodeProjWriter!
|
||||
var templatesDirectoryLocator: MockTemplatesDirectoryLocator!
|
||||
var projectDescriptionHelpersBuilder: MockProjectDescriptionHelpersBuilder!
|
||||
var projectDescriptionHelpersBuilderFactory: MockProjectDescriptionHelpersBuilderFactory!
|
||||
var subject: ProjectEditor!
|
||||
private var generator: MockDescriptorGenerator!
|
||||
private var projectEditorMapper: MockProjectEditorMapper!
|
||||
private var resourceLocator: MockResourceLocator!
|
||||
private var manifestFilesLocator: MockManifestFilesLocator!
|
||||
private var helpersDirectoryLocator: MockHelpersDirectoryLocator!
|
||||
private var writer: MockXcodeProjWriter!
|
||||
private var templatesDirectoryLocator: MockTemplatesDirectoryLocator!
|
||||
private var projectDescriptionHelpersBuilder: MockProjectDescriptionHelpersBuilder!
|
||||
private var projectDescriptionHelpersBuilderFactory: MockProjectDescriptionHelpersBuilderFactory!
|
||||
private var tasksLocator: MockTasksLocator!
|
||||
private var subject: ProjectEditor!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
@ -50,6 +52,7 @@ final class ProjectEditorTests: TuistUnitTestCase {
|
|||
projectDescriptionHelpersBuilder = MockProjectDescriptionHelpersBuilder()
|
||||
projectDescriptionHelpersBuilderFactory = MockProjectDescriptionHelpersBuilderFactory()
|
||||
projectDescriptionHelpersBuilderFactory.projectDescriptionHelpersBuilderStub = { _ in self.projectDescriptionHelpersBuilder }
|
||||
tasksLocator = MockTasksLocator()
|
||||
|
||||
subject = ProjectEditor(
|
||||
generator: generator,
|
||||
|
@ -59,25 +62,28 @@ final class ProjectEditorTests: TuistUnitTestCase {
|
|||
helpersDirectoryLocator: helpersDirectoryLocator,
|
||||
writer: writer,
|
||||
templatesDirectoryLocator: templatesDirectoryLocator,
|
||||
projectDescriptionHelpersBuilderFactory: projectDescriptionHelpersBuilderFactory
|
||||
projectDescriptionHelpersBuilderFactory: projectDescriptionHelpersBuilderFactory,
|
||||
tasksLocator: tasksLocator
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
generator = nil
|
||||
projectEditorMapper = nil
|
||||
resourceLocator = nil
|
||||
manifestFilesLocator = nil
|
||||
helpersDirectoryLocator = nil
|
||||
templatesDirectoryLocator = nil
|
||||
tasksLocator = nil
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_edit() throws {
|
||||
// Given
|
||||
let directory = try temporaryPath()
|
||||
let projectDescriptionPath = directory.appending(component: "ProjectDescription.framework")
|
||||
let projectAutomationPath = directory.appending(component: "ProjectAutomation.framework")
|
||||
let graph = ValueGraph.test(name: "Edit")
|
||||
let helpersDirectory = directory.appending(component: "ProjectDescriptionHelpers")
|
||||
try FileHandler.shared.createFolder(helpersDirectory)
|
||||
|
@ -88,13 +94,21 @@ final class ProjectEditorTests: TuistUnitTestCase {
|
|||
let setupPath = directory.appending(components: "Setup.swift")
|
||||
let configPath = directory.appending(components: "Tuist", "Config.swift")
|
||||
let dependenciesPath = directory.appending(components: "Tuist", "Dependencies.swif")
|
||||
let locateTasksPaths = [
|
||||
directory.appending(components: "Tuist", "Tasks", "TaskOne.swift"),
|
||||
directory.appending(components: "Tuist", "Tasks", "TaskTwo.swift"),
|
||||
]
|
||||
|
||||
resourceLocator.projectDescriptionStub = { projectDescriptionPath }
|
||||
resourceLocator.projectAutomationStub = { projectAutomationPath }
|
||||
manifestFilesLocator.locateProjectManifestsStub = manifests
|
||||
manifestFilesLocator.locateConfigStub = configPath
|
||||
manifestFilesLocator.locateDependenciesStub = dependenciesPath
|
||||
manifestFilesLocator.locateSetupStub = setupPath
|
||||
helpersDirectoryLocator.locateStub = helpersDirectory
|
||||
tasksLocator.locateTasksStub = { _ in
|
||||
locateTasksPaths
|
||||
}
|
||||
projectEditorMapper.mapStub = graph
|
||||
generator.generateWorkspaceStub = { _ in
|
||||
.test(xcworkspacePath: directory.appending(component: "Edit.xcworkspacepath"))
|
||||
|
@ -110,9 +124,11 @@ final class ProjectEditorTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(mapArgs?.helpers, helpers)
|
||||
XCTAssertEqual(mapArgs?.sourceRootPath, directory)
|
||||
XCTAssertEqual(mapArgs?.projectDescriptionPath, projectDescriptionPath)
|
||||
XCTAssertEqual(mapArgs?.projectAutomationPath, projectAutomationPath)
|
||||
XCTAssertEqual(mapArgs?.configPath, configPath)
|
||||
XCTAssertEqual(mapArgs?.setupPath, setupPath)
|
||||
XCTAssertEqual(mapArgs?.dependenciesPath, dependenciesPath)
|
||||
XCTAssertEqual(mapArgs?.tasks, locateTasksPaths)
|
||||
}
|
||||
|
||||
func test_edit_when_there_are_no_editable_files() throws {
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistLoaderTesting
|
||||
import TuistSupport
|
||||
import TuistSupportTesting
|
||||
import TuistTasksTesting
|
||||
import XCTest
|
||||
|
||||
@testable import TuistKit
|
||||
|
||||
final class TaskServiceTests: TuistUnitTestCase {
|
||||
private var manifestLoader: MockManifestLoader!
|
||||
private var tasksLocator: MockTasksLocator!
|
||||
private var subject: ExecService!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
manifestLoader = MockManifestLoader()
|
||||
tasksLocator = MockTasksLocator()
|
||||
subject = ExecService(
|
||||
manifestLoader: manifestLoader,
|
||||
tasksLocator: tasksLocator
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
manifestLoader = nil
|
||||
tasksLocator = nil
|
||||
subject = nil
|
||||
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_load_task_options_when_task_not_found() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
tasksLocator.locateTasksStub = { path in
|
||||
[
|
||||
path.appending(component: "TaskB.swift"),
|
||||
path.appending(component: "TaskC.swift"),
|
||||
]
|
||||
}
|
||||
|
||||
// When / Then
|
||||
XCTAssertThrowsSpecific(
|
||||
try subject.loadTaskOptions(taskName: "task-a", path: path.pathString),
|
||||
ExecError.taskNotFound("task-a", ["task-b", "task-c"])
|
||||
)
|
||||
}
|
||||
|
||||
func test_load_task_options_when_none() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let taskPath = path.appending(component: "TaskA.swift")
|
||||
try fileHandler.write(
|
||||
"""
|
||||
import ProjectAutomation
|
||||
import Foundation
|
||||
|
||||
let task = Task(
|
||||
options: [
|
||||
]
|
||||
) { options in
|
||||
// some task code
|
||||
}
|
||||
|
||||
""",
|
||||
path: taskPath,
|
||||
atomically: true
|
||||
)
|
||||
tasksLocator.locateTasksStub = { path in
|
||||
[
|
||||
path.appending(component: "TaskA.swift"),
|
||||
path.appending(component: "TaskB.swift"),
|
||||
]
|
||||
}
|
||||
|
||||
// When / Then
|
||||
XCTAssertEmpty(
|
||||
try subject.loadTaskOptions(
|
||||
taskName: "task-a",
|
||||
path: path.pathString
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func test_load_task_options_when_defined_on_single_line() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let taskPath = path.appending(component: "TaskA.swift")
|
||||
try fileHandler.write(
|
||||
"""
|
||||
import ProjectAutomation
|
||||
import Foundation
|
||||
|
||||
let task = Task(
|
||||
options: [.option("option-a"), .option("option-b")]
|
||||
) { options in
|
||||
// some task code
|
||||
}
|
||||
|
||||
""",
|
||||
path: taskPath,
|
||||
atomically: true
|
||||
)
|
||||
tasksLocator.locateTasksStub = { path in
|
||||
[
|
||||
path.appending(component: "TaskA.swift"),
|
||||
path.appending(component: "TaskB.swift"),
|
||||
]
|
||||
}
|
||||
|
||||
// When / Then
|
||||
XCTAssertEqual(
|
||||
try subject.loadTaskOptions(
|
||||
taskName: "task-a",
|
||||
path: path.pathString
|
||||
),
|
||||
[
|
||||
"option-a",
|
||||
"option-b",
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func test_load_task_options_when_defined_on_multiple_lines() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let taskPath = path.appending(component: "TaskA.swift")
|
||||
try fileHandler.write(
|
||||
"""
|
||||
import ProjectAutomation
|
||||
import Foundation
|
||||
|
||||
let task = Task(
|
||||
options: [
|
||||
.option("option-a"),
|
||||
.option("option-b")
|
||||
]
|
||||
) { options in
|
||||
// some task code
|
||||
}
|
||||
|
||||
""",
|
||||
path: taskPath,
|
||||
atomically: true
|
||||
)
|
||||
tasksLocator.locateTasksStub = { path in
|
||||
[
|
||||
path.appending(component: "TaskA.swift"),
|
||||
path.appending(component: "TaskB.swift"),
|
||||
]
|
||||
}
|
||||
|
||||
// When / Then
|
||||
XCTAssertEqual(
|
||||
try subject.loadTaskOptions(
|
||||
taskName: "task-a",
|
||||
path: path.pathString
|
||||
),
|
||||
[
|
||||
"option-a",
|
||||
"option-b",
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func test_run_when_task_not_found() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
tasksLocator.locateTasksStub = { path in
|
||||
[
|
||||
path.appending(component: "TaskB.swift"),
|
||||
path.appending(component: "TaskC.swift"),
|
||||
]
|
||||
}
|
||||
|
||||
// When / Then
|
||||
XCTAssertThrowsSpecific(
|
||||
try subject.run("task-a", options: [:], path: path.pathString),
|
||||
ExecError.taskNotFound("task-a", ["task-b", "task-c"])
|
||||
)
|
||||
}
|
||||
|
||||
func test_run_when_task_found() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let taskPath = path.appending(component: "TaskA.swift")
|
||||
tasksLocator.locateTasksStub = { path in
|
||||
[
|
||||
path.appending(component: "TaskA.swift"),
|
||||
path.appending(component: "TaskB.swift"),
|
||||
path.appending(component: "TaskC.swift"),
|
||||
]
|
||||
}
|
||||
manifestLoader.taskLoadArgumentsStub = {
|
||||
[
|
||||
"load",
|
||||
$0.pathString,
|
||||
]
|
||||
}
|
||||
system.succeedCommand(
|
||||
"load",
|
||||
taskPath.pathString,
|
||||
"--tuist-task",
|
||||
"{\"option-a\":\"Value\"}"
|
||||
)
|
||||
|
||||
// When / Then
|
||||
XCTAssertNoThrow(
|
||||
try subject.run(
|
||||
"task-a",
|
||||
options: ["option-a": "Value"],
|
||||
path: path.pathString
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistSupport
|
||||
import TuistSupportTesting
|
||||
import XCTest
|
||||
|
||||
@testable import TuistTasks
|
||||
|
||||
final class TasksLocatorIntegrationTests: TuistTestCase {
|
||||
var subject: TasksLocator!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
subject = TasksLocator()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_locate_when_a_tasks_directory_exists() throws {
|
||||
// Given
|
||||
let temporaryDirectory = try temporaryPath()
|
||||
try createFolders(["this/is/a/very/nested/directory", "this/is/Tuist/Tasks"])
|
||||
let tasksPath = temporaryDirectory.appending(RelativePath("this/is/Tuist/Tasks"))
|
||||
try createFiles(
|
||||
[
|
||||
"this/is/Tuist/Tasks/TaskA.swift",
|
||||
"this/is/Tuist/Tasks/TaskB.swift",
|
||||
]
|
||||
)
|
||||
|
||||
// When
|
||||
let got = try subject.locateTasks(at: temporaryDirectory.appending(RelativePath("this/is/a/very/nested/directory")))
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(
|
||||
got.sorted(),
|
||||
[
|
||||
tasksPath.appending(component: "TaskA.swift"),
|
||||
tasksPath.appending(component: "TaskB.swift"),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func test_locate_when_a_tasks_directory_does_not_exist() throws {
|
||||
// Given
|
||||
let temporaryDirectory = try temporaryPath()
|
||||
try createFolders(["this/is/a/very/nested/directory"])
|
||||
|
||||
// When
|
||||
let got = try subject.locateTasks(at: temporaryDirectory.appending(RelativePath("this/is/a/very/nested/directory")))
|
||||
|
||||
// Then
|
||||
XCTAssertEmpty(got)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
title: Run tasks
|
||||
slug: '/commands/task'
|
||||
description: 'Learn how to to automate arbitrary tasks with tuist Swift.'
|
||||
---
|
||||
|
||||
### Context
|
||||
|
||||
When we write apps, it is often necessary to write some supporting code for e.g. releasing, downloading localizations, etc.
|
||||
These are often written in Shell or Ruby which only a handful of developers on your team might be familiar with.
|
||||
This means that these files are edited by an exclusive group and they are sort of "magical" for others.
|
||||
We try to fix that by introducing a concept of "Tasks" where you can define custom commands - in Swift!
|
||||
|
||||
### Defining a task
|
||||
|
||||
To define a task, you can run `tuist edit` and then create a file `NameOfCommand.swift` in `Tuist/Tasks` directory.
|
||||
Afterwards, you will need to define the task's options (if there are any) and the code that should be executed when the task is run.
|
||||
Below you can find an example of the `CreateFile` task:
|
||||
|
||||
```swift
|
||||
import ProjectAutomation
|
||||
import Foundation
|
||||
|
||||
let task = Task(
|
||||
options: [
|
||||
.option("file-name"),
|
||||
]
|
||||
) { options in
|
||||
let fileName = options["file-name"] ?? "file"
|
||||
try "File created with a task".write(
|
||||
to: URL(fileURLWithPath: "\(fileName).txt"),
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
print("File created!")
|
||||
}
|
||||
```
|
||||
|
||||
If you added this file to `Tuist/Tasks/CreateFile.swift`, you can run it by `tuist exec create-file --file-name MyFileName`.
|
||||
The `Task` accepts two parameters - `options: [Option]` which defines the possible options of the task.
|
||||
Then there is a parameter `task: ([String: String]) throws -> Void` which is a simple closure that is executed when the task is run.
|
||||
Note that the closure has input of `[String: String]` -
|
||||
this is a dictionary of options defined by the user where the key is the name of the option and value is the option's value.
|
|
@ -4,10 +4,10 @@ Then(/tuist scaffolds a (.+) template to (.+) named (.+)/) do |template, path, n
|
|||
system("swift", "run", "tuist", "scaffold", template, "--path", File.join(@dir, path), "--name", name)
|
||||
end
|
||||
|
||||
Then(/content of a file named (.+) in a directory (.+) should be equal to (.+)/) do |file, dir, content|
|
||||
Then(/content of a file named ([a-zA-Z\-_]+) in a directory (.+) should be equal to (.+)/) do |file, dir, content|
|
||||
assert_equal File.read(File.join(@dir, dir, file)), content
|
||||
end
|
||||
|
||||
Then(/content of a file named (.+) in a directory (.+) should be equal to:$/) do |file, dir, content|
|
||||
Then(/content of a file named ([a-zA-Z\-_]+) in a directory (.+) should be equal to:$/) do |file, dir, content|
|
||||
assert_equal File.read(File.join(@dir, dir, file)), content
|
||||
end
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Then(/^tuist runs a task ([a-zA-Z\-]+)$/) do |name|
|
||||
system("swift", "run", "tuist", "exec", name, "--path", @dir)
|
||||
end
|
||||
|
||||
Then(/^tuist runs a task (.+) with attribute (.+) as (.+)$/) do |name, attribute, attribute_value|
|
||||
system("swift", "run", "tuist", "exec", name, "--#{attribute}", attribute_value, "--path", @dir)
|
||||
end
|
||||
|
||||
Then(/^content of a file named ([a-zA-Z\-_]+) should be equal to (.+)$/) do |file, content|
|
||||
assert_equal File.read(File.join(@dir, file)), content
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
Feature: Run tasks
|
||||
Scenario: The project is an application with tasks (app_with_tasks)
|
||||
Given that tuist is available
|
||||
And I have a working directory
|
||||
Then I copy the fixture app_with_tasks into the working directory
|
||||
Then tuist runs a task create-file
|
||||
Then content of a file named file.txt should be equal to File created with a task
|
||||
Then tuist runs a task create-file with attribute file-name as custom-file
|
||||
Then content of a file named custom-file.txt should be equal to File created with a task
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Xcode ###
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
|
||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
### Xcode Patch ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
/*.gcno
|
||||
|
||||
### Projects ###
|
||||
*.xcodeproj
|
||||
*.xcworkspace
|
||||
|
||||
### Tuist derived files ###
|
||||
graph.dot
|
||||
Derived/
|
||||
|
||||
### Tuist managed dependencies ###
|
||||
Tuist/Dependencies
|
||||
|
||||
*.txt
|
|
@ -0,0 +1,25 @@
|
|||
import ProjectDescription
|
||||
import ProjectDescriptionHelpers
|
||||
|
||||
/*
|
||||
+-------------+
|
||||
| |
|
||||
| App | Contains App App target and App unit-test target
|
||||
| |
|
||||
+------+-------------+-------+
|
||||
| depends on |
|
||||
| |
|
||||
+----v-----+ +-----v-----+
|
||||
| | | |
|
||||
| Kit | | UI | Two independent frameworks to share code and start modularising your app
|
||||
| | | |
|
||||
+----------+ +-----------+
|
||||
|
||||
*/
|
||||
|
||||
// MARK: - Project
|
||||
|
||||
// Creates our project using a helper function defined in ProjectDescriptionHelpers
|
||||
let project = Project.app(name: "App",
|
||||
platform: .iOS,
|
||||
additionalTargets: ["AppKit", "AppUI"])
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
|
@ -0,0 +1,25 @@
|
|||
import UIKit
|
||||
import AppKit
|
||||
import AppUI
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
|
||||
) -> Bool {
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
let viewController = UIViewController()
|
||||
viewController.view.backgroundColor = .white
|
||||
window?.rootViewController = viewController
|
||||
window?.makeKeyAndVisible()
|
||||
AppKit.hello()
|
||||
AppUI.hello()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
final class AppTests: XCTestCase {
|
||||
func test_twoPlusTwo_isFour() {
|
||||
XCTAssertEqual(2+2, 4)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
public final class AppKit {
|
||||
public static func hello() {
|
||||
print("Hello, from your Kit framework")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
final class AppKitTests: XCTestCase {
|
||||
func test_example() {
|
||||
XCTAssertEqual("AppKit", "AppKit")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
public final class AppUI {
|
||||
public static func hello() {
|
||||
print("Hello, from your UI framework")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
final class AppUITests: XCTestCase {
|
||||
func test_example() {
|
||||
XCTAssertEqual("AppUI", "AppUI")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import ProjectDescription
|
||||
|
||||
let config = Config(
|
||||
generationOptions: []
|
||||
)
|
|
@ -0,0 +1,76 @@
|
|||
import ProjectDescription
|
||||
|
||||
/// Project helpers are functions that simplify the way you define your project.
|
||||
/// Share code to create targets, settings, dependencies,
|
||||
/// Create your own conventions, e.g: a func that makes sure all shared targets are "static frameworks"
|
||||
/// See https://tuist.io/docs/usage/helpers/
|
||||
|
||||
extension Project {
|
||||
/// Helper function to create the Project for this ExampleApp
|
||||
public static func app(name: String, platform: Platform, additionalTargets: [String]) -> Project {
|
||||
var targets = makeAppTargets(name: name,
|
||||
platform: platform,
|
||||
dependencies: additionalTargets.map { TargetDependency.target(name: $0) })
|
||||
targets += additionalTargets.flatMap({ makeFrameworkTargets(name: $0, platform: platform) })
|
||||
return Project(name: name,
|
||||
organizationName: "tuist.io",
|
||||
targets: targets)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Helper function to create a framework target and an associated unit test target
|
||||
private static func makeFrameworkTargets(name: String, platform: Platform) -> [Target] {
|
||||
let sources = Target(name: name,
|
||||
platform: platform,
|
||||
product: .framework,
|
||||
bundleId: "io.tuist.\(name)",
|
||||
infoPlist: .default,
|
||||
sources: ["Targets/\(name)/Sources/**"],
|
||||
resources: [],
|
||||
dependencies: [])
|
||||
let tests = Target(name: "\(name)Tests",
|
||||
platform: platform,
|
||||
product: .unitTests,
|
||||
bundleId: "io.tuist.\(name)Tests",
|
||||
infoPlist: .default,
|
||||
sources: ["Targets/\(name)/Tests/**"],
|
||||
resources: [],
|
||||
dependencies: [.target(name: name)])
|
||||
return [sources, tests]
|
||||
}
|
||||
|
||||
/// Helper function to create the application target and the unit test target.
|
||||
private static func makeAppTargets(name: String, platform: Platform, dependencies: [TargetDependency]) -> [Target] {
|
||||
let platform: Platform = platform
|
||||
let infoPlist: [String: InfoPlist.Value] = [
|
||||
"CFBundleShortVersionString": "1.0",
|
||||
"CFBundleVersion": "1",
|
||||
"UIMainStoryboardFile": "",
|
||||
"UILaunchStoryboardName": "LaunchScreen"
|
||||
]
|
||||
|
||||
let mainTarget = Target(
|
||||
name: name,
|
||||
platform: platform,
|
||||
product: .app,
|
||||
bundleId: "io.tuist.\(name)",
|
||||
infoPlist: .extendingDefault(with: infoPlist),
|
||||
sources: ["Targets/\(name)/Sources/**"],
|
||||
resources: ["Targets/\(name)/Resources/**"],
|
||||
dependencies: dependencies
|
||||
)
|
||||
|
||||
let testTarget = Target(
|
||||
name: "\(name)Tests",
|
||||
platform: platform,
|
||||
product: .unitTests,
|
||||
bundleId: "io.tuist.\(name)Tests",
|
||||
infoPlist: .default,
|
||||
sources: ["Targets/\(name)/Tests/**"],
|
||||
dependencies: [
|
||||
.target(name: "\(name)")
|
||||
])
|
||||
return [mainTarget, testTarget]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import ProjectAutomation
|
||||
import Foundation
|
||||
|
||||
let task = Task(
|
||||
options: [
|
||||
.option("file-name"),
|
||||
]
|
||||
) { options in
|
||||
let fileName = options["file-name"] ?? "file"
|
||||
try "File created with a task".write(
|
||||
to: URL(fileURLWithPath: "\(fileName).txt"),
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
print("File created!")
|
||||
}
|
Loading…
Reference in New Issue