Files
AetherForge/server/internal/builder/estimate.go
drjones c95a4373de Add forge pipeline polish, simple forge UX, and fleet management upgrades.
Fusion copies prep icon and version info via go-winres; optional Garble obfuscation, Authenticode signing, and dry-run size estimates. Forge Simple mode with smart defaults; fleet roster gets compact expandable cards, filters, bulk commands, per-agent notes/tags, and typed WebSocket payloads.
2026-05-28 21:48:20 -07:00

130 lines
4.1 KiB
Go

package builder
import (
"fmt"
"path/filepath"
"strings"
)
const (
defaultWorkerBytes int64 = 12 * 1024 * 1024
defaultFusionStubBytes int64 = 2_500_000
resourcePatchOverhead int64 = 150_000
)
type FusionEstimateResponse struct {
PrepBytes int64 `json:"prep_bytes"`
PrepName string `json:"prep_name"`
EstimatedWorkerBytes int64 `json:"estimated_worker_bytes"`
EstimatedFusionStubBytes int64 `json:"estimated_fusion_stub_bytes"`
EstimatedResourcePatchBytes int64 `json:"estimated_resource_patch_bytes"`
EstimatedTotalBytes int64 `json:"estimated_total_bytes"`
OutputFileName string `json:"output_file_name"`
ProjectRootPath string `json:"project_root_path"`
ArchivePathHint string `json:"archive_path_hint"`
ExportPath string `json:"export_path,omitempty"`
Obfuscate bool `json:"obfuscate"`
SignBuild bool `json:"sign_build"`
Notes []string `json:"notes"`
}
func (h *Handler) estimateFusionBuild(req *BuildRequest, prepPath string, prepSize int64, prepName string) FusionEstimateResponse {
outputName := req.FusionOutputName
if outputName == "" {
outputName = prepName
}
if outputName == "" {
outputName = "prep.exe"
}
if !strings.HasSuffix(strings.ToLower(outputName), ".exe") {
outputName += ".exe"
}
outputName = sanitizeFileName(outputName)
workerBytes := h.estimateWorkerBytes()
stubBytes := defaultFusionStubBytes
total := prepSize + workerBytes + stubBytes + resourcePatchOverhead
root := h.projectRoot
if root == "" || root == "." {
root, _ = filepath.Abs(".")
}
projectOut := filepath.Join(root, outputName)
resp := FusionEstimateResponse{
PrepBytes: prepSize,
PrepName: prepName,
EstimatedWorkerBytes: workerBytes,
EstimatedFusionStubBytes: stubBytes,
EstimatedResourcePatchBytes: resourcePatchOverhead,
EstimatedTotalBytes: total,
OutputFileName: outputName,
ProjectRootPath: projectOut,
ArchivePathHint: filepath.Join(h.dataDir, "builds", "<build-id>", outputName),
Obfuscate: h.shouldObfuscate(req),
SignBuild: req.SignBuild,
Notes: []string{
fmt.Sprintf("Prep: %s", formatBytes(prepSize)),
fmt.Sprintf("Estimated worker: %s (from recent builds or default)", formatBytes(workerBytes)),
fmt.Sprintf("Fusion launcher overhead: ~%s", formatBytes(stubBytes)),
"Final size may differ slightly after icon + version info patch.",
},
}
if strings.TrimSpace(req.OutputDir) != "" {
clean := filepath.Clean(strings.TrimSpace(req.OutputDir))
if clean != "." && !strings.HasPrefix(clean, "..") && !filepath.IsAbs(clean) {
resp.ExportPath = filepath.Join(root, clean, outputName)
resp.Notes = append(resp.Notes, fmt.Sprintf("Secondary export: %s", resp.ExportPath))
}
}
if h.shouldObfuscate(req) && h.garblePath == "" {
resp.Notes = append(resp.Notes, "Garble not found — obfuscation will be skipped unless you install garble (run.bat installs it).")
}
if req.SignBuild && (!h.policy.Sign.Enabled || strings.TrimSpace(h.policy.Sign.CertThumbprint) == "") {
resp.Notes = append(resp.Notes, "Code signing requested but Calibrate has no certificate thumbprint configured.")
}
_ = prepPath
return resp
}
func (h *Handler) estimateWorkerBytes() int64 {
if h.db == nil {
return defaultWorkerBytes
}
builds, err := h.db.ListBuilds(40)
if err != nil || len(builds) == 0 {
return defaultWorkerBytes
}
var sum int64
var count int64
for _, b := range builds {
base := strings.ToLower(filepath.Base(b.FilePath))
if strings.HasPrefix(base, "worker-") || strings.HasPrefix(base, "install-") {
if b.FileSize > 0 {
sum += b.FileSize
count++
}
}
}
if count == 0 {
return defaultWorkerBytes
}
return sum / count
}
func formatBytes(n int64) string {
const unit = 1024
if n < unit {
return fmt.Sprintf("%d B", n)
}
div, exp := int64(unit), 0
for v := n / unit; v >= unit; v /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.2f %cB", float64(n)/float64(div), "KMGTPE"[exp])
}