Inital commit
This commit is contained in:
423
wails_bun/tools/frontend_dist_action.go
Normal file
423
wails_bun/tools/frontend_dist_action.go
Normal file
@@ -0,0 +1,423 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user