Files
wails_tools/wails_bun/tools/frontend_dist_action.go
2026-03-12 18:58:43 +01:00

424 lines
11 KiB
Go

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)
}
}