Compare commits

...

12 Commits

Author SHA1 Message Date
Andrey Nering
73aba36309 v3.20.0 2023-01-14 17:34:15 -03:00
Andrey Nering
cb393ccd3a Add CHANGELOG entry + small adjustments to #977 2023-01-14 17:18:26 -03:00
Amin Yahyaabadi
347fcf9f67 fix: avoid reruns when the timestamp method is used (#977) 2023-01-14 17:17:36 -03:00
Andrey Nering
fce7575b03 Add README entry for #982 2023-01-14 16:48:04 -03:00
Pete Davison
2da7ddc399 chore: optimize task filtering (#982) 2023-01-14 16:45:52 -03:00
Pete Davison
1c1be683ab feat: set and shopt directives (#929)
Co-authored-by: Andrey Nering <andrey@nering.com.br>
2023-01-14 16:41:56 -03:00
Andrey Nering
4be1050234 Optimize the Taskfile a bit
`go list ./...` takes quite a few seconds to run. Let's restrict it to the
tasks that actually use it.
2023-01-06 21:41:18 -03:00
Andrey Nering
2efb3533ec Add CHANGELOG + improvements to #980
Closes #978
2023-01-06 21:39:57 -03:00
Lea Anthony
aa6c7e4b94 Add support for 'platforms' in both task and command (#980) 2023-01-06 21:38:35 -03:00
Andrey Nering
63c50d13ee Website/README: Add link to the Mastodon account 2023-01-01 21:24:16 -03:00
Andrey Nering
c1e127e42f Website: Update outdated URL 2022-12-31 14:28:37 -03:00
dependabot[bot]
9e38e8a4db build(deps): bump json5 from 2.2.1 to 2.2.2 in /docs (#972)
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-31 14:15:20 -03:00
33 changed files with 875 additions and 113 deletions

View File

@@ -1,5 +1,22 @@
# Changelog # Changelog
## v3.20.0 - 2023-01-14
- Improve behavior and performance of status checking when using the
`timestamp` mode
([#976](https://github.com/go-task/task/issues/976), [#977](https://github.com/go-task/task/pull/977) by @aminya).
- Performance optimizations were made for large Taskfiles
([#982](https://github.com/go-task/task/pull/982) by @pd93).
- Add ability to configure options for the [`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)
and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) builtins
([#908](https://github.com/go-task/task/issues/908), [#929](https://github.com/go-task/task/pull/929) by @pd93, [Documentation](http://taskfile.dev/usage/#set-and-shopt)).
- Add new `platforms:` attribute to `task` and `cmd`, so it's now possible to
choose in which platforms that given task or command will be run on. Possible
values are operating system (GOOS), architecture (GOARCH) or a combination of
the two. Example: `platforms: [linux]`, `platforms: [amd64]` or
`platforms: [linux/amd64]`. Other platforms will be skipped
([#978](https://github.com/go-task/task/issues/978), [#980](https://github.com/go-task/task/pull/980) by @leaanthony).
## v3.19.1 - 2022-12-31 ## v3.19.1 - 2022-12-31
- Small bug fix: closing `Taskfile.yml` once we're done reading it - Small bug fix: closing `Taskfile.yml` once we're done reading it

View File

@@ -10,7 +10,7 @@
</p> </p>
<p> <p>
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a> <a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
</p> </p>
</div> </div>

View File

@@ -6,13 +6,6 @@ includes:
taskfile: ./docs taskfile: ./docs
dir: ./docs dir: ./docs
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
GO_PACKAGES:
sh: go list ./...
env: env:
CGO_ENABLED: '0' CGO_ENABLED: '0'
@@ -29,6 +22,9 @@ tasks:
- './**/*.go' - './**/*.go'
cmds: cmds:
- go install -v -ldflags="-w -s -X main.version={{.GIT_COMMIT}}" ./cmd/task - go install -v -ldflags="-w -s -X main.version={{.GIT_COMMIT}}" ./cmd/task
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
mod: mod:
desc: Downloads and tidy Go modules desc: Downloads and tidy Go modules
@@ -73,12 +69,18 @@ tasks:
deps: [install] deps: [install]
cmds: cmds:
- go test {{catLines .GO_PACKAGES}} - go test {{catLines .GO_PACKAGES}}
vars:
GO_PACKAGES:
sh: go list ./...
test:all: test:all:
desc: Runs test suite with signals and watch tests included desc: Runs test suite with signals and watch tests included
deps: [install, sleepit:build] deps: [install, sleepit:build]
cmds: cmds:
- go test {{catLines .GO_PACKAGES}} -tags 'signals watch' - go test {{catLines .GO_PACKAGES}} -tags 'signals watch'
vars:
GO_PACKAGES:
sh: go list ./...
test-release: test-release:
desc: Tests release process without publishing desc: Tests release process without publishing
@@ -106,4 +108,7 @@ tasks:
packages: packages:
cmds: cmds:
- echo '{{.GO_PACKAGES}}' - echo '{{.GO_PACKAGES}}'
vars:
GO_PACKAGES:
sh: go list ./...
silent: true silent: true

View File

@@ -1,11 +1,13 @@
const GITHUB_URL = 'https://github.com/go-task/task'; const GITHUB_URL = 'https://github.com/go-task/task';
const TWITTER_URL = 'https://twitter.com/taskfiledev'; const TWITTER_URL = 'https://twitter.com/taskfiledev';
const MASTODON_URL = 'https://fosstodon.org/@task';
const DISCORD_URL = 'https://discord.gg/6TY36E39UK'; const DISCORD_URL = 'https://discord.gg/6TY36E39UK';
const CHINESE_URL = 'https://task-zh.readthedocs.io/zh_CN/latest/'; const CHINESE_URL = 'https://task-zh.readthedocs.io/zh_CN/latest/';
module.exports = { module.exports = {
GITHUB_URL, CHINESE_URL,
TWITTER_URL,
DISCORD_URL, DISCORD_URL,
CHINESE_URL GITHUB_URL,
MASTODON_URL,
TWITTER_URL
}; };

View File

@@ -91,6 +91,8 @@ Some environment variables can be overriden to adjust Task behavior.
| `dotenv` | `[]string` | | A list of `.env` file paths to be parsed. | | `dotenv` | `[]string` | | A list of `.env` file paths to be parsed. |
| `run` | `string` | `always` | Default 'run' option for this Taskfile. Available options: `always`, `once` and `when_changed`. | | `run` | `string` | `always` | Default 'run' option for this Taskfile. Available options: `always`, `once` and `when_changed`. |
| `interval` | `string` | `5s` | Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid [Go Duration](https://pkg.go.dev/time#ParseDuration). | | `interval` | `string` | `5s` | Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid [Go Duration](https://pkg.go.dev/time#ParseDuration). |
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
### Include ### Include
@@ -139,6 +141,9 @@ includes:
| `prefix` | `string` | | Defines a string to prefix the output of tasks running in parallel. Only used when the output mode is `prefixed`. | | `prefix` | `string` | | Defines a string to prefix the output of tasks running in parallel. Only used when the output mode is `prefixed`. |
| `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing commands. | | `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing commands. |
| `run` | `string` | The one declared globally in the Taskfile or `always` | Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`. | | `run` | `string` | The one declared globally in the Taskfile or `always` | Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`. |
| `platforms` | `[]string` | All platforms | Specifies which platforms the task should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/go/build/syslist.go). Task will be skipped otherwise. |
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
:::info :::info
@@ -189,6 +194,9 @@ tasks:
| `vars` | [`map[string]Variable`](#variable) | | Optional additional variables to be passed to the referenced task. Only relevant when setting `task` instead of `cmd`. | | `vars` | [`map[string]Variable`](#variable) | | Optional additional variables to be passed to the referenced task. Only relevant when setting `task` instead of `cmd`. |
| `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing the command. | | `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing the command. |
| `defer` | `string` | | Alternative to `cmd`, but schedules the command to be executed at the end of this task instead of immediately. This cannot be used together with `cmd`. | | `defer` | `string` | | Alternative to `cmd`, but schedules the command to be executed at the end of this task instead of immediately. This cannot be used together with `cmd`. |
| `platforms` | `[]string` | All platforms | Specifies which platforms the command should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/go/build/syslist.go). Command will be skipped otherwise. |
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
:::info :::info

View File

@@ -5,6 +5,23 @@ sidebar_position: 7
# Changelog # Changelog
## v3.20.0 - 2023-01-14
- Improve behavior and performance of status checking when using the
`timestamp` mode
([#976](https://github.com/go-task/task/issues/976), [#977](https://github.com/go-task/task/pull/977) by @aminya).
- Performance optimizations were made for large Taskfiles
([#982](https://github.com/go-task/task/pull/982) by @pd93).
- Add ability to configure options for the [`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)
and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) builtins
([#908](https://github.com/go-task/task/issues/908), [#929](https://github.com/go-task/task/pull/929) by @pd93, [Documentation](http://taskfile.dev/usage/#set-and-shopt)).
- Add new `platforms:` attribute to `task` and `cmd`, so it's now possible to
choose in which platforms that given task or command will be run on. Possible
values are operating system (GOOS), architecture (GOARCH) or a combination of
the two. Example: `platforms: [linux]`, `platforms: [amd64]` or
`platforms: [linux/amd64]`. Other platforms will be skipped
([#978](https://github.com/go-task/task/issues/978), [#980](https://github.com/go-task/task/pull/980) by @leaanthony).
## v3.19.1 - 2022-12-31 ## v3.19.1 - 2022-12-31
- Small bug fix: closing `Taskfile.yml` once we're done reading it - Small bug fix: closing `Taskfile.yml` once we're done reading it

View File

@@ -41,7 +41,7 @@ the [Snapcraft dashboard][snapcraftdashboard].
Scoop is a command-line package manager for the Windows operating system. Scoop is a command-line package manager for the Windows operating system.
Scoop package manifests are maintained by the community. Scoop package manifests are maintained by the community.
Scoop owners usually take care of updating versions there by editing [this file](https://github.com/lukesampson/scoop-extras/blob/master/bucket/task.json). Scoop owners usually take care of updating versions there by editing [this file](https://github.com/ScoopInstaller/Main/blob/master/bucket/task.json).
If you think its Task version is outdated, open an issue to let us know. If you think its Task version is outdated, open an issue to let us know.
# Nix # Nix

View File

@@ -439,6 +439,78 @@ tasks:
- echo {{.TEXT}} - echo {{.TEXT}}
``` ```
## Platform specific tasks and commands
If you want to restrict the running of tasks to explicit platforms, this can be achieved
using the `platforms:` key. Tasks can be restricted to a specific OS, architecture or a
combination of both.
On a mismatch, the task or command will be skipped, and no error will be thrown.
The values allowed as OS or Arch are valid `GOOS` and `GOARCH` values, as
defined by the Go language
[here](https://github.com/golang/go/blob/master/src/go/build/syslist.go).
The `build-windows` task below will run only on Windows, and on any architecture:
```yaml
version: '3'
tasks:
build-windows:
platforms: [windows]
cmds:
- echo 'Running command on Windows'
```
This can be restricted to a specific architecture as follows:
```yaml
version: '3'
tasks:
build-windows-amd64:
platforms: [windows/amd64]
cmds:
- echo 'Running command on Windows (amd64)'
```
It is also possible to restrict the task to specific architectures:
```yaml
version: '3'
tasks:
build-amd64:
platforms: [amd64]
cmds:
- echo 'Running command on amd64'
```
Multiple platforms can be specified as follows:
```yaml
version: '3'
tasks:
build:
platforms: [windows/amd64, darwin]
cmds:
- echo 'Running command on Windows (amd64) and macOS'
```
Individual commands can also be restricted to specific platforms:
```yaml
version: '3'
tasks:
build:
cmds:
- cmd: echo 'Running command on Windows (amd64) and macOS'
platforms: [windows/amd64, darwin]
- cmd: echo 'Running on all platforms'
```
## Calling another task ## Calling another task
When a task has many dependencies, they are executed concurrently. This will When a task has many dependencies, they are executed concurrently. This will
@@ -595,9 +667,9 @@ The method `none` skips any validation and always run the task.
:::info :::info
For the `checksum` (default) method to work, it is only necessary to For the `checksum` (default) or `timestamp` method to work, it is only necessary to
inform the source files, but if you want to use the `timestamp` method, you inform the source files.
also need to inform the generated files with `generates`. When the `timestamp` method is used, the last time of the running the task is considered as a generate.
::: :::
@@ -1348,6 +1420,31 @@ tasks:
- ./app{{exeExt}} -h localhost -p 8080 - ./app{{exeExt}} -h localhost -p 8080
``` ```
## `set` and `shopt`
It's possible to specify options to the
[`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)
and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html)
builtins. This can be added at global, task or command level.
```yaml
version: '2'
set: [pipefail]
shopt: [globstar]
tasks:
# `globstar` required for double star globs to work
default: echo **/*.go
```
:::info
Keep in mind that not all options are available in the
[shell interpreter library](https://github.com/mvdan/sh) that Task uses.
:::
## Watch tasks ## Watch tasks
With the flags `--watch` or `-w` task will watch for file changes With the flags `--watch` or `-w` task will watch for file changes

View File

@@ -2,10 +2,11 @@
// Note: type annotations allow type checking and IDEs autocompletion // Note: type annotations allow type checking and IDEs autocompletion
const { const {
GITHUB_URL, CHINESE_URL,
TWITTER_URL,
DISCORD_URL, DISCORD_URL,
CHINESE_URL GITHUB_URL,
MASTODON_URL,
TWITTER_URL
} = require('./constants'); } = require('./constants');
const lightCodeTheme = require('./src/themes/prismLight'); const lightCodeTheme = require('./src/themes/prismLight');
const darkCodeTheme = require('./src/themes/prismDark'); const darkCodeTheme = require('./src/themes/prismDark');
@@ -108,6 +109,11 @@ const config = {
label: 'Twitter', label: 'Twitter',
position: 'right' position: 'right'
}, },
{
href: MASTODON_URL,
label: 'Mastodon',
position: 'right'
},
{ {
href: DISCORD_URL, href: DISCORD_URL,
label: 'Discord', label: 'Discord',
@@ -146,6 +152,10 @@ const config = {
label: 'Twitter', label: 'Twitter',
href: TWITTER_URL href: TWITTER_URL
}, },
{
label: 'Mastodon',
href: MASTODON_URL
},
{ {
label: 'Discord', label: 'Discord',
href: DISCORD_URL href: DISCORD_URL

View File

@@ -108,6 +108,20 @@
"description": "The directory in which this task should run. Defaults to the current working directory.", "description": "The directory in which this task should run. Defaults to the current working directory.",
"type": "string" "type": "string"
}, },
"set": {
"description": "Enables POSIX shell options for all of a task's commands. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/set"
}
},
"shopt": {
"description": "Enables Bash shell options for all of a task's commands. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/shopt"
}
},
"vars": { "vars": {
"description": "A set of variables that can be used in the task.", "description": "A set of variables that can be used in the task.",
"$ref": "#/definitions/3/vars" "$ref": "#/definitions/3/vars"
@@ -155,6 +169,13 @@
"run": { "run": {
"description": "Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`.", "description": "Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`.",
"$ref": "#/definitions/3/run" "$ref": "#/definitions/3/run"
},
"platforms": {
"description": "Specifies which platforms the task should be run on.",
"type": "array",
"items": {
"type": "string"
}
} }
} }
}, },
@@ -177,6 +198,14 @@
} }
] ]
}, },
"set": {
"type": "string",
"enum": ["allexport", "a", "errexit", "e", "noexec", "n", "noglob", "f", "nounset", "u", "xtrace", "x", "pipefail"]
},
"shopt": {
"type": "string",
"enum": ["expand_aliases", "globstar", "nullglob"]
},
"vars": { "vars": {
"type": "object", "type": "object",
"patternProperties": { "patternProperties": {
@@ -226,6 +255,20 @@
"description": "Silent mode disables echoing of command before Task runs it", "description": "Silent mode disables echoing of command before Task runs it",
"type": "boolean" "type": "boolean"
}, },
"set": {
"description": "Enables POSIX shell options for this command. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/set"
}
},
"shopt": {
"description": "Enables Bash shell options for this command. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/shopt"
}
},
"ignore_error": { "ignore_error": {
"description": "Prevent command from aborting the execution of task even after receiving a status code of 1", "description": "Prevent command from aborting the execution of task even after receiving a status code of 1",
"type": "boolean" "type": "boolean"
@@ -233,6 +276,13 @@
"defer": { "defer": {
"description": "", "description": "",
"type": "boolean" "type": "boolean"
},
"platforms": {
"description": "Specifies which platforms the command should be run on.",
"type": "array",
"items": {
"type": "string"
}
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -357,6 +407,20 @@
"description": "Default 'silent' options for this Taskfile. If `false`, can be overidden with `true` in a task by task basis.", "description": "Default 'silent' options for this Taskfile. If `false`, can be overidden with `true` in a task by task basis.",
"type": "boolean" "type": "boolean"
}, },
"set": {
"description": "Enables POSIX shell options for all commands in the Taskfile. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/set"
}
},
"shopt": {
"description": "Enables Bash shell options for all commands in the Taskfile. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/3/shopt"
}
},
"dotenv": { "dotenv": {
"type": "array", "type": "array",
"description": "A list of `.env` file paths to be parsed.", "description": "A list of `.env` file paths to be parsed.",

View File

@@ -5877,9 +5877,9 @@ json-schema-traverse@^1.0.0:
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
json5@^2.1.2, json5@^2.2.1: json5@^2.1.2, json5@^2.2.1:
version "2.2.1" version "2.2.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.2.tgz#64471c5bdcc564c18f7c1d4df2e2297f2457c5ab"
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== integrity sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==
jsonfile@^6.0.1: jsonfile@^6.0.1:
version "6.1.0" version "6.1.0"

View File

@@ -51,10 +51,10 @@ func (o ListOptions) Validate() error {
// Filters returns the slice of FilterFunc which filters a list // Filters returns the slice of FilterFunc which filters a list
// of taskfile.Task according to the given ListOptions // of taskfile.Task according to the given ListOptions
func (o ListOptions) Filters() []FilterFunc { func (o ListOptions) Filters() []FilterFunc {
filters := []FilterFunc{FilterOutInternal()} filters := []FilterFunc{FilterOutInternal}
if o.ListOnlyTasksWithDescriptions { if o.ListOnlyTasksWithDescriptions {
filters = append(filters, FilterOutNoDesc()) filters = append(filters, FilterOutNoDesc)
} }
return filters return filters
@@ -65,7 +65,10 @@ func (o ListOptions) Filters() []FilterFunc {
// The function returns a boolean indicating whether tasks were found // The function returns a boolean indicating whether tasks were found
// and an error if one was encountered while preparing the output. // and an error if one was encountered while preparing the output.
func (e *Executor) ListTasks(o ListOptions) (bool, error) { func (e *Executor) ListTasks(o ListOptions) (bool, error) {
tasks := e.GetTaskList(o.Filters()...) tasks, err := e.GetTaskList(o.Filters()...)
if err != nil {
return false, err
}
if o.FormatTaskListAsJSON { if o.FormatTaskListAsJSON {
output, err := e.ToEditorOutput(tasks) output, err := e.ToEditorOutput(tasks)
if err != nil { if err != nil {

View File

@@ -3,6 +3,7 @@ package execext
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@@ -17,12 +18,14 @@ import (
// RunCommandOptions is the options for the RunCommand func // RunCommandOptions is the options for the RunCommand func
type RunCommandOptions struct { type RunCommandOptions struct {
Command string Command string
Dir string Dir string
Env []string Env []string
Stdin io.Reader PosixOpts []string
Stdout io.Writer BashOpts []string
Stderr io.Writer Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
} }
var ( var (
@@ -36,9 +39,18 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
return ErrNilOptions return ErrNilOptions
} }
p, err := syntax.NewParser().Parse(strings.NewReader(opts.Command), "") // Set "-e" or "errexit" by default
if err != nil { opts.PosixOpts = append(opts.PosixOpts, "e")
return err
// Format POSIX options into a slice that mvdan/sh understands
var params []string
for _, opt := range opts.PosixOpts {
if len(opt) == 1 {
params = append(params, fmt.Sprintf("-%s", opt))
} else {
params = append(params, "-o")
params = append(params, opt)
}
} }
environ := opts.Env environ := opts.Env
@@ -47,7 +59,7 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
} }
r, err := interp.New( r, err := interp.New(
interp.Params("-e"), interp.Params(params...),
interp.Env(expand.ListEnviron(environ...)), interp.Env(expand.ListEnviron(environ...)),
interp.ExecHandler(interp.DefaultExecHandler(15*time.Second)), interp.ExecHandler(interp.DefaultExecHandler(15*time.Second)),
interp.OpenHandler(openHandler), interp.OpenHandler(openHandler),
@@ -58,6 +70,25 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
return err return err
} }
parser := syntax.NewParser()
// Run any shopt commands
if len(opts.BashOpts) > 0 {
shoptCmdStr := fmt.Sprintf("shopt -s %s", strings.Join(opts.BashOpts, " "))
shoptCmd, err := parser.Parse(strings.NewReader(shoptCmdStr), "")
if err != nil {
return err
}
if err := r.Run(ctx, shoptCmd); err != nil {
return err
}
}
// Run the user-defined command
p, err := parser.Parse(strings.NewReader(opts.Command), "")
if err != nil {
return err
}
return r.Run(ctx, p) return r.Run(ctx, p)
} }

62
internal/goext/meta.go Normal file
View File

@@ -0,0 +1,62 @@
package goext
// NOTE(@andreynering): The lists in this file were copied from:
//
// https://github.com/golang/go/blob/master/src/go/build/syslist.go
func IsKnownOS(str string) bool {
_, known := knownOS[str]
return known
}
func IsKnownArch(str string) bool {
_, known := knownArch[str]
return known
}
var knownOS = map[string]struct{}{
"aix": {},
"android": {},
"darwin": {},
"dragonfly": {},
"freebsd": {},
"hurd": {},
"illumos": {},
"ios": {},
"js": {},
"linux": {},
"nacl": {},
"netbsd": {},
"openbsd": {},
"plan9": {},
"solaris": {},
"windows": {},
"zos": {},
}
var knownArch = map[string]struct{}{
"386": {},
"amd64": {},
"amd64p32": {},
"arm": {},
"armbe": {},
"arm64": {},
"arm64be": {},
"loong64": {},
"mips": {},
"mipsle": {},
"mips64": {},
"mips64le": {},
"mips64p32": {},
"mips64p32le": {},
"ppc": {},
"ppc64": {},
"ppc64le": {},
"riscv": {},
"riscv64": {},
"s390": {},
"s390x": {},
"sparc": {},
"sparc64": {},
"wasm": {},
}

View File

@@ -0,0 +1,20 @@
package slicesext
import (
"golang.org/x/exp/constraints"
"golang.org/x/exp/slices"
)
func UniqueJoin[T constraints.Ordered](ss ...[]T) []T {
var length int
for _, s := range ss {
length += len(s)
}
r := make([]T, length)
var i int
for _, s := range ss {
i += copy(r[i:], s)
}
slices.Sort(r)
return slices.Compact(r)
}

View File

@@ -85,6 +85,7 @@ func (c *Checksum) checksum(files ...string) (string, error) {
if _, err = io.Copy(h, f); err != nil { if _, err = io.Copy(h, f); err != nil {
return "", err return "", err
} }
f.Close()
} }
return fmt.Sprintf("%x", h.Sum(nil)), nil return fmt.Sprintf("%x", h.Sum(nil)), nil
@@ -109,12 +110,12 @@ func (*Checksum) Kind() string {
} }
func (c *Checksum) checksumFilePath() string { func (c *Checksum) checksumFilePath() string {
return filepath.Join(c.TempDir, "checksum", c.normalizeFilename(c.Task)) return filepath.Join(c.TempDir, "checksum", normalizeFilename(c.Task))
} }
var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]") var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]")
// replaces invalid caracters on filenames with "-" // replaces invalid caracters on filenames with "-"
func (*Checksum) normalizeFilename(f string) string { func normalizeFilename(f string) string {
return checksumFilenameRegexp.ReplaceAllString(f, "-") return checksumFilenameRegexp.ReplaceAllString(f, "-")
} }

View File

@@ -16,6 +16,6 @@ func TestNormalizeFilename(t *testing.T) {
{"foo1bar2baz3", "foo1bar2baz3"}, {"foo1bar2baz3", "foo1bar2baz3"},
} }
for _, test := range tests { for _, test := range tests {
assert.Equal(t, test.Out, (&Checksum{}).normalizeFilename(test.In)) assert.Equal(t, test.Out, normalizeFilename(test.In))
} }
} }

View File

@@ -2,20 +2,24 @@ package status
import ( import (
"os" "os"
"path/filepath"
"time" "time"
) )
// Timestamp checks if any source change compared with the generated files, // Timestamp checks if any source change compared with the generated files,
// using file modifications timestamps. // using file modifications timestamps.
type Timestamp struct { type Timestamp struct {
TempDir string
Task string
Dir string Dir string
Sources []string Sources []string
Generates []string Generates []string
Dry bool
} }
// IsUpToDate implements the Checker interface // IsUpToDate implements the Checker interface
func (t *Timestamp) IsUpToDate() (bool, error) { func (t *Timestamp) IsUpToDate() (bool, error) {
if len(t.Sources) == 0 || len(t.Generates) == 0 { if len(t.Sources) == 0 {
return false, nil return false, nil
} }
@@ -28,17 +32,51 @@ func (t *Timestamp) IsUpToDate() (bool, error) {
return false, nil return false, nil
} }
sourcesMaxTime, err := getMaxTime(sources...) timestampFile := t.timestampFilePath()
if err != nil || sourcesMaxTime.IsZero() {
// If the file exists, add the file path to the generates.
// If the generate file is old, the task will be executed.
_, err = os.Stat(timestampFile)
if err == nil {
generates = append(generates, timestampFile)
} else {
// Create the timestamp file for the next execution when the file does not exist.
if !t.Dry {
if err := os.MkdirAll(filepath.Dir(timestampFile), 0o755); err != nil {
return false, err
}
f, err := os.Create(timestampFile)
if err != nil {
return false, err
}
f.Close()
}
}
taskTime := time.Now()
// Compare the time of the generates and sources. If the generates are old, the task will be executed.
// Get the max time of the generates.
generateMaxTime, err := getMaxTime(generates...)
if err != nil || generateMaxTime.IsZero() {
return false, nil return false, nil
} }
generatesMinTime, err := getMinTime(generates...) // Check if any of the source files is newer than the max time of the generates.
if err != nil || generatesMinTime.IsZero() { shouldUpdate, err := anyFileNewerThan(sources, generateMaxTime)
if err != nil {
return false, nil return false, nil
} }
return !generatesMinTime.Before(sourcesMaxTime), nil // Modify the metadata of the file to the the current time.
if !t.Dry {
if err := os.Chtimes(timestampFile, taskTime, taskTime); err != nil {
return false, err
}
}
return !shouldUpdate, nil
} }
func (t *Timestamp) Kind() string { func (t *Timestamp) Kind() string {
@@ -64,18 +102,6 @@ func (t *Timestamp) Value() (interface{}, error) {
return sourcesMaxTime, nil return sourcesMaxTime, nil
} }
func getMinTime(files ...string) (time.Time, error) {
var t time.Time
for _, f := range files {
info, err := os.Stat(f)
if err != nil {
return time.Time{}, err
}
t = minTime(t, info.ModTime())
}
return t, nil
}
func getMaxTime(files ...string) (time.Time, error) { func getMaxTime(files ...string) (time.Time, error) {
var t time.Time var t time.Time
for _, f := range files { for _, f := range files {
@@ -88,13 +114,6 @@ func getMaxTime(files ...string) (time.Time, error) {
return t, nil return t, nil
} }
func minTime(a, b time.Time) time.Time {
if !a.IsZero() && a.Before(b) {
return a
}
return b
}
func maxTime(a, b time.Time) time.Time { func maxTime(a, b time.Time) time.Time {
if a.After(b) { if a.After(b) {
return a return a
@@ -102,7 +121,26 @@ func maxTime(a, b time.Time) time.Time {
return b return b
} }
// If the modification time of any of the files is newer than the the given time, returns true.
// This function is lazy, as it stops when it finds a file newer than the given time.
func anyFileNewerThan(files []string, givenTime time.Time) (bool, error) {
for _, f := range files {
info, err := os.Stat(f)
if err != nil {
return false, err
}
if info.ModTime().After(givenTime) {
return true, nil
}
}
return false, nil
}
// OnError implements the Checker interface // OnError implements the Checker interface
func (*Timestamp) OnError() error { func (*Timestamp) OnError() error {
return nil return nil
} }
func (t *Timestamp) timestampFilePath() string {
return filepath.Join(t.TempDir, "timestamp", normalizeFilename(t.Task))
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@go-task/cli", "name": "@go-task/cli",
"version": "3.19.1", "version": "3.20.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@go-task/cli", "name": "@go-task/cli",
"version": "3.19.1", "version": "3.20.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@go-task/cli", "name": "@go-task/cli",
"version": "3.19.1", "version": "3.20.0",
"description": "A task runner / simpler Make alternative written in Go", "description": "A task runner / simpler Make alternative written in Go",
"scripts": { "scripts": {
"postinstall": "go-npm install", "postinstall": "go-npm install",

View File

@@ -87,9 +87,12 @@ func (e *Executor) getStatusChecker(t *taskfile.Task) (status.Checker, error) {
func (e *Executor) timestampChecker(t *taskfile.Task) status.Checker { func (e *Executor) timestampChecker(t *taskfile.Task) status.Checker {
return &status.Timestamp{ return &status.Timestamp{
TempDir: e.TempDir,
Task: t.Name(),
Dir: t.Dir, Dir: t.Dir,
Sources: t.Sources, Sources: t.Sources,
Generates: t.Generates, Generates: t.Generates,
Dry: e.Dry,
} }
} }

111
task.go
View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"runtime"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -15,6 +16,7 @@ import (
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/slicesext"
"github.com/go-task/task/v3/internal/summary" "github.com/go-task/task/v3/internal/summary"
"github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile"
@@ -135,6 +137,11 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
defer release() defer release()
return e.startExecution(ctx, t, func(ctx context.Context) error { return e.startExecution(ctx, t, func(ctx context.Context) error {
if !shouldRunOnCurrentPlatform(t.Platforms) {
e.Logger.VerboseOutf(logger.Yellow, `task: "%s" not for current platform - ignored`, call.Task)
return nil
}
e.Logger.VerboseErrf(logger.Magenta, `task: "%s" started`, call.Task) e.Logger.VerboseErrf(logger.Magenta, `task: "%s" started`, call.Task)
if err := e.runDeps(ctx, t); err != nil { if err := e.runDeps(ctx, t); err != nil {
return err return err
@@ -252,6 +259,11 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
} }
return nil return nil
case cmd.Cmd != "": case cmd.Cmd != "":
if !shouldRunOnCurrentPlatform(cmd.Platforms) {
e.Logger.VerboseOutf(logger.Yellow, `task: [%s] %s not for current platform - ignored`, t.Name(), cmd.Cmd)
return nil
}
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) { if e.Verbose || (!cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
e.Logger.Errf(logger.Green, "task: [%s] %s", t.Name(), cmd.Cmd) e.Logger.Errf(logger.Green, "task: [%s] %s", t.Name(), cmd.Cmd)
} }
@@ -272,17 +284,19 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
stdOut, stdErr, close := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater) stdOut, stdErr, close := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)
defer func() { defer func() {
if err := close(); err != nil { if err := close(); err != nil {
e.Logger.Errf(logger.Red, "task: unable to close writter: %v", err) e.Logger.Errf(logger.Red, "task: unable to close writer: %v", err)
} }
}() }()
err = execext.RunCommand(ctx, &execext.RunCommandOptions{ err = execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: cmd.Cmd, Command: cmd.Cmd,
Dir: t.Dir, Dir: t.Dir,
Env: getEnviron(t), Env: getEnviron(t),
Stdin: e.Stdin, PosixOpts: slicesext.UniqueJoin(e.Taskfile.Set, t.Set, cmd.Set),
Stdout: stdOut, BashOpts: slicesext.UniqueJoin(e.Taskfile.Shopt, t.Shopt, cmd.Shopt),
Stderr: stdErr, Stdin: e.Stdin,
Stdout: stdOut,
Stderr: stdErr,
}) })
if execext.IsExitError(err) && cmd.IgnoreError { if execext.IsExitError(err) && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v", t.Name(), err) e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v", t.Name(), err)
@@ -386,23 +400,39 @@ func (e *Executor) GetTask(call taskfile.Call) (*taskfile.Task, error) {
return matchingTask, nil return matchingTask, nil
} }
type FilterFunc func(tasks []*taskfile.Task) []*taskfile.Task type FilterFunc func(task *taskfile.Task) bool
func (e *Executor) GetTaskList(filters ...FilterFunc) []*taskfile.Task { func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*taskfile.Task, error) {
tasks := make([]*taskfile.Task, 0, len(e.Taskfile.Tasks)) tasks := make([]*taskfile.Task, 0, len(e.Taskfile.Tasks))
// Create an error group to wait for each task to be compiled
var g errgroup.Group
// Fetch and compile the list of tasks // Fetch and compile the list of tasks
for _, task := range e.Taskfile.Tasks { for key := range e.Taskfile.Tasks {
compiledTask, err := e.FastCompiledTask(taskfile.Call{Task: task.Task}) task := e.Taskfile.Tasks[key]
if err == nil { g.Go(func() error {
task = compiledTask
} // Check if we should filter the task
tasks = append(tasks, task) for _, filter := range filters {
if filter(task) {
return nil
}
}
// Compile the task
compiledTask, err := e.FastCompiledTask(taskfile.Call{Task: task.Task})
if err == nil {
task = compiledTask
}
tasks = append(tasks, task)
return nil
})
} }
// Filter the tasks // Wait for all the go routines to finish
for _, filter := range filters { if err := g.Wait(); err != nil {
tasks = filter(tasks) return nil, err
} }
// Sort the tasks. // Sort the tasks.
@@ -420,38 +450,27 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) []*taskfile.Task {
return false return false
}) })
return tasks return tasks, nil
}
// Filter is a generic task filtering function. It will remove each task in the
// slice where the result of the given function is true.
func Filter(f func(task *taskfile.Task) bool) FilterFunc {
return func(tasks []*taskfile.Task) []*taskfile.Task {
shift := 0
for _, task := range tasks {
if !f(task) {
tasks[shift] = task
shift++
}
}
// This loop stops any memory leaks
for j := shift; j < len(tasks); j++ {
tasks[j] = nil
}
return slices.Clip(tasks[:shift])
}
} }
// FilterOutNoDesc removes all tasks that do not contain a description. // FilterOutNoDesc removes all tasks that do not contain a description.
func FilterOutNoDesc() FilterFunc { func FilterOutNoDesc(task *taskfile.Task) bool {
return Filter(func(task *taskfile.Task) bool { return task.Desc == ""
return task.Desc == ""
})
} }
// FilterOutInternal removes all tasks that are marked as internal. // FilterOutInternal removes all tasks that are marked as internal.
func FilterOutInternal() FilterFunc { func FilterOutInternal(task *taskfile.Task) bool {
return Filter(func(task *taskfile.Task) bool { return task.Internal
return task.Internal }
})
func shouldRunOnCurrentPlatform(platforms []*taskfile.Platform) bool {
if len(platforms) == 0 {
return true
}
for _, p := range platforms {
if (p.OS == "" || p.OS == runtime.GOOS) && (p.Arch == "" || p.Arch == runtime.GOARCH) {
return true
}
}
return false
} }

View File

@@ -1696,3 +1696,99 @@ func TestUserWorkingDirectory(t *testing.T) {
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"})) assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String()) assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
} }
func TestPlatforms(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/platforms",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build-" + runtime.GOOS}))
assert.Equal(t, fmt.Sprintf("task: [build-%s] echo 'Running task on %s'\nRunning task on %s\n", runtime.GOOS, runtime.GOOS, runtime.GOOS), buff.String())
}
func TestPOSIXShellOptsGlobalLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/global_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "pipefail"})
assert.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String())
}
func TestPOSIXShellOptsTaskLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/task_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "pipefail"})
assert.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String())
}
func TestPOSIXShellOptsCommandLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/command_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "pipefail"})
assert.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String())
}
func TestBashShellOptsGlobalLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/global_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "globstar"})
assert.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String())
}
func TestBashShellOptsTaskLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/task_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "globstar"})
assert.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String())
}
func TestBashShellOptsCommandLevel(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/shopts/command_level",
Stdout: &buff,
Stderr: &buff,
}
assert.NoError(t, e.Setup())
err := e.Run(context.Background(), taskfile.Call{Task: "globstar"})
assert.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String())
}

View File

@@ -11,9 +11,12 @@ type Cmd struct {
Cmd string Cmd string
Silent bool Silent bool
Task string Task string
Set []string
Shopt []string
Vars *Vars Vars *Vars
IgnoreError bool IgnoreError bool
Defer bool Defer bool
Platforms []*Platform
} }
// Dep is a task dependency // Dep is a task dependency
@@ -39,12 +42,18 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
var cmdStruct struct { var cmdStruct struct {
Cmd string Cmd string
Silent bool Silent bool
Set []string
Shopt []string
IgnoreError bool `yaml:"ignore_error"` IgnoreError bool `yaml:"ignore_error"`
Platforms []*Platform
} }
if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" { if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd c.Cmd = cmdStruct.Cmd
c.Silent = cmdStruct.Silent c.Silent = cmdStruct.Silent
c.Set = cmdStruct.Set
c.Shopt = cmdStruct.Shopt
c.IgnoreError = cmdStruct.IgnoreError c.IgnoreError = cmdStruct.IgnoreError
c.Platforms = cmdStruct.Platforms
return nil return nil
} }

90
taskfile/platforms.go Normal file
View File

@@ -0,0 +1,90 @@
package taskfile
import (
"fmt"
"strings"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/goext"
)
// Platform represents GOOS and GOARCH values
type Platform struct {
OS string
Arch string
}
type ErrInvalidPlatform struct {
Platform string
}
func (err *ErrInvalidPlatform) Error() string {
return fmt.Sprintf(`task: Invalid platform "%s"`, err.Platform)
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (p *Platform) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var platform string
if err := node.Decode(&platform); err != nil {
return err
}
if err := p.parsePlatform(platform); err != nil {
return err
}
return nil
}
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into platform", node.Line, node.ShortTag())
}
// parsePlatform takes a string representing an OS/Arch combination (or either on their own)
// and parses it into the Platform struct. It returns an error if the input string is invalid.
// Valid combinations for input: OS, Arch, OS/Arch
func (p *Platform) parsePlatform(input string) error {
splitValues := strings.Split(input, "/")
if len(splitValues) > 2 {
return &ErrInvalidPlatform{Platform: input}
}
if err := p.parseOsOrArch(splitValues[0]); err != nil {
return &ErrInvalidPlatform{Platform: input}
}
if len(splitValues) == 2 {
if err := p.parseArch(splitValues[1]); err != nil {
return &ErrInvalidPlatform{Platform: input}
}
}
return nil
}
// parseOsOrArch will check if the given input is a valid OS or Arch value.
// If so, it will store it. If not, an error is returned
func (p *Platform) parseOsOrArch(osOrArch string) error {
if osOrArch == "" {
return fmt.Errorf("task: Blank OS/Arch value provided")
}
if goext.IsKnownOS(osOrArch) {
p.OS = osOrArch
return nil
}
if goext.IsKnownArch(osOrArch) {
p.Arch = osOrArch
return nil
}
return fmt.Errorf("task: Invalid OS/Arch value provided (%s)", osOrArch)
}
func (p *Platform) parseArch(arch string) error {
if arch == "" {
return fmt.Errorf("task: Blank Arch value provided")
}
if p.Arch != "" {
return fmt.Errorf("task: Multiple Arch values provided")
}
if goext.IsKnownArch(arch) {
p.Arch = arch
return nil
}
return fmt.Errorf("task: Invalid Arch value provided (%s)", arch)
}

View File

@@ -0,0 +1,49 @@
package taskfile
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPlatformParsing(t *testing.T) {
tests := []struct {
Input string
ExpectedOS string
ExpectedArch string
Error string
}{
{Input: "windows", ExpectedOS: "windows", ExpectedArch: ""},
{Input: "linux", ExpectedOS: "linux", ExpectedArch: ""},
{Input: "darwin", ExpectedOS: "darwin", ExpectedArch: ""},
{Input: "386", ExpectedOS: "", ExpectedArch: "386"},
{Input: "amd64", ExpectedOS: "", ExpectedArch: "amd64"},
{Input: "arm64", ExpectedOS: "", ExpectedArch: "arm64"},
{Input: "windows/386", ExpectedOS: "windows", ExpectedArch: "386"},
{Input: "windows/amd64", ExpectedOS: "windows", ExpectedArch: "amd64"},
{Input: "windows/arm64", ExpectedOS: "windows", ExpectedArch: "arm64"},
{Input: "invalid", Error: `task: Invalid platform "invalid"`},
{Input: "invalid/invalid", Error: `task: Invalid platform "invalid/invalid"`},
{Input: "windows/invalid", Error: `task: Invalid platform "windows/invalid"`},
{Input: "invalid/amd64", Error: `task: Invalid platform "invalid/amd64"`},
}
for _, test := range tests {
t.Run(test.Input, func(t *testing.T) {
var p Platform
err := p.parsePlatform(test.Input)
if test.Error != "" {
assert.Error(t, err)
assert.Equal(t, test.Error, err.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, test.ExpectedOS, p.OS)
assert.Equal(t, test.ExpectedArch, p.Arch)
}
})
}
}

View File

@@ -23,6 +23,8 @@ type Task struct {
Status []string Status []string
Preconditions []*Precondition Preconditions []*Precondition
Dir string Dir string
Set []string
Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Dotenv []string Dotenv []string
@@ -36,6 +38,7 @@ type Task struct {
IncludeVars *Vars IncludeVars *Vars
IncludedTaskfileVars *Vars IncludedTaskfileVars *Vars
IncludedTaskfile *IncludedTaskfile IncludedTaskfile *IncludedTaskfile
Platforms []*Platform
} }
func (t *Task) Name() string { func (t *Task) Name() string {
@@ -80,6 +83,8 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Status []string Status []string
Preconditions []*Precondition Preconditions []*Precondition
Dir string Dir string
Set []string
Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Dotenv []string Dotenv []string
@@ -90,6 +95,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Prefix string Prefix string
IgnoreError bool `yaml:"ignore_error"` IgnoreError bool `yaml:"ignore_error"`
Run string Run string
Platforms []*Platform
} }
if err := node.Decode(&task); err != nil { if err := node.Decode(&task); err != nil {
return err return err
@@ -105,6 +111,8 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Status = task.Status t.Status = task.Status
t.Preconditions = task.Preconditions t.Preconditions = task.Preconditions
t.Dir = task.Dir t.Dir = task.Dir
t.Set = task.Set
t.Shopt = task.Shopt
t.Vars = task.Vars t.Vars = task.Vars
t.Env = task.Env t.Env = task.Env
t.Dotenv = task.Dotenv t.Dotenv = task.Dotenv
@@ -115,6 +123,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Prefix = task.Prefix t.Prefix = task.Prefix
t.IgnoreError = task.IgnoreError t.IgnoreError = task.IgnoreError
t.Run = task.Run t.Run = task.Run
t.Platforms = task.Platforms
return nil return nil
} }
@@ -137,6 +146,8 @@ func (t *Task) DeepCopy() *Task {
Status: deepCopySlice(t.Status), Status: deepCopySlice(t.Status),
Preconditions: deepCopySlice(t.Preconditions), Preconditions: deepCopySlice(t.Preconditions),
Dir: t.Dir, Dir: t.Dir,
Set: deepCopySlice(t.Set),
Shopt: deepCopySlice(t.Shopt),
Vars: t.Vars.DeepCopy(), Vars: t.Vars.DeepCopy(),
Env: t.Env.DeepCopy(), Env: t.Env.DeepCopy(),
Dotenv: deepCopySlice(t.Dotenv), Dotenv: deepCopySlice(t.Dotenv),
@@ -150,6 +161,7 @@ func (t *Task) DeepCopy() *Task {
IncludeVars: t.IncludeVars.DeepCopy(), IncludeVars: t.IncludeVars.DeepCopy(),
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(), IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
IncludedTaskfile: t.IncludedTaskfile.DeepCopy(), IncludedTaskfile: t.IncludedTaskfile.DeepCopy(),
Platforms: deepCopySlice(t.Platforms),
} }
return c return c
} }

View File

@@ -15,6 +15,8 @@ type Taskfile struct {
Output Output Output Output
Method string Method string
Includes *IncludedTaskfiles Includes *IncludedTaskfiles
Set []string
Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Tasks Tasks Tasks Tasks
@@ -34,6 +36,8 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
Output Output Output Output
Method string Method string
Includes *IncludedTaskfiles Includes *IncludedTaskfiles
Set []string
Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Tasks Tasks Tasks Tasks
@@ -50,6 +54,8 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
tf.Output = taskfile.Output tf.Output = taskfile.Output
tf.Method = taskfile.Method tf.Method = taskfile.Method
tf.Includes = taskfile.Includes tf.Includes = taskfile.Includes
tf.Set = taskfile.Set
tf.Shopt = taskfile.Shopt
tf.Vars = taskfile.Vars tf.Vars = taskfile.Vars
tf.Env = taskfile.Env tf.Env = taskfile.Env
tf.Tasks = taskfile.Tasks tf.Tasks = taskfile.Tasks

55
testdata/platforms/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
version: '3'
tasks:
build-windows:
platforms: [windows]
cmds:
- echo 'Running task on windows'
build-darwin:
platforms: [darwin]
cmds:
- echo 'Running task on darwin'
build-linux:
platforms: [linux]
cmds:
- echo 'Running task on linux'
build-freebsd:
platforms: [freebsd]
cmds:
- echo 'Running task on freebsd'
build-blank-os:
platforms: []
cmds:
- echo 'Running command'
build-multiple:
platforms: []
cmds:
- cmd: echo 'Running command'
- cmd: echo 'Running on Windows'
platforms: [windows]
- cmd: echo 'Running on Darwin'
platforms: [darwin]
build-amd64:
platforms: [amd64]
cmds:
- echo "Running command on amd64"
build-arm64:
platforms: [arm64]
cmds:
- echo "Running command on arm64"
build-mixed:
cmds:
- cmd: echo 'building on windows/arm64'
platforms: [windows/arm64]
- cmd: echo 'building on linux/amd64'
platforms: [linux/amd64]
- cmd: echo 'building on darwin'
platforms: [darwin]

View File

@@ -0,0 +1,14 @@
version: '3'
silent: true
tasks:
pipefail:
cmds:
- cmd: set -o | grep pipefail
set: [pipefail]
globstar:
cmds:
- cmd: shopt | grep globstar
shopt: [globstar]

View File

@@ -0,0 +1,14 @@
version: '3'
silent: true
set: [pipefail]
shopt: [globstar]
tasks:
pipefail:
cmds:
- set -o | grep pipefail
globstar:
cmds:
- shopt | grep globstar

14
testdata/shopts/task_level/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
version: '3'
silent: true
tasks:
pipefail:
set: [pipefail]
cmds:
- set -o | grep pipefail
globstar:
shopt: [globstar]
cmds:
- shopt | grep globstar

View File

@@ -56,6 +56,8 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
Sources: r.ReplaceSlice(origTask.Sources), Sources: r.ReplaceSlice(origTask.Sources),
Generates: r.ReplaceSlice(origTask.Generates), Generates: r.ReplaceSlice(origTask.Generates),
Dir: r.Replace(origTask.Dir), Dir: r.Replace(origTask.Dir),
Set: origTask.Set,
Shopt: origTask.Shopt,
Vars: nil, Vars: nil,
Env: nil, Env: nil,
Dotenv: r.ReplaceSlice(origTask.Dotenv), Dotenv: r.ReplaceSlice(origTask.Dotenv),
@@ -68,6 +70,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
Run: r.Replace(origTask.Run), Run: r.Replace(origTask.Run),
IncludeVars: origTask.IncludeVars, IncludeVars: origTask.IncludeVars,
IncludedTaskfileVars: origTask.IncludedTaskfileVars, IncludedTaskfileVars: origTask.IncludedTaskfileVars,
Platforms: origTask.Platforms,
} }
new.Dir, err = execext.Expand(new.Dir) new.Dir, err = execext.Expand(new.Dir)
if err != nil { if err != nil {
@@ -124,12 +127,15 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
continue continue
} }
new.Cmds = append(new.Cmds, &taskfile.Cmd{ new.Cmds = append(new.Cmds, &taskfile.Cmd{
Task: r.Replace(cmd.Task),
Silent: cmd.Silent,
Cmd: r.Replace(cmd.Cmd), Cmd: r.Replace(cmd.Cmd),
Silent: cmd.Silent,
Task: r.Replace(cmd.Task),
Set: cmd.Set,
Shopt: cmd.Shopt,
Vars: r.ReplaceVars(cmd.Vars), Vars: r.ReplaceVars(cmd.Vars),
IgnoreError: cmd.IgnoreError, IgnoreError: cmd.IgnoreError,
Defer: cmd.Defer, Defer: cmd.Defer,
Platforms: cmd.Platforms,
}) })
} }
} }