diff --git a/cmd/gen-build-assets/main.go b/cmd/gen-build-assets/main.go index 4f4c265..d6f9723 100644 --- a/cmd/gen-build-assets/main.go +++ b/cmd/gen-build-assets/main.go @@ -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) { diff --git a/internal/build/asset_cache.go b/internal/build/asset_cache.go new file mode 100644 index 0000000..28eaad9 --- /dev/null +++ b/internal/build/asset_cache.go @@ -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 +} diff --git a/internal/build/build_assets.tar.gz b/internal/build/build_assets.tar.gz index bb2ff45..b6864fb 100644 Binary files a/internal/build/build_assets.tar.gz and b/internal/build/build_assets.tar.gz differ diff --git a/internal/build/compiler.go b/internal/build/compiler.go index 775d05c..e5ca563 100644 --- a/internal/build/compiler.go +++ b/internal/build/compiler.go @@ -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", ) is a valid wasmforge sysshim package source tree. +// In development mode (wasmforge module source on disk), B is +// /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) }