goexec

package
v1.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package goexec configures the `go` subprocesses forge runs on its own behalf. Graceful makes a cancelled one clean up after itself; Env keeps a caller's GOFLAGS=-mod=mod from letting one rewrite go.mod and go.sum (see env.go).

The leak

`cmd/go` and `cmd/link` do their work in scratch directories under TMPDIR — `go-build*` for the build cache staging area, `go-link-*` for the linker's object inputs, which for a large binary is hundreds of megabytes. Both tools remove their own scratch on exit, including on SIGINT and SIGTERM, which they trap.

They cannot remove it on SIGKILL, because nothing can.

exec.CommandContext's default cancellation is exactly SIGKILL: when the context is done it calls Process.Kill. So every forge build lane running under a cancellable context — a `forge env up` the developer ^C's, a deadline that fires, an agent that gives up on a run — orphans one scratch directory per interrupted `go` invocation, permanently. Measured on one developer machine: 128 `go-link-*` directories totalling 15 GB, plus 27 `go-build*` at 5.4 GB. Nothing ever reclaims them, because from the outside they are indistinguishable from the scratch of a build that is still running.

The fix

Graceful replaces the default kill with SIGINT and bounds the wait, so the toolchain gets the signal it already handles and the few hundred milliseconds it needs to unlink its scratch. WaitDelay is the backstop: if the child has not exited when it elapses, Go kills the process and closes the pipes, so a wedged compiler cannot turn a cancelled build into a hang. A cancel that reclaims nothing is still strictly better than the SIGKILL it replaced.

This is deliberately narrow. It is for subprocesses whose own exit path does cleanup worth waiting for — the Go toolchain is the case that motivated it. A process with no cleanup to do gains nothing and loses up to WaitDelay of shutdown latency, so do not reach for it by reflex.

Index

Constants

View Source
const GraceDelay = 10 * time.Second

GraceDelay is how long a signalled child may take to exit before it is killed outright. Ten seconds is generous for unlinking a scratch directory and still short enough that a ^C feels like a ^C.

Variables

This section is empty.

Functions

func CheckEnv added in v1.0.0

func CheckEnv(extra ...string) []string

CheckEnv is Env for a go command that only CHECKS code — compiles it and throws the output away (`go list -export`, a compile check, `go vet`) or loads it for an analyzer — with -trimpath added to GOFLAGS (WithTrimpath).

func Env

func Env(extra ...string) []string

Env is the environment for a `go` subprocess forge runs on its own behalf: the current process environment with -mod=mod removed from GOFLAGS, then extra (KEY=VALUE entries, appended verbatim).

Why -mod=mod, and only -mod=mod

-mod=mod is the one -mod value that lets `go build`, `go list` and everything built on them (go/packages, goimports) rewrite go.mod and go.sum as a side effect. The default, readonly, is what CI runs with. A caller that exports GOFLAGS=-mod=mod for its OWN go commands — control- plane's scripts/pin-sibling.sh did, for its `go get`s — therefore made `forge generate` record ~350 go.sum lines a CI regenerate never produces, and the tree went dirty for a reason that had nothing to do with the code. A generator's output must be a function of the repository, not of whoever happened to invoke it.

Everything else in GOFLAGS is kept: -tags, -trimpath, -buildvcs and -mod=vendor say how to build, not whether the build may edit the module files, and a user who sets them means them for forge's builds too.

What is not scrubbed

  • extra. It is what forge itself — or a project's declared build config, such as a GoBuild env block in KCL — asks for explicitly, so it is appended after the scrub and wins over the inherited value.
  • A GOFLAGS persisted with `go env -w`. That is machine-wide toolchain configuration the user's own `go build` obeys too, not a value a calling script leaked into forge, and an empty GOFLAGS cannot override it anyway (cmd/go treats an empty variable as unset).

func ExcludeNodeModules added in v1.0.0

func ExcludeNodeModules(ctx context.Context, dir string, env []string, patterns []string) []string

ExcludeNodeModules rewrites Go package patterns so a `...` wildcard stops matching packages that live under a node_modules directory.

The Go tool skips directories named testdata and those starting with "." or "_", but not node_modules. An npm package that ships Go sources (reliant's web/node_modules/flatted/golang/pkg/flatted, no go.mod of its own) is therefore a package of the enclosing module, and anything that analyses ./... — golangci-lint, go vet, go/packages — lints third-party code the project does not own and cannot fix.

The common case is untouched: with no node_modules directory under dir, or when every matched package lies outside one, patterns come back as given. Only when a package inside node_modules is matched are the patterns replaced by one ./<dir> per remaining package. If `go list` cannot answer (a broken go.mod, no toolchain) the patterns also come back as given, so the caller reports the real problem itself rather than this helper hiding it.

func Graceful

func Graceful(cmd *exec.Cmd) *exec.Cmd

Graceful configures cmd so that cancelling its context asks the child to exit rather than killing it, and returns cmd for call-site brevity.

Call it after exec.CommandContext and before Run/Output/CombinedOutput. It is a no-op on a nil cmd and on a cmd built without a context (Cancel is only consulted by CommandContext), so a call site that later loses its context does not silently change meaning.

On Windows there is no SIGINT to deliver: os.Interrupt is unsupported for Process.Signal there, and returning that error from Cancel makes Go fall back to its own Kill. That is the pre-existing behaviour, so Windows is no worse off and needs no build-tagged variant.

func WithTrimpath added in v1.0.0

func WithTrimpath(env []string) []string

WithTrimpath returns env with -trimpath appended to its effective GOFLAGS, unless that already says -trimpath (or -trimpath=false, which is an opt-out and is respected). The effective GOFLAGS is env's own GOFLAGS when it has one, else the value `go env -w` or the toolchain's go.env sets — cmd/go reads exactly one of the layers, so the merge keeps whichever the command would have used. env itself is not modified.

Why

Without -trimpath, cmd/go hashes every main-module package's ABSOLUTE directory into its build-cache key. A scratch copy (verify-generated regenerates in a fresh `.<repo>.forge-verify-*` every run) or another worktree of the same commit therefore compiles every package again while identical cache entries sit unused, and the test result cache misses with it. On a shared machine that compile happens while holding a heavy slot, so it is paid again in every other agent's wait. Measured on a 16-CPU Mac, one commit, two worktrees: the second compiled 267 packages and cached 0 of 73 test packages; with -trimpath, 0 compiles and 58 of 73 cached.

Not for everything

A binary someone runs or debugs keeps its paths: delve needs absolute ones (forge debug passes -trimpath=false), and the dev loop's stack traces stay clickable. So this is opt-in per call site, for checks only.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL