424 lines
11 KiB
Go
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)
|
|
}
|
|
}
|