diff --git a/docs/docs/api_reference.md b/docs/docs/api_reference.md index c5a5047a..6e9b87ce 100644 --- a/docs/docs/api_reference.md +++ b/docs/docs/api_reference.md @@ -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 @@ -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 diff --git a/docs/docs/usage.md b/docs/docs/usage.md index d444b01d..8044f5fc 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -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 diff --git a/docs/static/schema.json b/docs/static/schema.json index a7f84c6f..5f967795 100644 --- a/docs/static/schema.json +++ b/docs/static/schema.json @@ -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" + } } } }, @@ -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, diff --git a/task.go b/task.go index a4e801fd..827c3c2c 100644 --- a/task.go +++ b/task.go @@ -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 @@ -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) } @@ -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 +} diff --git a/task_test.go b/task_test.go index e620545d..d0f89db6 100644 --- a/task_test.go +++ b/task_test.go @@ -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()) +} diff --git a/taskfile/cmd.go b/taskfile/cmd.go index 8551b4e7..e2820b60 100644 --- a/taskfile/cmd.go +++ b/taskfile/cmd.go @@ -14,6 +14,7 @@ type Cmd struct { Vars *Vars IgnoreError bool Defer bool + Platforms []*Platform } // Dep is a task dependency @@ -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 } diff --git a/taskfile/platforms.go b/taskfile/platforms.go new file mode 100644 index 00000000..4ed23839 --- /dev/null +++ b/taskfile/platforms.go @@ -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) +} diff --git a/taskfile/task.go b/taskfile/task.go index 4efc00ed..b6367007 100644 --- a/taskfile/task.go +++ b/taskfile/task.go @@ -36,6 +36,7 @@ type Task struct { IncludeVars *Vars IncludedTaskfileVars *Vars IncludedTaskfile *IncludedTaskfile + Platforms []*Platform } func (t *Task) Name() string { @@ -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 @@ -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 } @@ -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 } diff --git a/testdata/platforms/Taskfile.yml b/testdata/platforms/Taskfile.yml new file mode 100644 index 00000000..c25e244c --- /dev/null +++ b/testdata/platforms/Taskfile.yml @@ -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] diff --git a/variables.go b/variables.go index ea41d1ac..cff6e152 100644 --- a/variables.go +++ b/variables.go @@ -68,6 +68,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf Run: r.Replace(origTask.Run), IncludeVars: origTask.IncludeVars, IncludedTaskfileVars: origTask.IncludedTaskfileVars, + Platforms: origTask.Platforms, } new.Dir, err = execext.Expand(new.Dir) if err != nil { @@ -130,6 +131,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf Vars: r.ReplaceVars(cmd.Vars), IgnoreError: cmd.IgnoreError, Defer: cmd.Defer, + Platforms: cmd.Platforms, }) } }