- Avoid research checkout shadowing logo/script lookup - Strengthen packaging/CERT tests for multihash rules
584 lines
18 KiB
Go
584 lines
18 KiB
Go
package main
|
|
|
|
// PT-W3 / PT-06 tray / menu-bar launcher.
|
|
// Ships a working presence without a multi-month native C++ tray:
|
|
// Windows → platform/windows/scripts/verae-tray.ps1 (NotifyIcon + /status poll)
|
|
// → Startup shortcut via menubar --install (or install-tray-startup.ps1)
|
|
// → Start Menu folder via build-tray-startmenu.ps1 (WiX/NSIS-free stub)
|
|
// macOS → platform/macos/menubar/run-menubar.sh (NSStatusItem via Swift)
|
|
// → durable copy under ~/Library/Application Support/Verae/menubar
|
|
// → LaunchAgent via menubar --install; optional .app via build-menubar-app.sh
|
|
// IPP POST stays pure; this only opens scripts that GET /status and /queue.
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/statuslang"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
menubarLaunchLabel = "com.verae.timestamping.menubar"
|
|
winStartupLnkName = "Verae Tray.lnk"
|
|
)
|
|
|
|
// runMenubar starts the platform tray/menu-bar helper, or prints install hints.
|
|
func runMenubar(args []string) int {
|
|
fs := flag.NewFlagSet("menubar", flag.ContinueOnError)
|
|
fs.SetOutput(os.Stderr)
|
|
addr := fs.String("addr", defaultHelperAddr(), "helper loopback address for /status and /queue")
|
|
install := fs.Bool("install", false, "install at login (Windows Startup shortcut; macOS LaunchAgent)")
|
|
uninstall := fs.Bool("uninstall", false, "remove login install (Windows Startup shortcut; macOS LaunchAgent)")
|
|
foreground := fs.Bool("foreground", false, "run attached (default: detach on Windows/macOS)")
|
|
if err := fs.Parse(args); err != nil {
|
|
return 2
|
|
}
|
|
_ = os.Setenv("VERAE_IPP_ADDR", *addr)
|
|
|
|
if *uninstall {
|
|
return menubarUninstall()
|
|
}
|
|
if *install {
|
|
return menubarInstall(*addr)
|
|
}
|
|
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
return startWindowsTray(*addr, *foreground)
|
|
case "darwin":
|
|
return startDarwinMenubar(*addr, *foreground)
|
|
default:
|
|
fmt.Fprintln(os.Stderr, "menubar: no tray script on this OS — use: verae-print tray-status --open")
|
|
fmt.Fprintln(os.Stderr, " loopback: GET http://"+*addr+"/queue and GET http://"+*addr+"/status")
|
|
return runTrayStatus([]string{"--open", "--addr", *addr})
|
|
}
|
|
}
|
|
|
|
func startWindowsTray(addr string, foreground bool) int {
|
|
script, err := locateWindowsTrayScript()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar: verae-tray.ps1 not found (%v)\n", err)
|
|
fmt.Fprintln(os.Stderr, "fallback: verae-print tray-status --open")
|
|
return runTrayStatus([]string{"--open", "--addr", addr})
|
|
}
|
|
ps := windowsPowerShell()
|
|
args := []string{
|
|
"-NoProfile", "-ExecutionPolicy", "Bypass", "-STA",
|
|
"-File", script,
|
|
"-Addr", addr,
|
|
}
|
|
cmd := exec.Command(ps, args...)
|
|
if foreground {
|
|
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar: tray exited: %v\n", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
hideWindow(cmd)
|
|
if err := cmd.Start(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar: start tray: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Printf("Verae tray started (pid %d) · polls http://%s/status · queue http://%s/queue\n", cmd.Process.Pid, addr, addr)
|
|
fmt.Println("Install at login: verae-print menubar --install")
|
|
fmt.Println(statuslang.EvidenceNote)
|
|
return 0
|
|
}
|
|
|
|
func startDarwinMenubar(addr string, foreground bool) int {
|
|
script, err := locateDarwinMenubarScript()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar: run-menubar.sh not found (%v)\n", err)
|
|
fmt.Fprintln(os.Stderr, "fallback: verae-print tray-status --open")
|
|
return runTrayStatus([]string{"--open", "--addr", addr})
|
|
}
|
|
_ = os.Chmod(script, 0o755)
|
|
cmd := exec.Command("/bin/bash", script)
|
|
cmd.Env = append(os.Environ(), "VERAE_IPP_ADDR="+addr)
|
|
if foreground {
|
|
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar: %v\n", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar: start: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Printf("Verae menu bar started (pid %d) · polls http://%s/status · queue http://%s/queue\n", cmd.Process.Pid, addr, addr)
|
|
fmt.Println("Install at login: verae-print menubar --install")
|
|
fmt.Println(statuslang.EvidenceNote)
|
|
return 0
|
|
}
|
|
|
|
func menubarInstall(addr string) int {
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
return menubarInstallWindows(addr)
|
|
case "darwin":
|
|
return menubarInstallDarwin(addr)
|
|
default:
|
|
fmt.Fprintln(os.Stderr, "menubar --install: no tray packaging on this OS")
|
|
fmt.Fprintln(os.Stderr, " use: verae-print tray-status --open")
|
|
return 2
|
|
}
|
|
}
|
|
|
|
func menubarUninstall() int {
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
return menubarUninstallWindows()
|
|
case "darwin":
|
|
return menubarUninstallDarwin()
|
|
default:
|
|
fmt.Fprintln(os.Stderr, "menubar --uninstall: no tray packaging on this OS")
|
|
return 2
|
|
}
|
|
}
|
|
|
|
// --- Windows Startup shortcut ------------------------------------------------
|
|
|
|
func menubarInstallWindows(addr string) int {
|
|
tray, err := locateWindowsTrayScript()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar --install: %v\n", err)
|
|
return 1
|
|
}
|
|
installer, err := locateWindowsStartupInstaller()
|
|
if err == nil {
|
|
ps := windowsPowerShell()
|
|
cmd := exec.Command(ps,
|
|
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
|
"-File", installer,
|
|
"-Addr", addr,
|
|
"-TrayScript", tray,
|
|
)
|
|
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar --install: %v\n", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
// Fallback without install-tray-startup.ps1: create .lnk via PowerShell COM.
|
|
if err := writeWindowsStartupShortcut(tray, addr); err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar --install: %v\n", err)
|
|
return 1
|
|
}
|
|
fmt.Println("Installed Startup shortcut:", windowsStartupShortcutPath())
|
|
fmt.Println("Tray starts at next login. Run now: verae-print menubar")
|
|
fmt.Println(statuslang.EvidenceNote)
|
|
return 0
|
|
}
|
|
|
|
func menubarUninstallWindows() int {
|
|
installer, err := locateWindowsStartupInstaller()
|
|
if err == nil {
|
|
ps := windowsPowerShell()
|
|
cmd := exec.Command(ps,
|
|
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
|
"-File", installer,
|
|
"-Uninstall",
|
|
)
|
|
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar --uninstall: %v\n", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
path := windowsStartupShortcutPath()
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
fmt.Println("Removed Startup shortcut:", path)
|
|
return 0
|
|
}
|
|
|
|
func windowsStartupShortcutPath() string {
|
|
appData := os.Getenv("APPDATA")
|
|
if appData == "" {
|
|
home, _ := os.UserHomeDir()
|
|
appData = filepath.Join(home, "AppData", "Roaming")
|
|
}
|
|
return filepath.Join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", winStartupLnkName)
|
|
}
|
|
|
|
func writeWindowsStartupShortcut(trayScript, addr string) error {
|
|
ps := windowsPowerShell()
|
|
lnk := windowsStartupShortcutPath()
|
|
if err := os.MkdirAll(filepath.Dir(lnk), 0o755); err != nil {
|
|
return err
|
|
}
|
|
// Escape single quotes for PowerShell single-quoted strings.
|
|
esc := func(s string) string { return strings.ReplaceAll(s, "'", "''") }
|
|
psExe := filepath.Join(os.Getenv("WINDIR"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
if _, err := os.Stat(psExe); err != nil {
|
|
psExe = "powershell.exe"
|
|
}
|
|
args := fmt.Sprintf(`-NoProfile -ExecutionPolicy Bypass -STA -WindowStyle Hidden -File "%s" -Addr %s`, trayScript, addr)
|
|
script := fmt.Sprintf(`
|
|
$ErrorActionPreference = 'Stop'
|
|
$wsh = New-Object -ComObject WScript.Shell
|
|
$sc = $wsh.CreateShortcut('%s')
|
|
$sc.TargetPath = '%s'
|
|
$sc.Arguments = '%s'
|
|
$sc.WorkingDirectory = '%s'
|
|
$sc.WindowStyle = 7
|
|
$sc.Description = 'Verae Timestamping tray — polls GET /status; hold id ≠ evidence.'
|
|
$sc.Save()
|
|
`, esc(lnk), esc(psExe), esc(args), esc(filepath.Dir(trayScript)))
|
|
cmd := exec.Command(ps, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func windowsPowerShell() string {
|
|
ps := filepath.Join(os.Getenv("WINDIR"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
if _, err := os.Stat(ps); err != nil {
|
|
return "powershell.exe"
|
|
}
|
|
return ps
|
|
}
|
|
|
|
func locateWindowsTrayScript() (string, error) {
|
|
script, err := findRepoFile("platform", "windows", "scripts", "verae-tray.ps1")
|
|
if err == nil {
|
|
return script, nil
|
|
}
|
|
// Installer layout next to the exe.
|
|
for _, alt := range []string{
|
|
filepath.Join(exeDirHint(), "scripts", "verae-tray.ps1"),
|
|
filepath.Join(exeDirHint(), "..", "scripts", "verae-tray.ps1"),
|
|
filepath.Join(exeDirHint(), "verae-tray.ps1"),
|
|
} {
|
|
if fileExists(alt) {
|
|
abs, _ := filepath.Abs(alt)
|
|
return abs, nil
|
|
}
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
func locateWindowsStartupInstaller() (string, error) {
|
|
script, err := findRepoFile("platform", "windows", "scripts", "install-tray-startup.ps1")
|
|
if err == nil {
|
|
return script, nil
|
|
}
|
|
for _, alt := range []string{
|
|
filepath.Join(exeDirHint(), "scripts", "install-tray-startup.ps1"),
|
|
filepath.Join(exeDirHint(), "..", "scripts", "install-tray-startup.ps1"),
|
|
} {
|
|
if fileExists(alt) {
|
|
abs, _ := filepath.Abs(alt)
|
|
return abs, nil
|
|
}
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
// --- macOS durable LaunchAgent -----------------------------------------------
|
|
|
|
func menubarSupportDir(home string) string {
|
|
return filepath.Join(home, "Library", "Application Support", "Verae", "menubar")
|
|
}
|
|
|
|
func menubarInstallDarwin(addr string) int {
|
|
srcScript, err := findRepoFile("platform", "macos", "menubar", "run-menubar.sh")
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
srcDir := filepath.Dir(srcScript)
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
support := menubarSupportDir(home)
|
|
if err := os.MkdirAll(support, 0o755); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
// Copy durable assets so LaunchAgent does not depend on the checkout path.
|
|
for _, name := range []string{"run-menubar.sh", "verae-menubar.swift", "com.verae.timestamping.menubar.plist"} {
|
|
src := filepath.Join(srcDir, name)
|
|
if !fileExists(src) {
|
|
continue
|
|
}
|
|
dst := filepath.Join(support, name)
|
|
if err := copyFile(src, dst); err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar --install: copy %s: %v\n", name, err)
|
|
return 1
|
|
}
|
|
}
|
|
script := filepath.Join(support, "run-menubar.sh")
|
|
_ = os.Chmod(script, 0o755)
|
|
|
|
// Best-effort compile so LaunchAgent does not need swift at login.
|
|
bin := filepath.Join(support, "verae-menubar")
|
|
swiftSrc := filepath.Join(support, "verae-menubar.swift")
|
|
if fileExists(swiftSrc) {
|
|
if path, lookErr := exec.LookPath("swiftc"); lookErr == nil {
|
|
c := exec.Command(path, "-O", "-o", bin, swiftSrc)
|
|
if out, err := c.CombinedOutput(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar: swiftc optional compile failed (will use swift at run): %v\n%s\n", err, strings.TrimSpace(string(out)))
|
|
} else {
|
|
_ = os.Chmod(bin, 0o755)
|
|
}
|
|
}
|
|
}
|
|
|
|
logDir := filepath.Join(home, "Library", "Logs")
|
|
_ = os.MkdirAll(logDir, 0o755)
|
|
agents := filepath.Join(home, "Library", "LaunchAgents")
|
|
_ = os.MkdirAll(agents, 0o755)
|
|
plistPath := filepath.Join(agents, menubarLaunchLabel+".plist")
|
|
|
|
body := menubarPlistBody(script, logDir, addr)
|
|
// Prefer rewriting the template if present.
|
|
if tpl := filepath.Join(support, "com.verae.timestamping.menubar.plist"); fileExists(tpl) {
|
|
raw, err := os.ReadFile(tpl)
|
|
if err == nil {
|
|
s := string(raw)
|
|
s = strings.ReplaceAll(s, "MENUBAR_SCRIPT_PLACEHOLDER", script)
|
|
s = strings.ReplaceAll(s, "LOG_DIR_PLACEHOLDER", logDir)
|
|
// Ensure VERAE_IPP_ADDR matches --addr.
|
|
if strings.Contains(s, "<key>VERAE_IPP_ADDR</key>") {
|
|
// Replace the next <string>…</string> after VERAE_IPP_ADDR.
|
|
s = replaceEnvString(s, "VERAE_IPP_ADDR", addr)
|
|
}
|
|
body = s
|
|
}
|
|
}
|
|
if err := os.WriteFile(plistPath, []byte(body), 0o644); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
|
|
if err := launchctlLoadMenubar(plistPath); err != nil {
|
|
fmt.Fprintf(os.Stderr, "menubar: wrote %s but launchctl load failed: %v\n", plistPath, err)
|
|
fmt.Fprintln(os.Stderr, " load manually after login, or: verae-print menubar")
|
|
fmt.Fprintln(os.Stderr, " assets:", support)
|
|
return 1
|
|
}
|
|
fmt.Println("Installed LaunchAgent:", plistPath)
|
|
fmt.Println("Durable assets:", support)
|
|
fmt.Println("Menu bar polls http://" + addr + "/status · Open queue → /queue")
|
|
fmt.Println("Optional .app: platform/macos/menubar/build-menubar-app.sh")
|
|
fmt.Println(statuslang.EvidenceNote)
|
|
return 0
|
|
}
|
|
|
|
func menubarUninstallDarwin() int {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
plistPath := filepath.Join(home, "Library", "LaunchAgents", menubarLaunchLabel+".plist")
|
|
_ = launchctlUnloadMenubar(plistPath)
|
|
if err := os.Remove(plistPath); err != nil && !os.IsNotExist(err) {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
// Leave Application Support assets; user may still run menubar manually.
|
|
fmt.Println("Removed LaunchAgent:", plistPath)
|
|
fmt.Println("Assets kept at:", menubarSupportDir(home), "(delete manually if desired)")
|
|
return 0
|
|
}
|
|
|
|
func menubarPlistBody(script, logDir, addr string) string {
|
|
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>%s</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/bin/bash</string>
|
|
<string>%s</string>
|
|
</array>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>KeepAlive</key>
|
|
<true/>
|
|
<key>ProcessType</key>
|
|
<string>Interactive</string>
|
|
<key>StandardOutPath</key>
|
|
<string>%s/verae-menubar.launchd.out.log</string>
|
|
<key>StandardErrorPath</key>
|
|
<string>%s/verae-menubar.launchd.err.log</string>
|
|
<key>EnvironmentVariables</key>
|
|
<dict>
|
|
<key>VERAE_IPP_ADDR</key>
|
|
<string>%s</string>
|
|
</dict>
|
|
</dict>
|
|
</plist>
|
|
`, menubarLaunchLabel, script, logDir, logDir, addr)
|
|
}
|
|
|
|
func launchctlLoadMenubar(plistPath string) error {
|
|
uid := os.Getuid()
|
|
domain := "gui/" + strconv.Itoa(uid)
|
|
label := menubarLaunchLabel
|
|
// Modern path first (macOS 10.13+); fall back to load -w.
|
|
_ = exec.Command("launchctl", "bootout", domain+"/"+label).Run()
|
|
_ = exec.Command("launchctl", "unload", plistPath).Run()
|
|
if err := exec.Command("launchctl", "bootstrap", domain, plistPath).Run(); err == nil {
|
|
_ = exec.Command("launchctl", "enable", domain+"/"+label).Run()
|
|
_ = exec.Command("launchctl", "kickstart", "-k", domain+"/"+label).Run()
|
|
return nil
|
|
}
|
|
if err := exec.Command("launchctl", "load", "-w", plistPath).Run(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func launchctlUnloadMenubar(plistPath string) error {
|
|
uid := os.Getuid()
|
|
domain := "gui/" + strconv.Itoa(uid)
|
|
label := menubarLaunchLabel
|
|
_ = exec.Command("launchctl", "bootout", domain+"/"+label).Run()
|
|
_ = exec.Command("launchctl", "unload", "-w", plistPath).Run()
|
|
return nil
|
|
}
|
|
|
|
func locateDarwinMenubarScript() (string, error) {
|
|
// Prefer durable install copy if present.
|
|
if home, err := os.UserHomeDir(); err == nil {
|
|
p := filepath.Join(menubarSupportDir(home), "run-menubar.sh")
|
|
if fileExists(p) {
|
|
return p, nil
|
|
}
|
|
}
|
|
script, err := findRepoFile("platform", "macos", "menubar", "run-menubar.sh")
|
|
if err == nil {
|
|
return script, nil
|
|
}
|
|
for _, alt := range []string{
|
|
filepath.Join(exeDirHint(), "menubar", "run-menubar.sh"),
|
|
filepath.Join(exeDirHint(), "..", "menubar", "run-menubar.sh"),
|
|
filepath.Join(exeDirHint(), "run-menubar.sh"),
|
|
} {
|
|
if fileExists(alt) {
|
|
abs, _ := filepath.Abs(alt)
|
|
return abs, nil
|
|
}
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
// replaceEnvString replaces the <string> value immediately following <key>NAME</key>.
|
|
func replaceEnvString(plist, key, value string) string {
|
|
needle := "<key>" + key + "</key>"
|
|
i := strings.Index(plist, needle)
|
|
if i < 0 {
|
|
return plist
|
|
}
|
|
rest := plist[i+len(needle):]
|
|
start := strings.Index(rest, "<string>")
|
|
end := strings.Index(rest, "</string>")
|
|
if start < 0 || end < 0 || end < start {
|
|
return plist
|
|
}
|
|
return plist[:i+len(needle)] + rest[:start] + "<string>" + value + rest[end:]
|
|
}
|
|
|
|
func copyFile(src, dst string) error {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
tmp := dst + ".tmp"
|
|
out, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, copyErr := io.Copy(out, in)
|
|
closeErr := out.Close()
|
|
if copyErr != nil {
|
|
_ = os.Remove(tmp)
|
|
return copyErr
|
|
}
|
|
if closeErr != nil {
|
|
_ = os.Remove(tmp)
|
|
return closeErr
|
|
}
|
|
if st, err := os.Stat(src); err == nil {
|
|
_ = os.Chmod(tmp, st.Mode())
|
|
}
|
|
return os.Rename(tmp, dst)
|
|
}
|
|
|
|
func findRepoFile(parts ...string) (string, error) {
|
|
rel := filepath.Join(parts...)
|
|
// Prefer go.mod tree (repoRootHint) over bare relative/exe guesses so a
|
|
// sibling research checkout cannot shadow the Verae-Projects package tree.
|
|
candidates := []string{
|
|
filepath.Join(repoRootHint(), rel),
|
|
filepath.Join(".", rel),
|
|
filepath.Join(exeDirHint(), rel),
|
|
filepath.Join(exeDirHint(), "..", rel),
|
|
filepath.Join(exeDirHint(), "..", "..", rel),
|
|
}
|
|
// Walk up from cwd for go.mod (tests + nested cwd).
|
|
if wd, err := os.Getwd(); err == nil {
|
|
dir := wd
|
|
for i := 0; i < 8; i++ {
|
|
if fileExists(filepath.Join(dir, "go.mod")) {
|
|
candidates = append(candidates, filepath.Join(dir, rel))
|
|
break
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
break
|
|
}
|
|
dir = parent
|
|
}
|
|
}
|
|
var tried []string
|
|
seen := map[string]bool{}
|
|
for _, c := range candidates {
|
|
abs, err := filepath.Abs(c)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if seen[abs] {
|
|
continue
|
|
}
|
|
seen[abs] = true
|
|
tried = append(tried, abs)
|
|
if fileExists(abs) {
|
|
return abs, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("not found: %s (tried %s)", rel, strings.Join(tried, ", "))
|
|
}
|
|
|
|
func exeDirHint() string {
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return "."
|
|
}
|
|
exe, _ = filepath.EvalSymlinks(exe)
|
|
return filepath.Dir(exe)
|
|
}
|