diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bc3697d19..8d9fd18b56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Unreleased + +- 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 - Small bug fix: closing `Taskfile.yml` once we're done reading it diff --git a/docs/docs/api_reference.md b/docs/docs/api_reference.md index 6e9b87ce33..b440442354 100644 --- a/docs/docs/api_reference.md +++ b/docs/docs/api_reference.md @@ -139,7 +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. | +| `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. | :::info @@ -190,7 +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. | +| `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. | :::info diff --git a/docs/docs/usage.md b/docs/docs/usage.md index 8044f5fc84..a075371325 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -442,8 +442,13 @@ tasks: ## 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 +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: @@ -454,7 +459,7 @@ tasks: build-windows: platforms: [windows] cmds: - - echo 'Running command on windows' + - echo 'Running command on Windows' ``` This can be restricted to a specific architecture as follows: @@ -466,7 +471,7 @@ tasks: build-windows-amd64: platforms: [windows/amd64] cmds: - - echo 'Running command on windows (amd64)' + - echo 'Running command on Windows (amd64)' ``` It is also possible to restrict the task to specific architectures: @@ -487,10 +492,10 @@ Multiple platforms can be specified as follows: version: '3' tasks: - build-windows: + build: platforms: [windows/amd64, darwin] cmds: - - echo 'Running command on windows (amd64) and darwin' + - echo 'Running command on Windows (amd64) and macOS' ``` Individual commands can also be restricted to specific platforms: @@ -499,9 +504,9 @@ Individual commands can also be restricted to specific platforms: version: '3' tasks: - build-windows: + build: cmds: - - cmd: echo 'Running command on windows (amd64) and darwin' + - cmd: echo 'Running command on Windows (amd64) and macOS' platforms: [windows/amd64, darwin] - cmd: echo 'Running on all platforms' ``` diff --git a/internal/goext/meta.go b/internal/goext/meta.go new file mode 100644 index 0000000000..24c53d0d6b --- /dev/null +++ b/internal/goext/meta.go @@ -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": {}, +} diff --git a/task.go b/task.go index 827c3c2c51..80aa7a7ab6 100644 --- a/task.go +++ b/task.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "runtime" "sort" "strings" "sync" @@ -135,9 +136,7 @@ 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) { + if !shouldRunOnCurrentPlatform(t.Platforms) { e.Logger.VerboseOutf(logger.Yellow, `task: "%s" not for current platform - ignored`, call.Task) return nil } @@ -259,11 +258,11 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi } return nil case cmd.Cmd != "": - // Check platform - if !ShouldRunOnCurrentPlatform(cmd.Platforms) { + 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) } @@ -468,12 +467,12 @@ func FilterOutInternal() FilterFunc { }) } -func ShouldRunOnCurrentPlatform(platforms []*taskfile.Platform) bool { +func shouldRunOnCurrentPlatform(platforms []*taskfile.Platform) bool { if len(platforms) == 0 { return true } - for _, platform := range platforms { - if platform.MatchesCurrentPlatform() { + for _, p := range platforms { + if (p.OS == "" || p.OS == runtime.GOOS) && (p.Arch == "" || p.Arch == runtime.GOARCH) { return true } } diff --git a/taskfile/platforms.go b/taskfile/platforms.go index 4ed2383911..f71dcbb851 100644 --- a/taskfile/platforms.go +++ b/taskfile/platforms.go @@ -2,10 +2,11 @@ package taskfile import ( "fmt" - "runtime" "strings" "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/internal/goext" ) // Platform represents GOOS and GOARCH values @@ -14,67 +15,23 @@ type Platform struct { 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": {}, +type ErrInvalidPlatform struct { + Platform string } -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) +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 { + if err := p.parsePlatform(platform); err != nil { return err } return nil @@ -82,22 +39,42 @@ func (p *Platform) UnmarshalYAML(node *yaml.Node) error { 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 isSupportedOS(osOrArch) { + if goext.IsKnownOS(osOrArch) { p.OS = osOrArch return nil } - if isSupportedArch(osOrArch) { + 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") @@ -105,7 +82,7 @@ func (p *Platform) parseArch(arch string) error { if p.Arch != "" { return fmt.Errorf("task: Multiple Arch values provided") } - if isSupportedArch(arch) { + if goext.IsKnownArch(arch) { p.Arch = arch return nil } diff --git a/taskfile/platforms_test.go b/taskfile/platforms_test.go new file mode 100644 index 0000000000..57b6b7acbf --- /dev/null +++ b/taskfile/platforms_test.go @@ -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) + } + }) + } +}