fix(build): keep the index tools free of the Wails application
The v3 migration put application.Get() in backend/events and a ServiceStartup hook in backend/explore, both of which cmd/indexbuild reaches. v3's application package is GTK/WebKit bindings on Linux, so the index-artifact job — a plain golang container with CGO_ENABLED=0, on the stated grounds that neither command imports the app — stopped compiling with "undefined: pointer". That job owns the ~205 GB dump checkpoint, so it is the worst place to learn this. Both are behind the indexbuild tag now: the one app.Event.Emit lives in runtime_wails.go, runtime_indexbuild.go answers ErrNoRuntime (what the app itself returns before Run, so Deliver's callers need no second path), and explore's ServiceStartup moves to its own tagged file. TestIndexToolsDoNotImportWails walks `go list -deps -tags indexbuild` so the claim the workflow makes is checked rather than assumed.
This commit is contained in:
@@ -42,7 +42,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
# CGO is not needed: the project uses the pure-Go modernc sqlite
|
# CGO is not needed: the project uses the pure-Go modernc sqlite
|
||||||
# driver, and neither command imports the Wails app.
|
# driver, and neither command imports the Wails app — which is a
|
||||||
|
# claim with a test behind it now (cmd/indexbuild/deps_test.go),
|
||||||
|
# because the v3 migration quietly broke it and this job was where
|
||||||
|
# that surfaced.
|
||||||
image: golang:1.25
|
image: golang:1.25
|
||||||
# This host path must exist on the runner and be listed verbatim in
|
# This host path must exist on the runner and be listed verbatim in
|
||||||
# act_runner's container.valid_volumes. It holds explore-staging/
|
# act_runner's container.valid_volumes. It holds explore-staging/
|
||||||
|
|||||||
@@ -1701,6 +1701,21 @@ frontend would receive (`backend/queue/emit_test.go` is the model).
|
|||||||
`events.Deliver` is the same call returning an error instead of
|
`events.Deliver` is the same call returning an error instead of
|
||||||
dropping, and has one legitimate caller — `/__test/emit`.
|
dropping, and has one legitimate caller — `/__test/emit`.
|
||||||
|
|
||||||
|
**Naming the Wails application costs cgo, so exactly two files may.**
|
||||||
|
v3's `application` package is GTK/WebKit bindings on Linux, and
|
||||||
|
`cmd/indexbuild` / `cmd/indexexport` are built in a plain `golang`
|
||||||
|
container with `CGO_ENABLED=0` — the index workflow says so and it is
|
||||||
|
the one job that must not fail, since it owns the ~205 GB checkpoint.
|
||||||
|
So the single `app.Event.Emit` lives in `backend/events/runtime_wails.go`
|
||||||
|
under `//go:build !indexbuild` (with `runtime_indexbuild.go` returning
|
||||||
|
`ErrNoRuntime`, which is what the app itself returns before Run), and
|
||||||
|
`explore`'s `ServiceStartup` — the only other thing in that dependency
|
||||||
|
tree naming `application` — sits in `backend/explore/servicestartup.go`
|
||||||
|
under the same tag. `TestIndexToolsDoNotImportWails` walks the dependency
|
||||||
|
graph with `go list -deps -tags indexbuild` and is what keeps it that
|
||||||
|
way; a `ServiceStartup` hook added to a package the index tools import
|
||||||
|
is the way this comes back.
|
||||||
|
|
||||||
## Code Generation
|
## Code Generation
|
||||||
|
|
||||||
Two generators run via `go generate ./...` (or `make generate`):
|
Two generators run via `go generate ./...` (or `make generate`):
|
||||||
|
|||||||
+3
-14
@@ -4,8 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNoRuntime is returned by Deliver when there is neither a test Sink
|
// ErrNoRuntime is returned by Deliver when there is neither a test Sink
|
||||||
@@ -82,16 +80,7 @@ func Deliver(ctx context.Context, name string, data ...any) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// application.Get() returns nil when no app is running — under test,
|
// emitRuntime is the only place the Wails application is touched,
|
||||||
// before Run, and after shutdown. It does not terminate the
|
// and it is behind a build tag: see runtime_wails.go.
|
||||||
// process, which is what the v2 probe of the private "events"
|
return emitRuntime(name, data...)
|
||||||
// context key existed to avoid.
|
|
||||||
app := application.Get()
|
|
||||||
if app == nil {
|
|
||||||
return ErrNoRuntime
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Event.Emit(name, data...)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// allowedEmitters are the only files permitted to call the Wails
|
// allowedEmitters are the only files permitted to call the Wails
|
||||||
// runtime's event emitter directly.
|
// runtime's event emitter directly. There is one call and it lives in
|
||||||
|
// runtime_wails.go rather than emit.go because naming the application
|
||||||
|
// package is what forces cgo, and cmd/indexbuild builds this package
|
||||||
|
// without it.
|
||||||
var allowedEmitters = map[string]bool{
|
var allowedEmitters = map[string]bool{
|
||||||
filepath.Join("backend", "events", "emit.go"): true,
|
filepath.Join("backend", "events", "runtime_wails.go"): true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestNoDirectRuntimeEmits fails if anything outside backend/events
|
// TestNoDirectRuntimeEmits fails if anything outside backend/events
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build indexbuild
|
||||||
|
|
||||||
|
package events
|
||||||
|
|
||||||
|
// emitRuntime is the index tools' half of the split described in
|
||||||
|
// runtime_wails.go: under the indexbuild tag there is no Wails
|
||||||
|
// application to emit through, and importing the one that would be
|
||||||
|
// there costs cgo and a GTK/WebKit toolchain the index build container
|
||||||
|
// deliberately does not have.
|
||||||
|
//
|
||||||
|
// Returning ErrNoRuntime is the same answer the app gives before Run
|
||||||
|
// and after shutdown, so Emit's callers need no second code path: an
|
||||||
|
// event emitted by cmd/indexbuild is logged and dropped. A test that
|
||||||
|
// wants to observe one installs a Sink with WithSink, which Deliver
|
||||||
|
// consults first and which works under either tag.
|
||||||
|
func emitRuntime(_ string, _ ...any) error {
|
||||||
|
return ErrNoRuntime
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//go:build !indexbuild
|
||||||
|
|
||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
)
|
||||||
|
|
||||||
|
// emitRuntime pushes an event through the running Wails application.
|
||||||
|
//
|
||||||
|
// It is the app's half of a two-file split, and the split exists for a
|
||||||
|
// build constraint rather than a design one: v3's application package
|
||||||
|
// is cgo on Linux, so anything importing it needs GTK and WebKit
|
||||||
|
// headers to compile. cmd/indexbuild and cmd/indexexport reach this
|
||||||
|
// package through backend/explore and are built in a plain golang
|
||||||
|
// container with CGO_ENABLED=0 (.gitea/workflows/index-artifact.yml),
|
||||||
|
// where that is not available and not wanted. See
|
||||||
|
// runtime_indexbuild.go for the other half.
|
||||||
|
func emitRuntime(name string, data ...any) error {
|
||||||
|
// application.Get() returns nil when no app is running — under
|
||||||
|
// test, before Run, and after shutdown. It does not terminate the
|
||||||
|
// process, which is what the v2 probe of the private "events"
|
||||||
|
// context key existed to avoid.
|
||||||
|
app := application.Get()
|
||||||
|
if app == nil {
|
||||||
|
return ErrNoRuntime
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Event.Emit(name, data...)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
|
|
||||||
"yellowjacket/backend/database"
|
"yellowjacket/backend/database"
|
||||||
@@ -144,19 +143,9 @@ func (e *Service) CAALimiter() *RateLimiter {
|
|||||||
return e.caaLimiter
|
return e.caaLimiter
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceStartup is v3's service lifecycle hook: it runs once the
|
// The v3 service lifecycle hook that fills e.ctx lives in
|
||||||
// runtime exists, and ctx is cancelled when the app shuts down. It
|
// servicestartup.go, behind a build tag: naming
|
||||||
// replaces v2's SetContext, which had to be called by hand from
|
// application.ServiceOptions is what would drag cgo into cmd/indexbuild.
|
||||||
// OnStartup and was exported, so it was also bound to the frontend.
|
|
||||||
func (e *Service) ServiceStartup(
|
|
||||||
ctx context.Context,
|
|
||||||
_ application.ServiceOptions,
|
|
||||||
) error {
|
|
||||||
e.ctx = ctx
|
|
||||||
e.index.SetContext(ctx)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartIndexBuild kicks off the background search index build.
|
// StartIndexBuild kicks off the background search index build.
|
||||||
// Call this after the library scan completes so the indexer doesn't
|
// Call this after the library scan completes so the indexer doesn't
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//go:build !indexbuild
|
||||||
|
|
||||||
|
package explore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceStartup is v3's service lifecycle hook: it runs once the
|
||||||
|
// runtime exists, and ctx is cancelled when the app shuts down. It
|
||||||
|
// replaces v2's SetContext, which had to be called by hand from
|
||||||
|
// OnStartup and was exported, so it was also bound to the frontend.
|
||||||
|
//
|
||||||
|
// It is the one thing in this package that names the Wails application,
|
||||||
|
// and it is behind a build tag for the reason
|
||||||
|
// backend/events/runtime_wails.go states: cmd/indexbuild imports this
|
||||||
|
// package and is built without cgo, GTK or WebKit. Nothing under the
|
||||||
|
// indexbuild tag runs a Wails app, so the hook — and the context it
|
||||||
|
// installs — is simply absent there.
|
||||||
|
func (e *Service) ServiceStartup(
|
||||||
|
ctx context.Context,
|
||||||
|
_ application.ServiceOptions,
|
||||||
|
) error {
|
||||||
|
e.ctx = ctx
|
||||||
|
e.index.SetContext(ctx)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
//go:build indexbuild
|
||||||
|
|
||||||
|
package main_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wailsApp is the package that costs cgo: on Linux it is GTK and
|
||||||
|
// WebKit bindings, which the index build container does not have.
|
||||||
|
const wailsApp = "github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
|
||||||
|
// TestIndexToolsDoNotImportWails fails if cmd/indexbuild or
|
||||||
|
// cmd/indexexport reach the Wails application package.
|
||||||
|
//
|
||||||
|
// .gitea/workflows/index-artifact.yml builds both in a plain golang
|
||||||
|
// image with CGO_ENABLED=0, on the stated grounds that neither imports
|
||||||
|
// the app. That stopped being true when the v3 migration put
|
||||||
|
// application.Get() in backend/events and a ServiceStartup hook in
|
||||||
|
// backend/explore, and the workflow only found out at the point where
|
||||||
|
// it is most expensive to find out — the job that owns the 205 GB
|
||||||
|
// checkpoint. Both are behind the indexbuild tag now
|
||||||
|
// (backend/events/runtime_wails.go), and this is what keeps them there.
|
||||||
|
//
|
||||||
|
// It shells out to `go list` because the constraint is about the
|
||||||
|
// *linked* dependency graph under a particular tag, which no import of
|
||||||
|
// this package can observe.
|
||||||
|
func TestIndexToolsDoNotImportWails(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if _, err := exec.LookPath("go"); err != nil {
|
||||||
|
t.Skip("no go toolchain on PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range []string{
|
||||||
|
"yellowjacket/cmd/indexbuild",
|
||||||
|
"yellowjacket/cmd/indexexport",
|
||||||
|
} {
|
||||||
|
t.Run(pkg, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
out, err := exec.Command(
|
||||||
|
"go", "list", "-deps", "-tags", "indexbuild", pkg,
|
||||||
|
).Output()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("go list %s: %v", pkg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dep := range strings.Split(string(out), "\n") {
|
||||||
|
if strings.TrimSpace(dep) == wailsApp {
|
||||||
|
t.Errorf(
|
||||||
|
"%s imports %s, so it needs cgo and a GTK/WebKit "+
|
||||||
|
"toolchain; put the dependency behind the "+
|
||||||
|
"indexbuild tag",
|
||||||
|
pkg, wailsApp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user