fix(build): bundle sysshim into build_assets for distribution mode (#4)

When wasmforge runs as a standalone binary with no source tree on disk,
injectSysshimReplace and injectPuregoSysshimReplace failed with
"cannot find wasmforge module root for sysshim injection" because all
four findModuleRoot() fallback paths returned "".

Three-part fix:
- cmd/gen-build-assets: add addSysshimDir (recursive walk) and include
  the internal/sysshim tree under the "sysshim/" prefix in the bundle.
  File count rises from ~399 to 442.
- internal/build/asset_cache.go: new file providing
  ensureAssetsExtracted(), a process-lifetime sync.Once cache that
  extracts build_assets.tar.gz to a temp dir on first call.
- internal/build/compiler.go: add sysshimSourceBase() helper that
  prefers the on-disk module root (dev mode) and falls back to the
  extracted asset cache (distribution mode); rewire both injection
  functions to use it instead of calling findModuleRoot() directly.
This commit is contained in:
michaelweber
2026-06-16 17:04:34 -04:00
committed by GitHub
parent a983ca221e
commit 108f19cb36
4 changed files with 106 additions and 9 deletions
+41
View File
@@ -112,6 +112,15 @@ func generateArchive(moduleRoot, wazeroDir, outputPath string) (int, error) {
totalFiles += n
}
// Add sysshim tree (recursive — has subdirs and per-subdir go.mod files).
// Needed at runtime when a guest depends on golang.org/x/sys and the
// wasmforge module source isn't otherwise locatable on disk.
n, err = addSysshimDir(tw, filepath.Join(moduleRoot, "internal", "sysshim"))
if err != nil {
return 0, fmt.Errorf("adding sysshim: %w", err)
}
totalFiles += n
// Add go.sum.
gosumPath := filepath.Join(moduleRoot, "go.sum")
if err := addFile(tw, gosumPath, "go.sum"); err != nil {
@@ -122,6 +131,38 @@ func generateArchive(moduleRoot, wazeroDir, outputPath string) (int, error) {
return totalFiles, nil
}
// addSysshimDir adds the internal/sysshim tree under the "sysshim/" prefix.
// Walks recursively because sysshim has nested subdirs (unix, purego,
// windows, windows/svc, etc.) and per-subdir go.mod files that the
// injection step needs at runtime. Includes *.go, *.s, and go.mod; excludes
// _test.go.
func addSysshimDir(tw *tar.Writer, sysshimDir string) (int, error) {
count := 0
err := filepath.Walk(sysshimDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
name := info.Name()
if name != "go.mod" && !isSourceFile(name) {
return nil
}
rel, err := filepath.Rel(sysshimDir, path)
if err != nil {
return err
}
archivePath := filepath.ToSlash(filepath.Join("sysshim", rel))
if err := addFile(tw, path, archivePath); err != nil {
return err
}
count++
return nil
})
return count, err
}
// addWazeroDir adds the wazero fork to the archive under the "wazero/" prefix.
// It applies the wazero-specific skip list and only includes *.go, *.s, go.mod, go.sum files.
func addWazeroDir(tw *tar.Writer, wazeroDir string) (int, error) {
+42
View File
@@ -0,0 +1,42 @@
package build
import (
"fmt"
"os"
"sync"
)
var (
cachedAssetDirOnce sync.Once
cachedAssetDir string
cachedAssetDirErr error
)
// ensureAssetsExtracted returns a process-lifetime temp directory containing
// the extracted contents of the embedded build_assets bundle. The first call
// performs the extraction; subsequent calls return the cached path.
//
// The directory is not cleaned up — it lives under the OS temp directory
// (e.g. /tmp on Linux, /var/folders/... on macOS) where the OS reclaims it
// eventually. This avoids re-extracting on every sysshim injection and lets
// the directory survive across multiple build pipeline stages within a
// single wasmforge invocation.
//
// Callers should access well-known subtrees via their bundled prefixes:
// "hostmod", "runtime", "names", "wazero", "sysshim", and "go.sum".
func ensureAssetsExtracted() (string, error) {
cachedAssetDirOnce.Do(func() {
dir, err := os.MkdirTemp("", "wasmforge-assets-*")
if err != nil {
cachedAssetDirErr = fmt.Errorf("creating asset cache dir: %w", err)
return
}
if err := extractBuildAssets(dir); err != nil {
os.RemoveAll(dir)
cachedAssetDirErr = fmt.Errorf("extracting build assets: %w", err)
return
}
cachedAssetDir = dir
})
return cachedAssetDir, cachedAssetDirErr
}
Binary file not shown.
+23 -9
View File
@@ -254,6 +254,19 @@ func CompileWASM(patchedGOROOT, pkg, tmpDir string, verbose, win32APIs bool, tar
return wasmOut, nil
}
// sysshimSourceBase returns a directory B such that filepath.Join(B,
// "sysshim", <pkg>) is a valid wasmforge sysshim package source tree.
// In development mode (wasmforge module source on disk), B is
// <moduleRoot>/internal. In distribution mode (standalone binary), B is
// the cached temp directory populated from the embedded build_assets
// bundle, where the sysshim tree was packed under the "sysshim/" prefix.
func sysshimSourceBase() (string, error) {
if root := findModuleRoot(); root != "" {
return filepath.Join(root, "internal"), nil
}
return ensureAssetsExtracted()
}
// injectSysshimReplace checks whether the guest module at pkgDir depends on
// golang.org/x/sys. If it does, it creates a temporary copy of the module
// directory with the sysshim replacing the original x/sys code. For vendored
@@ -274,15 +287,16 @@ func injectSysshimReplace(pkgDir, tmpDir string, verbose, win32APIs bool) (strin
return "", nil
}
// Locate the sysshim directory (relative to the wasmforge module root).
moduleRoot := findModuleRoot()
if moduleRoot == "" {
// Locate the sysshim directory (relative to the wasmforge module root, or
// from the embedded build assets when running as a standalone binary).
base, sysErr := sysshimSourceBase()
if sysErr != nil {
if win32APIs {
return "", fmt.Errorf("cannot find wasmforge module root for sysshim injection")
return "", fmt.Errorf("locating wasmforge sysshim source: %w", sysErr)
}
return "", nil
}
sysshimDir := filepath.Join(moduleRoot, "internal", "sysshim")
sysshimDir := filepath.Join(base, "sysshim")
if _, err := os.Stat(sysshimDir); err != nil {
if win32APIs {
return "", fmt.Errorf("sysshim directory not found at %s", sysshimDir)
@@ -567,11 +581,11 @@ func injectPuregoSysshimReplace(pkgDir, tmpDir string, verbose bool) (string, er
return "", nil
}
moduleRoot := findModuleRoot()
if moduleRoot == "" {
return "", fmt.Errorf("cannot find wasmforge module root for purego sysshim injection")
base, sysErr := sysshimSourceBase()
if sysErr != nil {
return "", fmt.Errorf("locating wasmforge purego sysshim source: %w", sysErr)
}
puregoSysshimDir := filepath.Join(moduleRoot, "internal", "sysshim", "purego")
puregoSysshimDir := filepath.Join(base, "sysshim", "purego")
if _, err := os.Stat(puregoSysshimDir); err != nil {
return "", fmt.Errorf("purego sysshim directory not found at %s", puregoSysshimDir)
}