Tasks plugin (#3013)
* 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 * Move Task model to ProjectAutomation * WIP: Editing tasks * Editing tasks * Add task documentation * WIP tests * Add tests * Format code * Rename task command to exec * Create PluginTasks * Add tasks plugin fixture * Add tests * Add documentation * Format code * Edit CHANGELOG * Fix typo * Update projects/docs/docs/plugins/creating-plugins.md Co-authored-by: Luis Padron <luis.padron@compass.com> Co-authored-by: Luis Padron <luis.padron@compass.com> Co-authored-by: Pedro Piñera Buendía <pedro@ppinera.es>
This commit is contained in:
parent
dc664d0ec4
commit
e69f70b03f
|
@ -104,6 +104,8 @@ jobs:
|
|||
'scaffold',
|
||||
'test',
|
||||
'up',
|
||||
'graph',
|
||||
'tasks'
|
||||
]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
|
|
@ -6,6 +6,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
|
|||
|
||||
### Added
|
||||
|
||||
- Add possibility to share tasks via a plugin [#3013](https://github.com/tuist/tuist/pull/3013) by [@fortmarek](https://github.com/fortmarek)
|
||||
- Add option to `Scaffolding` for copy folder with option `.directory(path: "destinationContainerFolder", sourcePath: "sourceFolder")`. [#2985](https://github.com/tuist/tuist/pull/2985) by [@santi-d](https://github.com/santi-d)
|
||||
- Add possibility to specify version of Swift in the `Config.swift` manifest file. [#2998](https://github.com/tuist/tuist/pull/2998) by [@laxmorek](https://github.com/laxmorek)
|
||||
- Add `tuist run` command which allows running schemes of a project. [#2917](https://github.com/tuist/tuist/pull/2917) by [@luispadron](https://github.com/luispadron)
|
||||
|
|
|
@ -9,7 +9,7 @@ public struct Task {
|
|||
}
|
||||
|
||||
public init(
|
||||
options: [Option],
|
||||
options: [Option] = [],
|
||||
task: @escaping ([String: String]) throws -> Void
|
||||
) {
|
||||
self.options = options
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
|
||||
/// Tasks plugin model
|
||||
public struct PluginTasks: Equatable {
|
||||
/// Name of the plugin.
|
||||
public let name: String
|
||||
/// Path to `Tasks` directory where all tasks are located.
|
||||
public let path: AbsolutePath
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
path: AbsolutePath
|
||||
) {
|
||||
self.name = name
|
||||
self.path = path
|
||||
}
|
||||
}
|
|
@ -12,6 +12,9 @@ public struct Plugins: Equatable {
|
|||
/// List of paths pointing to resource templates
|
||||
public let resourceSynthesizers: [PluginResourceSynthesizer]
|
||||
|
||||
/// List of tasks plugins.
|
||||
public let tasks: [PluginTasks]
|
||||
|
||||
/// Creates a `Plugins`.
|
||||
///
|
||||
/// - Parameters:
|
||||
|
@ -21,17 +24,20 @@ public struct Plugins: Equatable {
|
|||
public init(
|
||||
projectDescriptionHelpers: [ProjectDescriptionHelpersPlugin],
|
||||
templatePaths: [AbsolutePath],
|
||||
resourceSynthesizers: [PluginResourceSynthesizer]
|
||||
resourceSynthesizers: [PluginResourceSynthesizer],
|
||||
tasks: [PluginTasks]
|
||||
) {
|
||||
self.projectDescriptionHelpers = projectDescriptionHelpers
|
||||
templateDirectories = templatePaths
|
||||
self.resourceSynthesizers = resourceSynthesizers
|
||||
self.tasks = tasks
|
||||
}
|
||||
|
||||
/// An empty `Plugins`.
|
||||
public static let none: Plugins = .init(
|
||||
projectDescriptionHelpers: [],
|
||||
templatePaths: [],
|
||||
resourceSynthesizers: []
|
||||
resourceSynthesizers: [],
|
||||
tasks: []
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,12 +6,14 @@ public extension Plugins {
|
|||
static func test(
|
||||
projectDescriptionHelpers: [ProjectDescriptionHelpersPlugin] = [],
|
||||
templatePaths: [AbsolutePath] = [],
|
||||
resourceSynthesizers: [PluginResourceSynthesizer] = []
|
||||
resourceSynthesizers: [PluginResourceSynthesizer] = [],
|
||||
tasks: [PluginTasks] = []
|
||||
) -> Plugins {
|
||||
Plugins(
|
||||
projectDescriptionHelpers: projectDescriptionHelpers,
|
||||
templatePaths: templatePaths,
|
||||
resourceSynthesizers: resourceSynthesizers
|
||||
resourceSynthesizers: resourceSynthesizers,
|
||||
tasks: tasks
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,13 +29,19 @@ enum ExecError: FatalError, Equatable {
|
|||
struct ExecService {
|
||||
private let manifestLoader: ManifestLoading
|
||||
private let tasksLocator: TasksLocating
|
||||
private let pluginService: PluginServicing
|
||||
private let configLoader: ConfigLoading
|
||||
|
||||
init(
|
||||
manifestLoader: ManifestLoading = ManifestLoader(),
|
||||
tasksLocator: TasksLocating = TasksLocator()
|
||||
tasksLocator: TasksLocating = TasksLocator(),
|
||||
pluginService: PluginServicing = PluginService(),
|
||||
configLoader: ConfigLoading = ConfigLoader(manifestLoader: ManifestLoader())
|
||||
) {
|
||||
self.manifestLoader = manifestLoader
|
||||
self.tasksLocator = tasksLocator
|
||||
self.pluginService = pluginService
|
||||
self.configLoader = configLoader
|
||||
}
|
||||
|
||||
func run(
|
||||
|
@ -88,7 +94,12 @@ struct ExecService {
|
|||
// MARK: - Helpers
|
||||
|
||||
private func task(with name: String, path: AbsolutePath) throws -> AbsolutePath {
|
||||
let tasks: [String: AbsolutePath] = try tasksLocator.locateTasks(at: path)
|
||||
let config = try configLoader.loadConfig(path: path)
|
||||
let plugins = try pluginService.loadPlugins(using: config)
|
||||
let tasksPaths: [AbsolutePath] = try tasksLocator.locateTasks(at: path)
|
||||
+ plugins.tasks.map(\.path)
|
||||
.flatMap(FileHandler.shared.contentsOfDirectory)
|
||||
let tasks: [String: AbsolutePath] = tasksPaths
|
||||
.reduce(into: [:]) { acc, current in
|
||||
acc[current.basenameWithoutExt.camelCaseToKebabCase()] = current
|
||||
}
|
||||
|
|
|
@ -91,10 +91,19 @@ public final class PluginService: PluginServicing {
|
|||
.filter { _, path in FileHandler.shared.exists(path) }
|
||||
.map(PluginResourceSynthesizer.init)
|
||||
|
||||
let tasks = zip(
|
||||
(localPluginManifests + remotePluginManifests).map(\.name),
|
||||
pluginPaths
|
||||
.map { $0.appending(component: Constants.tasksDirectoryName) }
|
||||
)
|
||||
.filter { _, path in FileHandler.shared.exists(path) }
|
||||
.map(PluginTasks.init)
|
||||
|
||||
return Plugins(
|
||||
projectDescriptionHelpers: localProjectDescriptionHelperPlugins + remoteProjectDescriptionHelperPlugins,
|
||||
templatePaths: templatePaths,
|
||||
resourceSynthesizers: resourceSynthesizerPlugins
|
||||
resourceSynthesizers: resourceSynthesizerPlugins,
|
||||
tasks: tasks
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import Foundation
|
||||
import TSCBasic
|
||||
import TuistCore
|
||||
import TuistGraph
|
||||
import TuistLoaderTesting
|
||||
import TuistPluginTesting
|
||||
import TuistSupport
|
||||
import TuistSupportTesting
|
||||
import TuistTasksTesting
|
||||
|
@ -12,21 +14,25 @@ import XCTest
|
|||
final class TaskServiceTests: TuistUnitTestCase {
|
||||
private var manifestLoader: MockManifestLoader!
|
||||
private var tasksLocator: MockTasksLocator!
|
||||
private var pluginService: MockPluginService!
|
||||
private var subject: ExecService!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
manifestLoader = MockManifestLoader()
|
||||
tasksLocator = MockTasksLocator()
|
||||
pluginService = MockPluginService()
|
||||
subject = ExecService(
|
||||
manifestLoader: manifestLoader,
|
||||
tasksLocator: tasksLocator
|
||||
tasksLocator: tasksLocator,
|
||||
pluginService: pluginService
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
manifestLoader = nil
|
||||
tasksLocator = nil
|
||||
pluginService = nil
|
||||
subject = nil
|
||||
|
||||
super.tearDown()
|
||||
|
@ -90,17 +96,7 @@ final class TaskServiceTests: TuistUnitTestCase {
|
|||
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
|
||||
}
|
||||
|
||||
""",
|
||||
Self.taskContent,
|
||||
path: taskPath,
|
||||
atomically: true
|
||||
)
|
||||
|
@ -216,4 +212,59 @@ final class TaskServiceTests: TuistUnitTestCase {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
func test_run_when_task_from_plugin() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let tasksDirectory = path.appending(component: "TaskPlugins")
|
||||
try fileHandler.createFolder(tasksDirectory)
|
||||
let taskPath = tasksDirectory.appending(component: "TaskA.swift")
|
||||
try fileHandler.write(
|
||||
Self.taskContent,
|
||||
path: taskPath,
|
||||
atomically: true
|
||||
)
|
||||
pluginService.loadPluginsStub = { _ in
|
||||
Plugins(
|
||||
projectDescriptionHelpers: [],
|
||||
templatePaths: [],
|
||||
resourceSynthesizers: [],
|
||||
tasks: [
|
||||
PluginTasks(name: "Plugins", path: tasksDirectory),
|
||||
]
|
||||
)
|
||||
}
|
||||
manifestLoader.taskLoadArgumentsStub = {
|
||||
[
|
||||
"load",
|
||||
$0.pathString,
|
||||
]
|
||||
}
|
||||
system.succeedCommand(
|
||||
"load",
|
||||
taskPath.pathString,
|
||||
"--tuist-task",
|
||||
"{}"
|
||||
)
|
||||
|
||||
// When / Then
|
||||
XCTAssertNoThrow(
|
||||
try subject.run(
|
||||
"task-a",
|
||||
options: [:],
|
||||
path: path.pathString
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private static let taskContent = """
|
||||
import ProjectAutomation
|
||||
import Foundation
|
||||
|
||||
let task = Task(
|
||||
options: [.option("option-a"), .option("option-b")]
|
||||
) { options in
|
||||
// some task code
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
|
|
@ -165,6 +165,67 @@ final class PluginServiceTests: TuistTestCase {
|
|||
XCTAssertEqual(plugins, expectedPlugins)
|
||||
}
|
||||
|
||||
func test_loadPlugins_WHEN_localTasks() throws {
|
||||
// Given
|
||||
let pluginPath = try temporaryPath()
|
||||
let pluginName = "TestPlugin"
|
||||
let tasksPath = pluginPath.appending(components: Constants.tasksDirectoryName)
|
||||
|
||||
try makeDirectories(tasksPath)
|
||||
|
||||
// When
|
||||
manifestLoader.loadConfigStub = { _ in
|
||||
.test(plugins: [.local(path: .relativeToRoot(pluginPath.pathString))])
|
||||
}
|
||||
|
||||
manifestLoader.loadPluginStub = { _ in
|
||||
ProjectDescription.Plugin(name: pluginName)
|
||||
}
|
||||
|
||||
let config = mockConfig(plugins: [TuistGraph.PluginLocation.local(path: pluginPath.pathString)])
|
||||
|
||||
// Then
|
||||
let plugins = try subject.loadPlugins(using: config)
|
||||
let expectedPlugins = Plugins.test(
|
||||
tasks: [
|
||||
PluginTasks(name: pluginName, path: tasksPath),
|
||||
]
|
||||
)
|
||||
XCTAssertEqual(plugins, expectedPlugins)
|
||||
}
|
||||
|
||||
func test_loadPlugins_WHEN_gitTasks() throws {
|
||||
// Given
|
||||
let pluginGitUrl = "https://url/to/repo.git"
|
||||
let pluginGitId = "1.0.0"
|
||||
let pluginFingerprint = "\(pluginGitUrl)-\(pluginGitId)".md5
|
||||
let cachedPluginPath = cacheDirectoriesProvider.pluginCacheDirectory.appending(components: pluginFingerprint)
|
||||
let pluginName = "TestPlugin"
|
||||
let tasksPath = cachedPluginPath.appending(components: Constants.tasksDirectoryName)
|
||||
|
||||
try makeDirectories(tasksPath)
|
||||
|
||||
// When
|
||||
manifestLoader.loadConfigStub = { _ in
|
||||
.test(plugins: [ProjectDescription.PluginLocation.git(url: pluginGitUrl, tag: pluginGitId)])
|
||||
}
|
||||
|
||||
manifestLoader.loadPluginStub = { _ in
|
||||
ProjectDescription.Plugin(name: pluginName)
|
||||
}
|
||||
|
||||
let config = mockConfig(plugins: [TuistGraph.PluginLocation.gitWithTag(url: pluginGitUrl, tag: pluginGitId)])
|
||||
|
||||
// Then
|
||||
let plugins = try subject.loadPlugins(using: config)
|
||||
let expectedPlugins = Plugins.test(
|
||||
tasks: [
|
||||
PluginTasks(name: pluginName, path: tasksPath),
|
||||
]
|
||||
)
|
||||
XCTAssertEqual(plugins, expectedPlugins)
|
||||
}
|
||||
|
||||
func test_loadPlugins_WHEN_localTemplate() throws {
|
||||
// Given
|
||||
let pluginPath = try temporaryPath()
|
||||
|
|
|
@ -53,6 +53,22 @@ In order for Tuist to locate the templates for a plugin, they must be placed in
|
|||
└── ...
|
||||
```
|
||||
|
||||
### Tasks
|
||||
|
||||
Tasks serve as automation files written in Swift that can be easily edited in Xcode. Similarly to templates, Tuist finds all tasks defined in `Tasks`.
|
||||
For example, if you wanted to create a plugin task for releasing apps, you can create a file called `ReleaseApp.swift` in the `Tasks` directory of your plugin.
|
||||
To read more about how you can define tasks themselves, head over to the [Tasks documentation](/commands/task).
|
||||
|
||||
```
|
||||
.
|
||||
├── ...
|
||||
├── Plugin.swift
|
||||
├── Tasks
|
||||
├───── ReleaseApp.swift
|
||||
├───── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
## ResourceSynthesizers
|
||||
|
||||
ResourceSynthesizer plugins are for sharing & reusing templates for [synthesizing resources](/guides/resources/). If you want to use one of the predefined resource synthesizers, the template must also adhere to a specific naming.
|
||||
|
|
|
@ -77,6 +77,11 @@ let config = Config(
|
|||
)
|
||||
```
|
||||
|
||||
#### Tasks
|
||||
|
||||
To use a task plugin, simply import the plugin in `Config.swift` and it will be automatically available by running `tuist exec my-plugin-task`.
|
||||
You can read more about tasks [here](/commands/task).
|
||||
|
||||
#### Project description helpers
|
||||
|
||||
You can import a project description helper plugin with the name defined in the [`Plugin.swift`](/plugins/creating-plugins/) manifest, which can then be used in a project manfiest:
|
||||
|
|
|
@ -7,4 +7,10 @@ Feature: Run tasks
|
|||
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
|
||||
|
||||
|
||||
Scenario: The project is an application with plugins (app_with_plugins)
|
||||
Given that tuist is available
|
||||
And I have a working directory
|
||||
Then I copy the fixture app_with_plugins 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 plugin
|
||||
|
|
|
@ -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,11 @@
|
|||
import ProjectAutomation
|
||||
import Foundation
|
||||
|
||||
let task = Task() { options in
|
||||
try "File created with a plugin".write(
|
||||
to: URL(fileURLWithPath: "plugin-file.txt"),
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
print("File created with a plugin!")
|
||||
}
|
Loading…
Reference in New Issue