Single 53MB binary embedding PHP 8.4 ZTS runtime, Laravel 12, Livewire 4, and Octane worker mode inside a Wails v3 native desktop window. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
166 lines
4.5 KiB
Go
166 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
)
|
|
|
|
// AppEnvironment holds the resolved paths for the running application.
|
|
type AppEnvironment struct {
|
|
// DataDir is the persistent data directory (survives app updates).
|
|
DataDir string
|
|
// LaravelRoot is the extracted Laravel app in the temp directory.
|
|
LaravelRoot string
|
|
// DatabasePath is the full path to the SQLite database file.
|
|
DatabasePath string
|
|
}
|
|
|
|
// PrepareEnvironment creates data directories, generates .env, and symlinks
|
|
// storage so Laravel can write to persistent locations.
|
|
func PrepareEnvironment(laravelRoot string) (*AppEnvironment, error) {
|
|
dataDir, err := resolveDataDir()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve data dir: %w", err)
|
|
}
|
|
|
|
env := &AppEnvironment{
|
|
DataDir: dataDir,
|
|
LaravelRoot: laravelRoot,
|
|
DatabasePath: filepath.Join(dataDir, "core-app.sqlite"),
|
|
}
|
|
|
|
// Create persistent directories
|
|
dirs := []string{
|
|
dataDir,
|
|
filepath.Join(dataDir, "storage", "app"),
|
|
filepath.Join(dataDir, "storage", "framework", "cache", "data"),
|
|
filepath.Join(dataDir, "storage", "framework", "sessions"),
|
|
filepath.Join(dataDir, "storage", "framework", "views"),
|
|
filepath.Join(dataDir, "storage", "logs"),
|
|
}
|
|
for _, dir := range dirs {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return nil, fmt.Errorf("create dir %s: %w", dir, err)
|
|
}
|
|
}
|
|
|
|
// Create empty SQLite database if it doesn't exist
|
|
if _, err := os.Stat(env.DatabasePath); os.IsNotExist(err) {
|
|
if err := os.WriteFile(env.DatabasePath, nil, 0o644); err != nil {
|
|
return nil, fmt.Errorf("create database: %w", err)
|
|
}
|
|
log.Printf("Created new database: %s", env.DatabasePath)
|
|
}
|
|
|
|
// Replace the extracted storage/ with a symlink to the persistent one
|
|
extractedStorage := filepath.Join(laravelRoot, "storage")
|
|
os.RemoveAll(extractedStorage)
|
|
persistentStorage := filepath.Join(dataDir, "storage")
|
|
if err := os.Symlink(persistentStorage, extractedStorage); err != nil {
|
|
return nil, fmt.Errorf("symlink storage: %w", err)
|
|
}
|
|
|
|
// Generate .env file with resolved paths
|
|
if err := writeEnvFile(laravelRoot, env); err != nil {
|
|
return nil, fmt.Errorf("write .env: %w", err)
|
|
}
|
|
|
|
return env, nil
|
|
}
|
|
|
|
// resolveDataDir returns the OS-appropriate persistent data directory.
|
|
func resolveDataDir() (string, error) {
|
|
var base string
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
base = filepath.Join(home, "Library", "Application Support", "core-app")
|
|
case "linux":
|
|
if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
|
|
base = filepath.Join(xdg, "core-app")
|
|
} else {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
base = filepath.Join(home, ".local", "share", "core-app")
|
|
}
|
|
default:
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
base = filepath.Join(home, ".core-app")
|
|
}
|
|
return base, nil
|
|
}
|
|
|
|
// writeEnvFile generates the Laravel .env with resolved runtime paths.
|
|
func writeEnvFile(laravelRoot string, env *AppEnvironment) error {
|
|
appKey, err := loadOrGenerateAppKey(env.DataDir)
|
|
if err != nil {
|
|
return fmt.Errorf("app key: %w", err)
|
|
}
|
|
|
|
content := fmt.Sprintf(`APP_NAME="Core App"
|
|
APP_ENV=production
|
|
APP_KEY=%s
|
|
APP_DEBUG=false
|
|
APP_URL=http://localhost
|
|
|
|
DB_CONNECTION=sqlite
|
|
DB_DATABASE="%s"
|
|
|
|
CACHE_STORE=file
|
|
SESSION_DRIVER=file
|
|
LOG_CHANNEL=single
|
|
LOG_LEVEL=warning
|
|
`, appKey, env.DatabasePath)
|
|
|
|
return os.WriteFile(filepath.Join(laravelRoot, ".env"), []byte(content), 0o644)
|
|
}
|
|
|
|
// loadOrGenerateAppKey loads an existing APP_KEY from the data dir,
|
|
// or generates a new one and persists it.
|
|
func loadOrGenerateAppKey(dataDir string) (string, error) {
|
|
keyFile := filepath.Join(dataDir, ".app-key")
|
|
|
|
data, err := os.ReadFile(keyFile)
|
|
if err == nil && len(data) > 0 {
|
|
return string(data), nil
|
|
}
|
|
|
|
// Generate a new 32-byte key
|
|
key := make([]byte, 32)
|
|
if _, err := rand.Read(key); err != nil {
|
|
return "", fmt.Errorf("generate key: %w", err)
|
|
}
|
|
appKey := "base64:" + base64.StdEncoding.EncodeToString(key)
|
|
|
|
if err := os.WriteFile(keyFile, []byte(appKey), 0o600); err != nil {
|
|
return "", fmt.Errorf("save key: %w", err)
|
|
}
|
|
|
|
log.Printf("Generated new APP_KEY (saved to %s)", keyFile)
|
|
return appKey, nil
|
|
}
|
|
|
|
// appendEnv appends a key=value pair to the Laravel .env file.
|
|
func appendEnv(laravelRoot, key, value string) error {
|
|
envFile := filepath.Join(laravelRoot, ".env")
|
|
f, err := os.OpenFile(envFile, os.O_APPEND|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = fmt.Fprintf(f, "%s=\"%s\"\n", key, value)
|
|
return err
|
|
}
|