Critical bug fixes for the conditions of initial workspace setup and additions to the build scripts which now should fetch all the build dependencies automatically on all platforms.

This commit is contained in:
pszsh 2026-02-26 16:27:59 -08:00
parent 5d09f56e00
commit 968fd5a6c4
12 changed files with 329 additions and 29 deletions

View File

@ -30,13 +30,39 @@ A desktop application for generating 3D-printable solder paste stencils and snap
### Requirements
- [Go](https://go.dev/dl/) 1.21+
- [Wails CLI](https://wails.io/docs/gettingstarted/installation): `go install github.com/wailsapp/wails/v2/cmd/wails@latest`
- [Node.js](https://nodejs.org/) 18+ (for frontend build)
- macOS: Xcode Command Line Tools (`xcode-select --install`)
#### macOS
1. Install [Homebrew](https://brew.sh/) (if not already installed)
2. Install dependencies:
```bash
brew install go node
xcode-select --install # Xcode Command Line Tools
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```
#### Windows
1. Install dependencies (using [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), built into Windows 10/11):
```powershell
winget install GoLang.Go
winget install OpenJS.NodeJS.LTS
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```
Or download manually: [Go](https://go.dev/dl/) 1.21+, [Node.js](https://nodejs.org/) 18+
2. **WebView2 Runtime** — required by Wails. Pre-installed on most Windows 10/11 machines with Edge. If missing, download from [Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/).
#### Version Summary
| Tool | Minimum | Install docs |
|------|---------|--------------|
| Go | 1.21+ | https://go.dev/dl/ |
| Node.js | 18+ | https://nodejs.org/ |
| Wails CLI | v2 | https://wails.io/docs/gettingstarted/installation |
### Build
The build scripts check for missing dependencies and print install instructions if anything is missing.
**macOS:**
```bash
./build.sh

61
app.go
View File

@ -143,6 +143,7 @@ func (a *App) autosaveCutouts() {
}
func (a *App) startup(ctx context.Context) {
debugLog("app.startup() called")
a.ctx = ctx
// Render and cache the logo (PNG for favicon, SVG for background art)
@ -153,15 +154,19 @@ func (a *App) startup(ctx context.Context) {
a.imageServer.Store("/api/logo.png", buf.Bytes())
}
a.imageServer.Store("/api/logo.svg", formerLogoSVG)
debugLog("app.startup() done, logo stored (svg=%d bytes)", len(formerLogoSVG))
}
// ======== Landing Page ========
func (a *App) GetRecentProjects() []ProjectInfoJS {
debugLog("GetRecentProjects() called")
entries, err := ListProjects(20)
if err != nil {
debugLog("GetRecentProjects() error: %v", err)
return nil
}
debugLog("GetRecentProjects() found %d projects", len(entries))
var result []ProjectInfoJS
for _, e := range entries {
name := e.Data.ProjectName
@ -194,6 +199,13 @@ func (a *App) GetLogoDataURL() string {
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
}
func (a *App) GetLogoSVGDataURL() string {
if len(formerLogoSVG) == 0 {
return ""
}
return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString(formerLogoSVG)
}
// ======== File Dialogs ========
func (a *App) SelectFile(title string, patterns string) (string, error) {
@ -300,10 +312,15 @@ func (a *App) DiscoverGerberFiles(folderPath string) ([]string, error) {
}
func (a *App) BuildEnclosureSession(gbrjobPath string, gerberPaths []string, drillPath, npthPath string, wallThickness, wallHeight, clearance, dpi float64, exports []string) error {
debugLog("BuildEnclosureSession() called: gbrjob=%s gerbers=%d drill=%q npth=%q", gbrjobPath, len(gerberPaths), drillPath, npthPath)
debugLog(" params: wallThick=%.2f wallH=%.2f clearance=%.2f dpi=%.0f exports=%v", wallThickness, wallHeight, clearance, dpi, exports)
if gbrjobPath == "" {
debugLog(" ERROR: no gerber job file selected")
return fmt.Errorf("no gerber job file selected")
}
if len(gerberPaths) == 0 {
debugLog(" ERROR: no gerber files selected")
return fmt.Errorf("no gerber files selected")
}
@ -329,12 +346,21 @@ func (a *App) BuildEnclosureSession(gbrjobPath string, gerberPaths []string, dri
exports = []string{"stl", "scad"}
}
os.MkdirAll("temp", 0755)
tempDir := formerTempDir()
cwd, _ := os.Getwd()
debugLog(" CWD=%s, tempDir=%s", cwd, tempDir)
if err := os.MkdirAll(tempDir, 0755); err != nil {
debugLog(" ERROR creating temp dir: %v", err)
return fmt.Errorf("failed to create temp dir: %v", err)
}
uuid := randomID()
debugLog(" session uuid=%s", uuid)
// Copy gbrjob
gbrjobDst := filepath.Join("temp", uuid+"_"+filepath.Base(gbrjobPath))
gbrjobDst := filepath.Join(tempDir, uuid+"_"+filepath.Base(gbrjobPath))
debugLog(" copying gbrjob %s -> %s", gbrjobPath, gbrjobDst)
if err := CopyFile(gbrjobPath, gbrjobDst); err != nil {
debugLog(" ERROR copy gbrjob: %v", err)
return fmt.Errorf("failed to copy gbrjob: %v", err)
}
@ -343,8 +369,10 @@ func (a *App) BuildEnclosureSession(gbrjobPath string, gerberPaths []string, dri
var sourceDir string
for _, src := range gerberPaths {
baseName := filepath.Base(src)
dst := filepath.Join("temp", uuid+"_"+baseName)
dst := filepath.Join(tempDir, uuid+"_"+baseName)
debugLog(" copying gerber %s -> %s", src, dst)
if err := CopyFile(src, dst); err != nil {
debugLog(" ERROR copy gerber %s: %v", baseName, err)
return fmt.Errorf("failed to copy %s: %v", baseName, err)
}
savedGerbers[baseName] = dst
@ -356,36 +384,46 @@ func (a *App) BuildEnclosureSession(gbrjobPath string, gerberPaths []string, dri
// Copy drill files
var drillDst, npthDst string
if drillPath != "" {
drillDst = filepath.Join("temp", uuid+"_drill"+filepath.Ext(drillPath))
drillDst = filepath.Join(tempDir, uuid+"_drill"+filepath.Ext(drillPath))
debugLog(" copying drill %s -> %s", drillPath, drillDst)
if err := CopyFile(drillPath, drillDst); err != nil {
debugLog(" ERROR copy drill: %v", err)
return fmt.Errorf("failed to copy PTH drill: %v", err)
}
}
if npthPath != "" {
npthDst = filepath.Join("temp", uuid+"_npth"+filepath.Ext(npthPath))
npthDst = filepath.Join(tempDir, uuid+"_npth"+filepath.Ext(npthPath))
debugLog(" copying npth %s -> %s", npthPath, npthDst)
if err := CopyFile(npthPath, npthDst); err != nil {
debugLog(" ERROR copy npth: %v", err)
return fmt.Errorf("failed to copy NPTH drill: %v", err)
}
}
debugLog(" calling core BuildEnclosureSession...")
_, session, err := BuildEnclosureSession(gbrjobDst, savedGerbers, drillDst, npthDst, ecfg, exports)
if err != nil {
debugLog(" ERROR session build: %v", err)
return fmt.Errorf("session build failed: %v", err)
}
debugLog(" session built OK: project=%s board=%.1fx%.1fmm", session.ProjectName, session.BoardW, session.BoardH)
session.SourceDir = sourceDir
a.mu.Lock()
a.enclosureSession = session
a.cutouts = nil
a.mu.Unlock()
debugLog(" session stored in app state")
// Render board preview
if session.OutlineImg != nil {
var buf bytes.Buffer
png.Encode(&buf, session.OutlineImg)
a.imageServer.Store("/api/board-preview.png", buf.Bytes())
debugLog(" board preview rendered (%d bytes)", buf.Len())
}
debugLog("BuildEnclosureSession() returning nil (success)")
return nil
}
@ -605,6 +643,7 @@ func (a *App) ClearLidCutouts() {
}
func (a *App) GenerateEnclosureOutputs() (*GenerateResultJS, error) {
debugLog("GenerateEnclosureOutputs() called")
a.mu.RLock()
session := a.enclosureSession
allCutouts := make([]Cutout, len(a.cutouts))
@ -612,18 +651,22 @@ func (a *App) GenerateEnclosureOutputs() (*GenerateResultJS, error) {
a.mu.RUnlock()
if session == nil {
debugLog(" ERROR: no enclosure session active")
return nil, fmt.Errorf("no enclosure session active")
}
outputDir := session.SourceDir
if outputDir == "" {
outputDir = filepath.Join(".", "temp")
outputDir = formerTempDir()
}
debugLog(" outputDir=%s cutouts=%d", outputDir, len(allCutouts))
files, err := GenerateEnclosureOutputs(session, allCutouts, outputDir)
if err != nil {
debugLog(" ERROR generate: %v", err)
return nil, err
}
debugLog(" generated %d files", len(files))
// Auto-save session
inst := InstanceData{
@ -643,7 +686,7 @@ func (a *App) GenerateEnclosureOutputs() (*GenerateResultJS, error) {
ProjectName: session.ProjectName,
Cutouts: allCutouts,
}
if savedDir, saveErr := SaveSession(inst, filepath.Join(".", "temp"), session.OutlineImg); saveErr != nil {
if savedDir, saveErr := SaveSession(inst, formerTempDir(), session.OutlineImg); saveErr != nil {
log.Printf("Warning: could not save session: %v", saveErr)
} else {
a.mu.Lock()
@ -697,7 +740,7 @@ func (a *App) SaveEnclosureProfile(name string) error {
}
sourceDir := session.SourceDir
if sourceDir == "" {
sourceDir = filepath.Join(".", "temp")
sourceDir = formerTempDir()
}
savedDir, err := SaveProfile(inst, name, sourceDir, session.OutlineImg)
if err == nil && savedDir != "" {
@ -961,7 +1004,7 @@ func (a *App) GetOutputDir() (string, error) {
outputDir := session.SourceDir
if outputDir == "" {
outputDir = filepath.Join(".", "temp")
outputDir = formerTempDir()
}
absDir, err := filepath.Abs(outputDir)

View File

@ -1,7 +1,56 @@
@echo off
REM Build Former for Windows — run this on a Windows machine with Go and Wails installed
REM Prerequisites: Go 1.21+, Wails CLI (go install github.com/wailsapp/wails/v2/cmd/wails@latest)
REM Build Former for Windows
setlocal
REM --- Dependency checks ---
set "missing="
where go >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
set "missing=1"
echo ERROR: Go not found.
echo Install: winget install GoLang.Go
echo Or download from https://go.dev/dl/
echo.
)
where node >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
set "missing=1"
echo ERROR: Node.js not found.
echo Install: winget install OpenJS.NodeJS.LTS
echo Or download from https://nodejs.org/
echo.
)
where wails >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
set "missing=1"
echo ERROR: Wails CLI not found.
echo Install: go install github.com/wailsapp/wails/v2/cmd/wails@latest
echo Docs: https://wails.io/docs/gettingstarted/installation
echo.
)
REM Check for WebView2 runtime (required by Wails on Windows)
reg query "HKLM\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
reg query "HKLM\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
set "missing=1"
echo ERROR: WebView2 runtime not found.
echo Usually pre-installed on Windows 10/11 with Edge.
echo Download: https://developer.microsoft.com/en-us/microsoft-edge/webview2/
echo.
)
)
if defined missing (
echo Install the tools listed above and re-run this script.
exit /b 1
)
REM --- Build ---
echo Generating app icon...
go run ./cmd/genicon 2>nul && echo Icon generated. || echo Icon generation skipped.
@ -14,3 +63,4 @@ if %ERRORLEVEL% NEQ 0 (
echo.
echo Done: build\bin\Former.exe
endlocal

View File

@ -2,6 +2,49 @@
# Cross-compile Former for Windows (amd64) from macOS/Linux
set -e
# --- Dependency checks ---
missing=()
if ! command -v go &>/dev/null; then
missing+=("go")
echo "ERROR: Go not found."
if [[ "$OSTYPE" == darwin* ]]; then
echo " Install: brew install go"
else
echo " Install: sudo apt install golang (or your distro's package manager)"
fi
echo " Or download from https://go.dev/dl/"
echo ""
fi
if ! command -v node &>/dev/null; then
missing+=("node")
echo "ERROR: Node.js not found."
if [[ "$OSTYPE" == darwin* ]]; then
echo " Install: brew install node"
else
echo " Install: sudo apt install nodejs npm (or your distro's package manager)"
fi
echo " Or download from https://nodejs.org/"
echo ""
fi
if ! command -v wails &>/dev/null && [ ! -x "$HOME/go/bin/wails" ]; then
missing+=("wails")
echo "ERROR: Wails CLI not found."
echo " Install: go install github.com/wailsapp/wails/v2/cmd/wails@latest"
echo " Docs: https://wails.io/docs/gettingstarted/installation"
echo ""
fi
if [ ${#missing[@]} -gt 0 ]; then
echo "Missing dependencies: ${missing[*]}"
echo "Install the tools listed above and re-run this script."
exit 1
fi
# --- Build ---
# Generate app icon (needs CGO on macOS for native SVG rendering)
echo "Generating app icon..."
if [[ "$OSTYPE" == darwin* ]]; then

View File

@ -1,6 +1,48 @@
#!/bin/bash
# Build Former for macOS
set -e
# --- Dependency checks ---
missing=()
if ! command -v go &>/dev/null; then
missing+=("go")
echo "ERROR: Go not found."
echo " Install: brew install go"
echo " Or download from https://go.dev/dl/"
echo ""
fi
if ! command -v node &>/dev/null; then
missing+=("node")
echo "ERROR: Node.js not found."
echo " Install: brew install node"
echo " Or download from https://nodejs.org/"
echo ""
fi
if ! command -v wails &>/dev/null && [ ! -x "$HOME/go/bin/wails" ]; then
missing+=("wails")
echo "ERROR: Wails CLI not found."
echo " Install: go install github.com/wailsapp/wails/v2/cmd/wails@latest"
echo " Docs: https://wails.io/docs/gettingstarted/installation"
echo ""
fi
if ! xcode-select -p &>/dev/null; then
missing+=("xcode-cli")
echo "ERROR: Xcode Command Line Tools not found."
echo " Install: xcode-select --install"
echo ""
fi
if [ ${#missing[@]} -gt 0 ]; then
echo "Missing dependencies: ${missing[*]}"
echo "Install the tools listed above and re-run this script."
exit 1
fi
# --- Build ---
pkill -f "Former.app" || true
sleep 0.5

29
debug.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
# Build Former for macOS — DEBUG mode (logs to ~/former/debug.log)
set -e
pkill -f "Former.app" || true
sleep 0.5
export SDKROOT=$(xcrun --show-sdk-path)
export CC=/usr/bin/clang
export CGO_ENABLED=1
WAILS=$(command -v wails || echo "$HOME/go/bin/wails")
# Generate app icon from Former.svg
echo "Generating app icon..."
go run ./cmd/genicon 2>/dev/null && echo "Icon generated." || echo "Icon generation skipped."
echo "Building Former (DEBUG)..."
"$WAILS" build -skipbindings -tags debug
echo ""
echo "Debug build complete. Logs will be written to ~/former/debug.log"
echo "Launching..."
open build/bin/Former.app
# Tail the log so you can see it live
sleep 1
echo ""
echo "=== Tailing ~/former/debug.log (Ctrl-C to stop) ==="
tail -f ~/former/debug.log 2>/dev/null || echo "Waiting for log file..."

5
debug_off.go Normal file
View File

@ -0,0 +1,5 @@
//go:build !debug
package main
func debugLog(format string, args ...interface{}) {}

39
debug_on.go Normal file
View File

@ -0,0 +1,39 @@
//go:build debug
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"time"
)
var debugLogger *log.Logger
func init() {
home, _ := os.UserHomeDir()
logDir := filepath.Join(home, "former")
os.MkdirAll(logDir, 0755)
logPath := filepath.Join(logDir, "debug.log")
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
// Fall back to stderr
debugLogger = log.New(os.Stderr, "[DEBUG] ", log.Ltime|log.Lshortfile)
debugLogger.Printf("Could not open %s: %v — logging to stderr", logPath, err)
return
}
debugLogger = log.New(f, "[DEBUG] ", log.Ltime|log.Lshortfile)
debugLogger.Printf("=== Former debug session started %s ===", time.Now().Format(time.RFC3339))
debugLogger.Printf("Log file: %s", logPath)
fmt.Fprintf(os.Stderr, "[Former] Debug logging to %s\n", logPath)
}
func debugLog(format string, args ...interface{}) {
if debugLogger != nil {
debugLogger.Printf(format, args...)
}
}

View File

@ -53,8 +53,7 @@ function navigate(page) {
async function initLanding() {
const page = $('#page-landing');
const projects = await wails()?.GetRecentProjects() || [];
let logoSrc = '/api/logo.png';
const logoSrc = await wails()?.GetLogoSVGDataURL() || '';
let projectsHTML = '';
if (projects.length > 0) {
@ -73,13 +72,11 @@ async function initLanding() {
}
page.innerHTML = `
<div class="landing-bg-art">
<img src="/api/logo.svg" alt="" class="landing-bg-logo" onerror="this.style.display='none'">
</div>
<div class="landing-hero">
<h1>Former</h1>
<p>PCB Stencil & Enclosure Generator</p>
</div>
${logoSrc ? `<div class="landing-bg-art"><img src="${logoSrc}" alt="" class="landing-bg-logo"></div>` : ''}
<div class="landing-actions">
<div class="action-card" onclick="navigate('stencil')">
<h3>New Stencil</h3>
@ -286,19 +283,19 @@ function initEnclosure() {
<div class="card-title">Parameters</div>
<div class="form-row">
<span class="form-label">Wall Thickness (mm)</span>
<input class="form-input" id="enc-wall-thick" type="number" step="0.1" placeholder="1.5">
<input class="form-input" id="enc-wall-thick" type="number" step="0.1" value="1.5">
</div>
<div class="form-row">
<span class="form-label">Wall Height (mm)</span>
<input class="form-input" id="enc-wall-height" type="number" step="0.1" placeholder="10.0">
<input class="form-input" id="enc-wall-height" type="number" step="0.1" value="10.0">
</div>
<div class="form-row">
<span class="form-label">Clearance (mm)</span>
<input class="form-input" id="enc-clearance" type="number" step="0.1" placeholder="0.3">
<input class="form-input" id="enc-clearance" type="number" step="0.1" value="0.3">
</div>
<div class="form-row">
<span class="form-label">DPI</span>
<input class="form-input" id="enc-dpi" type="number" step="1" placeholder="600">
<input class="form-input" id="enc-dpi" type="number" step="1" value="600">
</div>
</div>
<div class="card">
@ -415,8 +412,16 @@ async function buildEnclosure() {
if ($('#enc-svg')?.checked) exports.push('svg');
if ($('#enc-png')?.checked) exports.push('png');
console.log('[Former] buildEnclosure: gbrjob=' + state.enclosure.gbrjobPath +
' gerbers=' + state.enclosure.gerberPaths.length +
' drill=' + state.enclosure.drillPath +
' npth=' + state.enclosure.npthPath +
' exports=' + exports.join(','));
console.log('[Former] wails() =', wails());
showLoading('Building enclosure session...');
try {
console.log('[Former] calling BuildEnclosureSession...');
await wails().BuildEnclosureSession(
state.enclosure.gbrjobPath,
state.enclosure.gerberPaths,
@ -428,9 +433,11 @@ async function buildEnclosure() {
parseFloat($('#enc-dpi')?.value) || 0,
exports,
);
console.log('[Former] BuildEnclosureSession succeeded, navigating to preview');
hideLoading();
navigate('preview');
} catch (e) {
console.error('[Former] BuildEnclosureSession FAILED:', e);
hideLoading();
alert('Session build failed: ' + e);
}

View File

@ -134,8 +134,6 @@ body {
width: 100%;
display: flex;
justify-content: center;
margin-top: -8px;
margin-bottom: -60px;
pointer-events: none;
z-index: 0;
-webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 20%, rgba(0,0,0,0.5) 100%);

View File

@ -45,8 +45,11 @@ func main() {
}
// Ensure working directories exist
os.MkdirAll("temp", 0755)
cwd, _ := os.Getwd()
debugLog("CWD: %s", cwd)
debugLog("Ensuring ~/former dirs (includes temp)...")
ensureFormerDirs()
debugLog("Startup complete, launching GUI")
runGUI()
}

View File

@ -17,6 +17,10 @@ func formerBaseDir() string {
return filepath.Join(home, "former")
}
func formerTempDir() string {
return filepath.Join(formerBaseDir(), "temp")
}
func formerSessionsDir() string {
return filepath.Join(formerBaseDir(), "sessions")
}
@ -26,8 +30,19 @@ func formerProfilesDir() string {
}
func ensureFormerDirs() {
os.MkdirAll(formerSessionsDir(), 0755)
os.MkdirAll(formerProfilesDir(), 0755)
td := formerTempDir()
sd := formerSessionsDir()
pd := formerProfilesDir()
debugLog("ensureFormerDirs: temp=%s sessions=%s profiles=%s", td, sd, pd)
if err := os.MkdirAll(td, 0755); err != nil {
debugLog(" ERROR creating temp dir: %v", err)
}
if err := os.MkdirAll(sd, 0755); err != nil {
debugLog(" ERROR creating sessions dir: %v", err)
}
if err := os.MkdirAll(pd, 0755); err != nil {
debugLog(" ERROR creating profiles dir: %v", err)
}
}
// ProjectEntry represents a saved project on disk