Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions v3/UNRELEASED_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ After processing, the content will be moved to the main changelog and this file

## Fixed
<!-- Bug fixes -->
- `wails3 build`/`dev`/`package` now run the project's root `Taskfile.yml` task instead of bypassing it with an OS-prefixed task, so customisations to the root `build`/`package`/`run` tasks are honoured. The root tasks dispatch to the platform Taskfile via a new `{{.GOOS}}` variable, which works for both native and cross-compilation builds (#5615)

## Deprecated
<!-- Soon-to-be removed features -->
Expand Down
33 changes: 29 additions & 4 deletions v3/internal/commands/task_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Comment on lines +24 to +34

func Build(buildFlags *flags.Build, otherArgs []string) error {
if buildFlags.Tags != "" {
otherArgs = append(otherArgs, "EXTRA_TAGS="+buildFlags.Tags)
Expand Down Expand Up @@ -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}
Expand Down
121 changes: 73 additions & 48 deletions v3/internal/commands/task_wrapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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},
},
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
10 changes: 7 additions & 3 deletions v3/internal/templates/_common/Taskfile.tmpl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
21 changes: 21 additions & 0 deletions v3/internal/templates/taskfile_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +21 to +25
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)")
}
Loading