Inital commit

This commit is contained in:
eric
2026-03-12 18:58:43 +01:00
commit 8555b02752
36 changed files with 3312 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary")
go_binary(
name = "frontend_dist_action",
srcs = ["frontend_dist_action.go"],
importpath = "github.com/Eriyc/rules_wails/wails_bun/tools/frontend_dist_action",
pure = "off",
visibility = ["//visibility:public"],
)
go_binary(
name = "bun_dev_session",
srcs = ["bun_dev_session.go"],
importpath = "github.com/Eriyc/rules_wails/wails_bun/tools/bun_dev_session",
pure = "off",
visibility = ["//visibility:public"],
)
go_binary(
name = "frontend_dev_server",
srcs = ["frontend_dev_server.go"],
importpath = "github.com/Eriyc/rules_wails/wails_bun/tools/frontend_dev_server",
pure = "off",
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,177 @@
package main
import (
"flag"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
func main() {
var frontendDev string
var frontendURL string
var readyTimeout time.Duration
var watchTarget string
var workspaceDir string
flag.StringVar(&frontendDev, "frontend-dev", "", "")
flag.StringVar(&frontendURL, "frontend-url", "http://127.0.0.1:9245", "")
flag.DurationVar(&readyTimeout, "ready-timeout", 2*time.Minute, "")
flag.StringVar(&watchTarget, "watch-target", "", "")
flag.StringVar(&workspaceDir, "workspace-dir", ".", "")
flag.Parse()
require(frontendDev != "", "missing --frontend-dev")
require(watchTarget != "", "missing --watch-target")
workspaceRoot := os.Getenv("BUILD_WORKSPACE_DIRECTORY")
if workspaceRoot == "" {
workspaceRoot = "."
}
runfilesDir := resolveRunfilesDir()
frontendCommand := exec.Command(frontendDev)
frontendCommand.Dir = workspaceRoot
frontendCommand.Env = withRunfilesEnv(os.Environ(), runfilesDir)
frontendCommand.Stdout = os.Stdout
frontendCommand.Stderr = os.Stderr
must(frontendCommand.Start())
waitCh := make(chan error, 1)
go func() {
waitCh <- frontendCommand.Wait()
}()
defer terminate(frontendCommand, waitCh)
must(waitForURL(frontendURL, readyTimeout, waitCh))
watchCommand, err := resolveWatchCommand(watchTarget)
must(err)
watchCommand.Dir = workspaceRoot
watchCommand.Env = os.Environ()
watchCommand.Stdout = os.Stdout
watchCommand.Stderr = os.Stderr
watchCommand.Stdin = os.Stdin
if err := watchCommand.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
os.Exit(exitError.ExitCode())
}
panic(err)
}
_ = workspaceDir
}
func resolveRunfilesDir() string {
for _, candidate := range []string{
os.Getenv("RUNFILES_DIR"),
os.Getenv("RUNFILES"),
} {
if candidate != "" {
return candidate
}
}
executablePath, err := os.Executable()
if err != nil {
return ""
}
normalizedPath := filepath.Clean(executablePath)
marker := ".runfiles"
if index := strings.Index(normalizedPath, marker+string(os.PathSeparator)); index >= 0 {
return normalizedPath[:index+len(marker)]
}
if strings.HasSuffix(normalizedPath, marker) {
return normalizedPath
}
return ""
}
func withRunfilesEnv(environment []string, runfilesDir string) []string {
if runfilesDir == "" {
return environment
}
environment = setEnv(environment, "RUNFILES_DIR", runfilesDir)
environment = setEnv(environment, "RUNFILES", runfilesDir)
return environment
}
func setEnv(environment []string, key string, value string) []string {
prefix := key + "="
for index, entry := range environment {
if strings.HasPrefix(entry, prefix) {
environment[index] = prefix + value
return environment
}
}
return append(environment, prefix+value)
}
func waitForURL(url string, timeout time.Duration, waitCh <-chan error) error {
client := http.Client{Timeout: 2 * time.Second}
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
select {
case err := <-waitCh:
if err == nil {
return fmt.Errorf("frontend dev server exited before becoming ready")
}
return fmt.Errorf("frontend dev server exited before becoming ready: %w", err)
default:
}
response, err := client.Get(url)
if err == nil {
response.Body.Close()
if response.StatusCode >= 200 && response.StatusCode < 400 {
return nil
}
}
time.Sleep(250 * time.Millisecond)
}
return fmt.Errorf("frontend dev server did not become ready at %s", url)
}
func resolveWatchCommand(target string) (*exec.Cmd, error) {
for _, candidate := range []string{"ibazel", "bazelisk", "bazel"} {
if _, err := exec.LookPath(candidate); err == nil {
return exec.Command(candidate, "run", target), nil
}
}
return nil, fmt.Errorf("neither ibazel, bazelisk, nor bazel is available in PATH")
}
func terminate(command *exec.Cmd, waitCh <-chan error) {
if command.Process == nil {
return
}
_ = command.Process.Kill()
select {
case <-waitCh:
case <-time.After(2 * time.Second):
}
}
func must(err error) {
if err != nil {
panic(err)
}
}
func require(condition bool, message string) {
if !condition {
panic(message)
}
}

View File

@@ -0,0 +1,139 @@
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
func main() {
var bunDarwinAarch64 string
var bunDarwinX64 string
var bunLinuxAarch64 string
var bunLinuxX64 string
var bunWindowsX64 string
var packageDir string
var script string
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(&packageDir, "package-dir", "", "")
flag.StringVar(&script, "script", "dev", "")
flag.Parse()
require(packageDir != "", "missing --package-dir")
bunPath, err := resolveBunBinary(bunDarwinAarch64, bunDarwinX64, bunLinuxAarch64, bunLinuxX64, bunWindowsX64)
must(err)
must(makeAbsolute(&bunPath))
workspaceRoot := os.Getenv("BUILD_WORKSPACE_DIRECTORY")
if workspaceRoot == "" {
returnError("BUILD_WORKSPACE_DIRECTORY is required for frontend dev sessions")
}
packageRoot := filepath.Join(workspaceRoot, filepath.FromSlash(packageDir))
if _, err := os.Stat(filepath.Join(packageRoot, "package.json")); err != nil {
returnError("frontend package.json not found in workspace package dir: " + packageRoot)
}
environment := append([]string{}, os.Environ()...)
environment = setPath(environment, buildPath(workspaceRoot, packageRoot))
command := exec.Command(bunPath, "--bun", "run", script)
command.Dir = packageRoot
command.Env = environment
command.Stdout = os.Stdout
command.Stderr = os.Stderr
command.Stdin = os.Stdin
if err := command.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
os.Exit(exitError.ExitCode())
}
panic(err)
}
}
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 buildPath(workspaceRoot string, packageRoot string) string {
entries := make([]string, 0, 4)
for _, candidate := range []string{
filepath.Join(packageRoot, "node_modules", ".bin"),
filepath.Join(workspaceRoot, "node_modules", ".bin"),
os.Getenv("PATH"),
} {
if candidate != "" {
entries = append(entries, candidate)
}
}
return strings.Join(entries, string(os.PathListSeparator))
}
func setPath(environment []string, pathValue string) []string {
return setEnv(environment, "PATH", pathValue)
}
func setEnv(environment []string, key string, value string) []string {
prefix := key + "="
for index, entry := range environment {
if strings.HasPrefix(entry, prefix) {
environment[index] = prefix + value
return environment
}
}
return append(environment, prefix+value)
}
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 returnError(message string) {
fmt.Fprintln(os.Stderr, message)
os.Exit(1)
}
func must(err error) {
if err != nil {
panic(err)
}
}
func require(condition bool, message string) {
if !condition {
panic(message)
}
}

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