package main import ( "bufio" "encoding/json" "flag" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" ) type manifestEntry struct { sourcePath string relativePath string } func main() { var buildScript string var bunDarwinAarch64 string var bunDarwinX64 string var bunLinuxAarch64 string var bunLinuxX64 string var bunWindowsX64 string var manifestPath string var nodeModulesRoot string var outDir string var packageDir string var sharedNodeModulesRoot string var workspaceDir string flag.StringVar(&buildScript, "build-script", "build", "") flag.StringVar(&bunDarwinAarch64, "bun-darwin-aarch64", "", "") flag.StringVar(&bunDarwinX64, "bun-darwin-x64", "", "") flag.StringVar(&bunLinuxAarch64, "bun-linux-aarch64", "", "") flag.StringVar(&bunLinuxX64, "bun-linux-x64", "", "") flag.StringVar(&bunWindowsX64, "bun-windows-x64", "", "") flag.StringVar(&manifestPath, "manifest", "", "") flag.StringVar(&nodeModulesRoot, "node-modules-root", "", "") flag.StringVar(&outDir, "out", "", "") flag.StringVar(&packageDir, "package-dir", ".", "") flag.StringVar(&sharedNodeModulesRoot, "shared-node-modules-root", "", "") flag.StringVar(&workspaceDir, "workspace-dir", "", "") flag.Parse() require(manifestPath != "", "missing --manifest") require(nodeModulesRoot != "", "missing --node-modules-root") require(outDir != "", "missing --out") require(packageDir != "", "missing --package-dir") require(sharedNodeModulesRoot != "", "missing --shared-node-modules-root") bunPath, err := resolveBunBinary(bunDarwinAarch64, bunDarwinX64, bunLinuxAarch64, bunLinuxX64, bunWindowsX64) must(err) must(makeAbsolute(&bunPath)) must(makeAbsolute(&manifestPath)) must(makeAbsolute(&nodeModulesRoot)) must(makeAbsolute(&outDir)) must(makeAbsolute(&sharedNodeModulesRoot)) tempRoot, err := os.MkdirTemp("", "rules-wails-frontend-*") must(err) defer os.RemoveAll(tempRoot) stageRoot := filepath.Join(tempRoot, "workspace") homeDir := filepath.Join(tempRoot, "home") must(os.MkdirAll(stageRoot, 0o755)) must(os.MkdirAll(homeDir, 0o755)) must(stageManifest(manifestPath, stageRoot)) must(os.MkdirAll(filepath.Join(stageRoot, "node_modules"), 0o755)) must(copyDirectoryContents(nodeModulesRoot, filepath.Join(stageRoot, "node_modules"), true)) sharedBunStore := filepath.Join(sharedNodeModulesRoot, ".bun") if pathExists(sharedBunStore) { stageBunStore := filepath.Join(stageRoot, "node_modules", ".bun") _ = os.RemoveAll(stageBunStore) must(os.Symlink(sharedBunStore, stageBunStore)) } must(restoreBunLinks(stageRoot)) must(overlayWorkspacePackages(stageRoot)) must(installWorkspaceAlias(stageRoot, packageDir, workspaceDir)) must(linkPackageNodeModules(stageRoot, packageDir)) packageRoot := stageRoot if packageDir != "." { packageRoot = filepath.Join(stageRoot, filepath.FromSlash(packageDir)) } command := exec.Command(bunPath, "--bun", "run", buildScript) command.Dir = packageRoot command.Env = append(os.Environ(), "HOME="+homeDir, "PATH="+buildPath(stageRoot, packageRoot), ) command.Stdout = os.Stdout command.Stderr = os.Stderr command.Stdin = os.Stdin must(command.Run()) must(os.RemoveAll(outDir)) must(os.MkdirAll(outDir, 0o755)) must(copyDirectoryContents(filepath.Join(packageRoot, "dist"), outDir, false)) } func resolveBunBinary(darwinAarch64 string, darwinX64 string, linuxAarch64 string, linuxX64 string, windowsX64 string) (string, error) { switch runtime.GOOS + "/" + runtime.GOARCH { case "darwin/arm64": return darwinAarch64, nil case "darwin/amd64": return darwinX64, nil case "linux/arm64": return linuxAarch64, nil case "linux/amd64": return linuxX64, nil case "windows/amd64": return windowsX64, nil default: return "", fmt.Errorf("unsupported Bun exec platform: %s/%s", runtime.GOOS, runtime.GOARCH) } } func overlayWorkspacePackages(stageRoot string) error { packageRoots := make([]string, 0) err := filepath.Walk(stageRoot, func(path string, info os.FileInfo, walkErr error) error { if walkErr != nil { return walkErr } if info.IsDir() && info.Name() == "node_modules" { return filepath.SkipDir } if !info.IsDir() && info.Name() == "package.json" { packageRoots = append(packageRoots, filepath.Dir(path)) } return nil }) if err != nil { return err } for _, packageRoot := range packageRoots { packageName, err := readPackageName(filepath.Join(packageRoot, "package.json")) if err != nil || packageName == "" { continue } nodePackageDir := filepath.Join(append([]string{stageRoot, "node_modules"}, strings.Split(packageName, "/")...)...) if !pathExists(nodePackageDir) { continue } entries, err := os.ReadDir(packageRoot) if err != nil { return err } for _, entry := range entries { entryPath := filepath.Join(packageRoot, entry.Name()) destinationPath := filepath.Join(nodePackageDir, entry.Name()) _ = os.RemoveAll(destinationPath) if err := os.Symlink(entryPath, destinationPath); err != nil { return err } } } return nil } func restoreBunLinks(stageRoot string) error { bunPackagesRoot := filepath.Join(stageRoot, "node_modules", ".bun", "node_modules") if !pathExists(bunPackagesRoot) { return nil } entries, err := os.ReadDir(bunPackagesRoot) if err != nil { return err } for _, entry := range entries { entryPath := filepath.Join(bunPackagesRoot, entry.Name()) if strings.HasPrefix(entry.Name(), "@") { scopeRoot := filepath.Join(stageRoot, "node_modules", entry.Name()) if err := os.MkdirAll(scopeRoot, 0o755); err != nil { return err } scopedEntries, err := os.ReadDir(entryPath) if err != nil { return err } for _, scopedEntry := range scopedEntries { scopedPath := filepath.Join(entryPath, scopedEntry.Name()) destinationPath := filepath.Join(scopeRoot, scopedEntry.Name()) _ = os.RemoveAll(destinationPath) if err := os.Symlink(scopedPath, destinationPath); err != nil { return err } } continue } destinationPath := filepath.Join(stageRoot, "node_modules", entry.Name()) _ = os.RemoveAll(destinationPath) if err := os.Symlink(entryPath, destinationPath); err != nil { return err } } return nil } func installWorkspaceAlias(stageRoot string, packageDir string, workspaceDir string) error { if workspaceDir == "" || workspaceDir == "." || workspaceDir == packageDir { return nil } targetPath := stageRoot if packageDir != "." { targetPath = filepath.Join(stageRoot, filepath.FromSlash(packageDir)) } aliasPath := filepath.Join(stageRoot, filepath.FromSlash(workspaceDir)) if err := os.MkdirAll(filepath.Dir(aliasPath), 0o755); err != nil { return err } _ = os.RemoveAll(aliasPath) return os.Symlink(targetPath, aliasPath) } func linkPackageNodeModules(stageRoot string, packageDir string) error { if packageDir == "." { return nil } packageNodeModules := filepath.Join(stageRoot, filepath.FromSlash(packageDir), "node_modules") if pathExists(packageNodeModules) { return nil } if err := os.MkdirAll(filepath.Dir(packageNodeModules), 0o755); err != nil { return err } return os.Symlink(filepath.Join(stageRoot, "node_modules"), packageNodeModules) } func buildPath(stageRoot string, packageRoot string) string { entries := make([]string, 0, 3) for _, candidate := range []string{ filepath.Join(packageRoot, "node_modules", ".bin"), filepath.Join(stageRoot, "node_modules", ".bin"), os.Getenv("PATH"), } { if candidate != "" { entries = append(entries, candidate) } } return strings.Join(entries, string(os.PathListSeparator)) } func stageManifest(manifestPath string, destinationRoot string) error { entries, err := readManifest(manifestPath) if err != nil { return err } for _, entry := range entries { destinationPath := filepath.Join(destinationRoot, filepath.FromSlash(entry.relativePath)) if err := os.MkdirAll(filepath.Dir(destinationPath), 0o755); err != nil { return err } if err := copyFile(entry.sourcePath, destinationPath); err != nil { return err } } return nil } func readManifest(path string) ([]manifestEntry, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() entries := make([]manifestEntry, 0) scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } parts := strings.SplitN(line, "\t", 2) if len(parts) != 2 { return nil, fmt.Errorf("invalid manifest line: %s", line) } entries = append(entries, manifestEntry{ sourcePath: parts[0], relativePath: parts[1], }) } return entries, scanner.Err() } func readPackageName(path string) (string, error) { file, err := os.Open(path) if err != nil { return "", err } defer file.Close() var payload struct { Name string `json:"name"` } if err := json.NewDecoder(file).Decode(&payload); err != nil { return "", err } return payload.Name, nil } func copyDirectoryContents(sourceRoot string, destinationRoot string, preserveSymlinks bool) error { if preserveSymlinks { copyBinary, err := resolveCopyBinary() if err != nil { return err } command := exec.Command(copyBinary, "-R", sourceRoot+"/.", destinationRoot+"/") command.Stdout = os.Stdout command.Stderr = os.Stderr return command.Run() } return filepath.Walk(sourceRoot, func(path string, info os.FileInfo, walkErr error) error { if walkErr != nil { return walkErr } relativePath, err := filepath.Rel(sourceRoot, path) if err != nil { return err } if relativePath == "." { return nil } destinationPath := filepath.Join(destinationRoot, relativePath) if info.IsDir() { return os.MkdirAll(destinationPath, 0o755) } if err := os.MkdirAll(filepath.Dir(destinationPath), 0o755); err != nil { return err } return copyFile(path, destinationPath) }) } func resolveCopyBinary() (string, error) { for _, candidate := range []string{"/bin/cp", "/usr/bin/cp"} { if pathExists(candidate) { return candidate, nil } } return "", fmt.Errorf("unable to locate cp binary for %s/%s", runtime.GOOS, runtime.GOARCH) } func copyFile(sourcePath string, destinationPath string) error { sourceFile, err := os.Open(sourcePath) if err != nil { return err } defer sourceFile.Close() destinationFile, err := os.Create(destinationPath) if err != nil { return err } defer destinationFile.Close() if _, err := io.Copy(destinationFile, sourceFile); err != nil { return err } return destinationFile.Chmod(0o644) } func makeAbsolute(path *string) error { if filepath.IsAbs(*path) { return nil } absolutePath, err := filepath.Abs(*path) if err != nil { return err } *path = absolutePath return nil } func pathExists(path string) bool { _, err := os.Stat(path) return err == nil } func must(err error) { if err != nil { panic(err) } } func require(condition bool, message string) { if !condition { panic(message) } }