package deps import ( "os" "os/exec" "path/filepath" "strings" ) // Dependency describes an external CLI tool the platform may use. type Dependency struct { Name string `json:"name"` Available bool `json:"available"` Version string `json:"version,omitempty"` Path string `json:"path,omitempty"` Required bool `json:"required"` InstallHint string `json:"installHint,omitempty"` } // CheckAll probes all known external dependencies. // // 偵測順序(由 lookupBin 實作): // 1. $VISIONA_BUNDLE_BIN_DIR/(Wails shell 啟動 server 時設定) // 2. exec.LookPath()(系統 PATH) func CheckAll() []Dependency { return []Dependency{ check("ffmpeg", false, "macOS: brew install ffmpeg | Windows: winget install Gyan.FFmpeg", "-version"), check("yt-dlp", false, "macOS: brew install yt-dlp | Windows: winget install yt-dlp", "--version"), check("python3", false, "Required only for Kneron KL720 hardware. macOS: brew install python3", "--version"), } } // lookupBin 依序在 bundle bin dir 與 PATH 中找 binary。 // // bundle bin dir 來自 env var VISIONA_BUNDLE_BIN_DIR(由 Wails shell 於啟動 // server 時注入,指向 .app/Contents/Resources/bin/ 或開發模式 payload/darwin/bin/)。 func lookupBin(name string) (string, bool) { if dir := strings.TrimSpace(os.Getenv("VISIONA_BUNDLE_BIN_DIR")); dir != "" { candidate := filepath.Join(dir, name) if info, err := os.Stat(candidate); err == nil && !info.IsDir() { return candidate, true } } if p, err := exec.LookPath(name); err == nil { return p, true } return "", false } func check(name string, required bool, hint string, args ...string) Dependency { d := Dependency{ Name: name, Required: required, InstallHint: hint, } path, ok := lookupBin(name) if !ok { return d } d.Available = true d.Path = path // 效能:bundle 內的 binary(尤其是 yt-dlp PyInstaller 單檔)冷啟動可能需 20 秒, // 會阻塞 server startup。bundle binary 已知良好,跳過 version 查詢以加速啟動。 // 若之後需要版本字串,handler 可 lazy 再打一次。 if strings.HasPrefix(path, strings.TrimSpace(os.Getenv("VISIONA_BUNDLE_BIN_DIR"))) && os.Getenv("VISIONA_BUNDLE_BIN_DIR") != "" { d.Version = "(bundled)" return d } out, err := exec.Command(path, args...).Output() if err == nil { lines := strings.SplitN(string(out), "\n", 2) if len(lines) > 0 { d.Version = strings.TrimSpace(lines[0]) } } return d } // Logger is the minimal interface used for startup reporting. type Logger interface { Info(msg string, args ...interface{}) } // PrintStartupReport logs the status of every external dependency. func PrintStartupReport(logger Logger) { deps := CheckAll() logger.Info("External dependency check:") if dir := os.Getenv("VISIONA_BUNDLE_BIN_DIR"); dir != "" { logger.Info(" (bundle bin dir: %s)", dir) } for _, d := range deps { if d.Available { logger.Info(" [OK] %s: %s (%s)", d.Name, d.Version, d.Path) } else { tag := "OPTIONAL" if d.Required { tag = "MISSING" } logger.Info(" [%s] %s: not found — %s", tag, d.Name, d.InstallHint) } } }