Skip to content

Commit

Permalink
Add support for 'platforms' in both task and command (#980)
Browse files Browse the repository at this point in the history
  • Loading branch information
leaanthony authored Jan 7, 2023
1 parent 63c50d1 commit aa6c7e4
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ includes:
| `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. |
| `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. |

:::info

Expand Down Expand Up @@ -189,6 +190,7 @@ tasks:
| `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. |
| `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. |

:::info

Expand Down
67 changes: 67 additions & 0 deletions docs/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,73 @@ tasks:
- 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.

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-windows:
platforms: [windows/amd64, darwin]
cmds:
- echo 'Running command on windows (amd64) and darwin'
```

Individual commands can also be restricted to specific platforms:

```yaml
version: '3'
tasks:
build-windows:
cmds:
- cmd: echo 'Running command on windows (amd64) and darwin'
platforms: [windows/amd64, darwin]
- cmd: echo 'Running on all platforms'
```

## Calling another task

When a task has many dependencies, they are executed concurrently. This will
Expand Down
14 changes: 14 additions & 0 deletions docs/static/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@
"run": {
"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"
},
"platforms": {
"description": "Specifies which platforms the task should be run on.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
Expand Down Expand Up @@ -233,6 +240,13 @@
"defer": {
"description": "",
"type": "boolean"
},
"platforms": {
"description": "Specifies which platforms the command should be run on.",
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
Expand Down
24 changes: 24 additions & 0 deletions task.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
defer release()

return e.startExecution(ctx, t, func(ctx context.Context) error {

// Check platform
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)
if err := e.runDeps(ctx, t); err != nil {
return err
Expand Down Expand Up @@ -252,6 +259,11 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
}
return nil
case cmd.Cmd != "":
// Check platform
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) {
e.Logger.Errf(logger.Green, "task: [%s] %s", t.Name(), cmd.Cmd)
}
Expand Down Expand Up @@ -455,3 +467,15 @@ func FilterOutInternal() FilterFunc {
return task.Internal
})
}

func ShouldRunOnCurrentPlatform(platforms []*taskfile.Platform) bool {
if len(platforms) == 0 {
return true
}
for _, platform := range platforms {
if platform.MatchesCurrentPlatform() {
return true
}
}
return false
}
11 changes: 11 additions & 0 deletions task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1696,3 +1696,14 @@ func TestUserWorkingDirectory(t *testing.T) {
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
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())
}
3 changes: 3 additions & 0 deletions taskfile/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Cmd struct {
Vars *Vars
IgnoreError bool
Defer bool
Platforms []*Platform
}

// Dep is a task dependency
Expand All @@ -40,11 +41,13 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
Cmd string
Silent bool
IgnoreError bool `yaml:"ignore_error"`
Platforms []*Platform
}
if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd
c.Silent = cmdStruct.Silent
c.IgnoreError = cmdStruct.IgnoreError
c.Platforms = cmdStruct.Platforms
return nil
}

Expand Down
113 changes: 113 additions & 0 deletions taskfile/platforms.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package taskfile

import (
"fmt"
"runtime"
"strings"

"gopkg.in/yaml.v3"
)

// Platform represents GOOS and GOARCH values
type Platform struct {
OS string
Arch string
}

// 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 {
// tidy up input
platformString := strings.ToLower(strings.TrimSpace(input))
splitValues := strings.Split(platformString, "/")
if len(splitValues) > 2 {
return fmt.Errorf("task: Invalid OS/Arch provided: %s", input)
}
err := p.parseOsOrArch(splitValues[0])
if err != nil {
return err
}
if len(splitValues) == 2 {
return p.parseArch(splitValues[1])
}
return nil
}

// supportedOSes is a list of supported OSes
var supportedOSes = map[string]struct{}{
"windows": {},
"darwin": {},
"linux": {},
"freebsd": {},
}

func isSupportedOS(input string) bool {
_, exists := supportedOSes[input]
return exists
}

// supportedArchs is a list of supported architectures
var supportedArchs = map[string]struct{}{
"amd64": {},
"arm64": {},
"386": {},
}

func isSupportedArch(input string) bool {
_, exists := supportedArchs[input]
return exists
}

// MatchesCurrentPlatform returns true if the platform matches the current platform
func (p *Platform) MatchesCurrentPlatform() bool {
return (p.OS == "" || p.OS == runtime.GOOS) &&
(p.Arch == "" || p.Arch == runtime.GOARCH)
}

// 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())
}

// 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 isSupportedOS(osOrArch) {
p.OS = osOrArch
return nil
}
if isSupportedArch(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 isSupportedArch(arch) {
p.Arch = arch
return nil
}
return fmt.Errorf("task: Invalid Arch value provided (%s)", arch)
}
4 changes: 4 additions & 0 deletions taskfile/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Task struct {
IncludeVars *Vars
IncludedTaskfileVars *Vars
IncludedTaskfile *IncludedTaskfile
Platforms []*Platform
}

func (t *Task) Name() string {
Expand Down Expand Up @@ -90,6 +91,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Prefix string
IgnoreError bool `yaml:"ignore_error"`
Run string
Platforms []*Platform
}
if err := node.Decode(&task); err != nil {
return err
Expand All @@ -115,6 +117,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Prefix = task.Prefix
t.IgnoreError = task.IgnoreError
t.Run = task.Run
t.Platforms = task.Platforms
return nil
}

Expand Down Expand Up @@ -150,6 +153,7 @@ func (t *Task) DeepCopy() *Task {
IncludeVars: t.IncludeVars.DeepCopy(),
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
IncludedTaskfile: t.IncludedTaskfile.DeepCopy(),
Platforms: deepCopySlice(t.Platforms),
}
return c
}
55 changes: 55 additions & 0 deletions testdata/platforms/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -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]
Loading

0 comments on commit aa6c7e4

Please sign in to comment.