diff --git a/v3/internal/commands/task_wrapper.go b/v3/internal/commands/task_wrapper.go index fbbc715494b..38cbcddb621 100644 --- a/v3/internal/commands/task_wrapper.go +++ b/v3/internal/commands/task_wrapper.go @@ -21,6 +21,18 @@ var validPlatforms = map[string]bool{ "linux": true, } +// rootDispatchTasks are the verbs that have a top-level task in the generated +// root Taskfile which dispatches to the platform-specific task via the GOOS +// variable (e.g. `build` -> `{{.GOOS}}:build`). For these we run the root task +// and pass GOOS as a variable, so user customisations in the root Taskfile are +// honoured for both native and cross-compilation builds. Verbs absent here +// (e.g. `sign`, which only exists per-platform) always target the +// platform-specific task directly. +var rootDispatchTasks = map[string]bool{ + "build": true, + "package": true, +} + func Build(buildFlags *flags.Build, otherArgs []string) error { if buildFlags.Tags != "" { otherArgs = append(otherArgs, "EXTRA_TAGS="+buildFlags.Tags) @@ -86,15 +98,28 @@ func wrapTask(action string, otherArgs []string) error { } } - taskName := action + // platformTaskName always targets the platform-specific task (e.g. + // "linux:build"). The experimental wake runner is built against this concrete + // name, so it keeps using it unconditionally. + platformTaskName := action if validPlatforms[goos] { - taskName = goos + ":" + action + platformTaskName = goos + ":" + action } - remainingArgs = append(remainingArgs, "ARCH="+goarch) + // Pass GOOS/ARCH through as Taskfile variables. The root build/package/run + // tasks dispatch on {{.GOOS}}, so running the root task (rather than the + // platform-prefixed one) means any customisations in the root Taskfile are + // honoured, for both native and cross-compilation builds. Verbs without a + // root task (e.g. `sign`) still target the platform task directly. See #5615. + remainingArgs = append(remainingArgs, "GOOS="+goos, "ARCH="+goarch) + + taskName := platformTaskName + if rootDispatchTasks[action] { + taskName = action + } if useWake() { - return runWakeTask(action, taskName, goos, goarch, remainingArgs) + return runWakeTask(action, platformTaskName, goos, goarch, remainingArgs) } newArgs := []string{"wails3", "task", taskName} diff --git a/v3/internal/commands/task_wrapper_test.go b/v3/internal/commands/task_wrapper_test.go index b9c059f11a8..c884afe3e2c 100644 --- a/v3/internal/commands/task_wrapper_test.go +++ b/v3/internal/commands/task_wrapper_test.go @@ -14,6 +14,13 @@ func TestWrapTask(t *testing.T) { currentOS := runtime.GOOS currentArch := runtime.GOARCH + // foreignOS is a GOOS guaranteed to differ from the host, so cross-compile + // cases are exercised regardless of which platform the test runs on. + foreignOS := "windows" + if currentOS == "windows" { + foreignOS = "linux" + } + tests := []struct { name string command string @@ -25,70 +32,88 @@ func TestWrapTask(t *testing.T) { expectedOsArgs []string }{ { - name: "Build with parameters uses current platform", + // Host-OS build runs the root Taskfile's `build` task (not the + // platform-prefixed one) so root customisations are honoured. The + // root task dispatches via the GOOS variable we pass through (#5615). + name: "Host build runs root build task", command: "build", otherArgs: []string{"CONFIG=debug"}, - expectedTaskName: currentOS + ":build", - expectedArgs: []string{"CONFIG=debug", "ARCH=" + currentArch}, - expectedOsArgs: []string{"wails3", "task", currentOS + ":build", "CONFIG=debug", "ARCH=" + currentArch}, + expectedTaskName: "build", + expectedArgs: []string{"CONFIG=debug", "GOOS=" + currentOS, "ARCH=" + currentArch}, + expectedOsArgs: []string{"wails3", "task", "build", "CONFIG=debug", "GOOS=" + currentOS, "ARCH=" + currentArch}, }, { - name: "Package with parameters uses current platform", + name: "Host package runs root package task", command: "package", otherArgs: []string{"VERSION=1.0.0", "OUTPUT=app.pkg"}, - expectedTaskName: currentOS + ":package", - expectedArgs: []string{"VERSION=1.0.0", "OUTPUT=app.pkg", "ARCH=" + currentArch}, - expectedOsArgs: []string{"wails3", "task", currentOS + ":package", "VERSION=1.0.0", "OUTPUT=app.pkg", "ARCH=" + currentArch}, + expectedTaskName: "package", + expectedArgs: []string{"VERSION=1.0.0", "OUTPUT=app.pkg", "GOOS=" + currentOS, "ARCH=" + currentArch}, + expectedOsArgs: []string{"wails3", "task", "package", "VERSION=1.0.0", "OUTPUT=app.pkg", "GOOS=" + currentOS, "ARCH=" + currentArch}, }, { - name: "Build without parameters", + name: "Host build without parameters runs root build task", command: "build", otherArgs: []string{}, - expectedTaskName: currentOS + ":build", - expectedArgs: []string{"ARCH=" + currentArch}, - expectedOsArgs: []string{"wails3", "task", currentOS + ":build", "ARCH=" + currentArch}, + expectedTaskName: "build", + expectedArgs: []string{"GOOS=" + currentOS, "ARCH=" + currentArch}, + expectedOsArgs: []string{"wails3", "task", "build", "GOOS=" + currentOS, "ARCH=" + currentArch}, }, { - name: "GOOS override changes task prefix", + // sign has no root dispatch task, so it always targets the platform. + name: "Sign always targets platform task", + command: "sign", + otherArgs: []string{"IDENTITY=Developer ID"}, + expectedTaskName: currentOS + ":sign", + expectedArgs: []string{"IDENTITY=Developer ID", "GOOS=" + currentOS, "ARCH=" + currentArch}, + expectedOsArgs: []string{"wails3", "task", currentOS + ":sign", "IDENTITY=Developer ID", "GOOS=" + currentOS, "ARCH=" + currentArch}, + }, + { + // Cross-OS build still runs the root `build` task; the GOOS variable + // carries the target so the root Taskfile dispatches to it. + name: "Cross-OS GOOS override runs root build task with GOOS var", command: "build", - otherArgs: []string{"GOOS=darwin", "CONFIG=release"}, - expectedTaskName: "darwin:build", - expectedArgs: []string{"CONFIG=release", "ARCH=" + currentArch}, - expectedOsArgs: []string{"wails3", "task", "darwin:build", "CONFIG=release", "ARCH=" + currentArch}, + otherArgs: []string{"GOOS=" + foreignOS, "CONFIG=release"}, + expectedTaskName: "build", + expectedArgs: []string{"CONFIG=release", "GOOS=" + foreignOS, "ARCH=" + currentArch}, + expectedOsArgs: []string{"wails3", "task", "build", "CONFIG=release", "GOOS=" + foreignOS, "ARCH=" + currentArch}, }, { - name: "GOARCH override changes ARCH arg", + // GOARCH alone (cross-arch, same OS) runs the root task; the root + // dispatch passes GOOS/ARCH through to the platform build. + name: "GOARCH-only override runs root build task", command: "build", otherArgs: []string{"GOARCH=arm64"}, - expectedTaskName: currentOS + ":build", - expectedArgs: []string{"ARCH=arm64"}, - expectedOsArgs: []string{"wails3", "task", currentOS + ":build", "ARCH=arm64"}, + expectedTaskName: "build", + expectedArgs: []string{"GOOS=" + currentOS, "ARCH=arm64"}, + expectedOsArgs: []string{"wails3", "task", "build", "GOOS=" + currentOS, "ARCH=arm64"}, }, { - name: "Both GOOS and GOARCH override", + name: "Cross-OS GOOS and GOARCH override", command: "package", - otherArgs: []string{"GOOS=windows", "GOARCH=386", "VERSION=2.0"}, - expectedTaskName: "windows:package", - expectedArgs: []string{"VERSION=2.0", "ARCH=386"}, - expectedOsArgs: []string{"wails3", "task", "windows:package", "VERSION=2.0", "ARCH=386"}, + otherArgs: []string{"GOOS=" + foreignOS, "GOARCH=386", "VERSION=2.0"}, + expectedTaskName: "package", + expectedArgs: []string{"VERSION=2.0", "GOOS=" + foreignOS, "ARCH=386"}, + expectedOsArgs: []string{"wails3", "task", "package", "VERSION=2.0", "GOOS=" + foreignOS, "ARCH=386"}, }, { - name: "Environment GOOS is used when no arg override", + name: "Environment GOOS (cross) is used when no arg override", command: "build", otherArgs: []string{"CONFIG=debug"}, - envGOOS: "darwin", - expectedTaskName: "darwin:build", - expectedArgs: []string{"CONFIG=debug", "ARCH=" + currentArch}, - expectedOsArgs: []string{"wails3", "task", "darwin:build", "CONFIG=debug", "ARCH=" + currentArch}, + envGOOS: foreignOS, + expectedTaskName: "build", + expectedArgs: []string{"CONFIG=debug", "GOOS=" + foreignOS, "ARCH=" + currentArch}, + expectedOsArgs: []string{"wails3", "task", "build", "CONFIG=debug", "GOOS=" + foreignOS, "ARCH=" + currentArch}, }, { + // Arg GOOS takes precedence over the GOOS environment variable; the + // passed-through GOOS var reflects the arg value. name: "Arg GOOS overrides environment GOOS", command: "build", - otherArgs: []string{"GOOS=linux"}, + otherArgs: []string{"GOOS=" + foreignOS}, envGOOS: "darwin", - expectedTaskName: "linux:build", - expectedArgs: []string{"ARCH=" + currentArch}, - expectedOsArgs: []string{"wails3", "task", "linux:build", "ARCH=" + currentArch}, + expectedTaskName: "build", + expectedArgs: []string{"GOOS=" + foreignOS, "ARCH=" + currentArch}, + expectedOsArgs: []string{"wails3", "task", "build", "GOOS=" + foreignOS, "ARCH=" + currentArch}, }, } @@ -195,8 +220,8 @@ func TestBuildCommand(t *testing.T) { err := Build(buildFlags, otherArgs) assert.NoError(t, err) - assert.Equal(t, currentOS+":build", capturedOptions.Name) - assert.Equal(t, []string{"CONFIG=release", "ARCH=" + currentArch}, capturedOtherArgs) + assert.Equal(t, "build", capturedOptions.Name) + assert.Equal(t, []string{"CONFIG=release", "GOOS=" + currentOS, "ARCH=" + currentArch}, capturedOtherArgs) } func TestBuildCommandWithTags(t *testing.T) { @@ -244,8 +269,8 @@ func TestBuildCommandWithTags(t *testing.T) { err := Build(buildFlags, otherArgs) assert.NoError(t, err) - assert.Equal(t, currentOS+":build", capturedOptions.Name) - assert.Equal(t, []string{"CONFIG=release", "EXTRA_TAGS=gtk4", "ARCH=" + currentArch}, capturedOtherArgs) + assert.Equal(t, "build", capturedOptions.Name) + assert.Equal(t, []string{"CONFIG=release", "EXTRA_TAGS=gtk4", "GOOS=" + currentOS, "ARCH=" + currentArch}, capturedOtherArgs) } func TestBuildCommandWithMultipleTags(t *testing.T) { @@ -292,8 +317,8 @@ func TestBuildCommandWithMultipleTags(t *testing.T) { err := Build(buildFlags, nil) assert.NoError(t, err) - assert.Equal(t, currentOS+":build", capturedOptions.Name) - assert.Equal(t, []string{"EXTRA_TAGS=gtk4,server", "ARCH=" + currentArch}, capturedOtherArgs) + assert.Equal(t, "build", capturedOptions.Name) + assert.Equal(t, []string{"EXTRA_TAGS=gtk4,server", "GOOS=" + currentOS, "ARCH=" + currentArch}, capturedOtherArgs) } func TestBuildCommandWithObfuscation(t *testing.T) { @@ -342,8 +367,8 @@ func TestBuildCommandWithObfuscation(t *testing.T) { err := Build(buildFlags, nil) assert.NoError(t, err) - assert.Equal(t, currentOS+":build", capturedOptions.Name) - assert.Equal(t, []string{"EXTRA_TAGS=gtk4", "OBFUSCATED=true", "GARBLE_ARGS=-literals -tiny", "ARCH=" + currentArch}, capturedOtherArgs) + assert.Equal(t, "build", capturedOptions.Name) + assert.Equal(t, []string{"EXTRA_TAGS=gtk4", "OBFUSCATED=true", "GARBLE_ARGS=-literals -tiny", "GOOS=" + currentOS, "ARCH=" + currentArch}, capturedOtherArgs) } func TestBuildCommandWithoutTags(t *testing.T) { @@ -389,8 +414,8 @@ func TestBuildCommandWithoutTags(t *testing.T) { err := Build(buildFlags, nil) assert.NoError(t, err) - assert.Equal(t, currentOS+":build", capturedOptions.Name) - assert.Equal(t, []string{"ARCH=" + currentArch}, capturedOtherArgs) + assert.Equal(t, "build", capturedOptions.Name) + assert.Equal(t, []string{"GOOS=" + currentOS, "ARCH=" + currentArch}, capturedOtherArgs) } func TestPackageCommand(t *testing.T) { @@ -437,8 +462,8 @@ func TestPackageCommand(t *testing.T) { err := Package(packageFlags, otherArgs) assert.NoError(t, err) - assert.Equal(t, currentOS+":package", capturedOptions.Name) - assert.Equal(t, []string{"VERSION=2.0.0", "OUTPUT=myapp.dmg", "ARCH=" + currentArch}, capturedOtherArgs) + assert.Equal(t, "package", capturedOptions.Name) + assert.Equal(t, []string{"VERSION=2.0.0", "OUTPUT=myapp.dmg", "GOOS=" + currentOS, "ARCH=" + currentArch}, capturedOtherArgs) } func TestSignWrapperCommand(t *testing.T) { @@ -486,5 +511,5 @@ func TestSignWrapperCommand(t *testing.T) { err := SignWrapper(signFlags, otherArgs) assert.NoError(t, err) assert.Equal(t, currentOS+":sign", capturedOptions.Name) - assert.Equal(t, []string{"IDENTITY=Developer ID", "ARCH=" + currentArch}, capturedOtherArgs) + assert.Equal(t, []string{"IDENTITY=Developer ID", "GOOS=" + currentOS, "ARCH=" + currentArch}, capturedOtherArgs) } diff --git a/v3/internal/templates/_common/Taskfile.tmpl.yml b/v3/internal/templates/_common/Taskfile.tmpl.yml index 89bbaa86c48..fd99895899f 100644 --- a/v3/internal/templates/_common/Taskfile.tmpl.yml +++ b/v3/internal/templates/_common/Taskfile.tmpl.yml @@ -5,6 +5,10 @@ vars: BIN_DIR: "bin" PACKAGE_MANAGER: '{{.Opn}}.PACKAGE_MANAGER | default "npm"{{.Cls}}' VITE_PORT: '{{.Opn}}.WAILS_VITE_PORT | default 9245{{.Cls}}' + # Target OS for build/package/run. Defaults to the host OS, and is overridden + # by `wails3 build GOOS=...` (or the GOOS env var) for cross-compilation. The + # tasks below dispatch to the matching platform Taskfile via this variable. + GOOS: '{{.Opn}}.GOOS | default OS{{.Cls}}' includes: common: ./build/Taskfile.yml @@ -18,17 +22,17 @@ tasks: build: summary: Builds the application cmds: - - task: "{{.Opn}}OS{{.Cls}}:build" + - task: "{{.Opn}}.GOOS{{.Cls}}:build" package: summary: Packages a production build of the application cmds: - - task: "{{.Opn}}OS{{.Cls}}:package" + - task: "{{.Opn}}.GOOS{{.Cls}}:package" run: summary: Runs the application cmds: - - task: "{{.Opn}}OS{{.Cls}}:run" + - task: "{{.Opn}}.GOOS{{.Cls}}:run" dev: summary: Runs the application in development mode diff --git a/v3/internal/templates/taskfile_template_test.go b/v3/internal/templates/taskfile_template_test.go index 58bfb9f933d..f78fb625839 100644 --- a/v3/internal/templates/taskfile_template_test.go +++ b/v3/internal/templates/taskfile_template_test.go @@ -17,3 +17,24 @@ func TestCommonTaskfileUsesBinaryName(t *testing.T) { assert.False(t, strings.Contains(content, `{{.ProjectName}}`), "root Taskfile template should not fall back to {{.ProjectName}} for APP_NAME") } + +// TestCommonTaskfileDispatchesViaGOOS guards the fix for #5615: the root +// build/package/run tasks must dispatch to the platform Taskfile via the GOOS +// variable (so `wails3 build`/`dev`/`package` run the root task and honour any +// customisations there, for both native and cross builds), rather than the +// built-in {{OS}} which the CLI used to bypass with an OS-prefixed task name. +func TestCommonTaskfileDispatchesViaGOOS(t *testing.T) { + data, err := templates.ReadFile("_common/Taskfile.tmpl.yml") + require.NoError(t, err, "_common/Taskfile.tmpl.yml should be present in embedded templates") + // Note: the template escapes literal go-task delimiters via {{.Opn}}/{{.Cls}}, + // so the rendered output contains `{{.GOOS}}` rather than the raw text here. + content := string(data) + for _, verb := range []string{"build", "package", "run"} { + assert.Contains(t, content, `{{.Opn}}.GOOS{{.Cls}}:`+verb, + "root Taskfile %s task should dispatch via {{.GOOS}}", verb) + } + assert.Contains(t, content, `GOOS: '{{.Opn}}.GOOS | default OS{{.Cls}}'`, + "root Taskfile should define a GOOS var defaulting to the host OS") + assert.NotContains(t, content, `{{.Opn}}OS{{.Cls}}:`, + "root Taskfile should not dispatch via the {{OS}} built-in (bypassed by the CLI)") +}