Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

241 lines
8.0 KiB
Go

package main
import (
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
"golang.org/x/mod/semver"
)
// InstallMethod is how this gortex binary was installed, inferred from where it
// lives on disk. Each method maps to the right self-update command, so
// `gortex upgrade` upgrades the way the user installed rather than clobbering a
// package-manager-owned binary with a raw download.
type InstallMethod string
const (
InstallBrew InstallMethod = "brew" // Homebrew Cellar
InstallScoop InstallMethod = "scoop" // Scoop apps dir (Windows)
InstallGoInstall InstallMethod = "go-install" // $GOPATH/bin or ~/go/bin
InstallScript InstallMethod = "script" // get.gortex.dev installer → ~/.local/bin
InstallUnknown InstallMethod = "unknown" // manual download / packaged elsewhere
)
const upgradeRepoURL = "https://github.com/zzet/gortex"
// detectInstallMethod infers the install method from the binary's path. brew
// and scoop are recognised by their well-known directory anchors; a binary
// under the Go bin dir is a `go install`; one under ~/.local/bin is the
// installer script's target. Everything else is unknown (the user gets the
// release-page fallback). Paths are slash-normalised so the same logic works on
// Windows.
func detectInstallMethod(execPath, goBinDir, homeDir string) InstallMethod {
// Normalise backslashes explicitly rather than via filepath.ToSlash, which
// only converts on Windows — a Windows path can be classified on any OS.
p := strings.ReplaceAll(execPath, "\\", "/")
switch {
case strings.Contains(p, "/Cellar/") || strings.Contains(p, "/homebrew/"):
return InstallBrew
case strings.Contains(p, "/scoop/apps/"):
return InstallScoop
case goBinDir != "" && underDir(p, goBinDir):
return InstallGoInstall
case homeDir != "" && underDir(p, filepath.Join(homeDir, ".local", "bin")):
return InstallScript
default:
return InstallUnknown
}
}
// underDir reports whether slash-path p sits directly under dir.
func underDir(p, dir string) bool {
d := strings.ReplaceAll(dir, "\\", "/")
return p == d || strings.HasPrefix(p, strings.TrimSuffix(d, "/")+"/")
}
// upgradeInstructions returns the command that updates gortex for the detected
// install method, honouring a version pin where the method supports it, and
// whether the upgrade is a manual step (no scripted command). cosign + SHA256
// verification is preserved: brew/scoop/the installer script all verify, and
// `go install` builds from the verified module proxy.
func upgradeInstructions(m InstallMethod, pinVersion string) (command string, manual bool) {
switch m {
case InstallBrew:
return "brew upgrade gortex", false
case InstallScoop:
return "scoop update gortex", false
case InstallGoInstall:
v := pinVersion
if v == "" {
v = "latest"
}
return "go install " + upgradeModulePath + "@" + v, false
case InstallScript:
return "curl -fsSL https://get.gortex.dev | sh", false
default:
return "", true
}
}
const upgradeModulePath = "github.com/zzet/gortex/cmd/gortex"
// tagFromReleaseLocation extracts the version tag from a GitHub
// /releases/latest redirect Location (…/releases/tag/v0.49.0 → v0.49.0).
// Returns "" when the location is not a tag URL.
func tagFromReleaseLocation(location string) string {
const marker = "/releases/tag/"
i := strings.Index(location, marker)
if i < 0 {
return ""
}
tag := location[i+len(marker):]
if j := strings.IndexAny(tag, "?#"); j >= 0 {
tag = tag[:j]
}
return strings.TrimSpace(tag)
}
// latestReleaseVersion resolves the newest release tag via the
// /releases/latest redirect — the redirect target carries the tag, so we read
// it without the 60-request/hour unauthenticated API limit.
func latestReleaseVersion() (string, error) {
client := &http.Client{
Timeout: 8 * time.Second,
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse // capture the redirect, don't follow it
},
}
resp, err := client.Get(upgradeRepoURL + "/releases/latest")
if err != nil {
return "", err
}
defer resp.Body.Close()
tag := tagFromReleaseLocation(resp.Header.Get("Location"))
if tag == "" {
return "", fmt.Errorf("could not read latest release tag from %s", upgradeRepoURL)
}
return tag, nil
}
// normalizeSemver ensures a leading "v" so golang.org/x/mod/semver accepts it.
func normalizeSemver(v string) string {
v = strings.TrimSpace(v)
if v != "" && !strings.HasPrefix(v, "v") {
return "v" + v
}
return v
}
var upgradeRun bool
var upgradeCmd = &cobra.Command{
Use: "upgrade [version]",
Short: "Update gortex to the latest release using the method it was installed with",
Long: "Detects how this gortex binary was installed (Homebrew, Scoop, go install, or the " +
"installer script) and runs the matching update command. Pass a version (or set " +
"GORTEX_VERSION) to pin a specific release. By default the command is printed; pass --run to " +
"execute it. cosign + SHA256 verification is preserved by every supported method.",
Args: cobra.MaximumNArgs(1),
RunE: runUpgrade,
}
func init() {
upgradeCmd.Flags().BoolVar(&upgradeRun, "run", false, "execute the detected upgrade command instead of printing it")
rootCmd.AddCommand(upgradeCmd)
}
func runUpgrade(cmd *cobra.Command, args []string) error {
out := cmd.OutOrStdout()
pin := os.Getenv("GORTEX_VERSION")
if len(args) == 1 {
pin = args[0]
}
execPath, err := os.Executable()
if err != nil {
execPath = ""
}
method := detectInstallMethod(execPath, goBinDir(), homeDirOrEmpty())
current := normalizeSemver(version)
target := normalizeSemver(pin)
if target == "" {
if latest, lerr := latestReleaseVersion(); lerr == nil {
target = normalizeSemver(latest)
} else {
fmt.Fprintf(out, "could not check the latest version (%v); proceeding with the upgrade command\n", lerr)
}
}
// Already current? Only short-circuit when we actually resolved a target
// and the user did not pin a (possibly older/specific) version.
if pin == "" && semver.IsValid(current) && semver.IsValid(target) && semver.Compare(current, target) >= 0 {
fmt.Fprintf(out, "gortex %s is already the latest release.\n", version)
return nil
}
command, manual := upgradeInstructions(method, pin)
if manual {
fmt.Fprintf(out, "Installed via an unrecognised method — download the latest release from:\n %s/releases/latest\n", upgradeRepoURL)
return nil
}
if target != "" {
fmt.Fprintf(out, "Upgrading gortex %s → %s (install method: %s)\n", version, target, method)
} else {
fmt.Fprintf(out, "Upgrading gortex (install method: %s)\n", method)
}
if !upgradeRun {
fmt.Fprintf(out, "Run:\n %s\n", command)
} else {
parts := strings.Fields(command)
fmt.Fprintf(out, "$ %s\n", command)
ex := exec.CommandContext(cmd.Context(), parts[0], parts[1:]...) //nolint:gosec // command is one of the fixed install-method templates
ex.Stdout, ex.Stderr, ex.Stdin = out, cmd.ErrOrStderr(), os.Stdin
if rerr := ex.Run(); rerr != nil {
return fmt.Errorf("upgrade command failed: %w", rerr)
}
}
// Post-upgrade advisory: a new binary may carry newer per-language
// extractors, so the indexed graph for an affected language is stale until
// reindexed. F2 enriches this with the exact stale languages per repo.
fmt.Fprintln(out, "\nAfter upgrading, reindex so the graph picks up any extractor changes:\n gortex index .")
return nil
}
// goBinDir returns the directory `go install` drops binaries into — $GOBIN, or
// $GOPATH/bin, or ~/go/bin — for install-method detection.
func goBinDir() string {
if b := strings.TrimSpace(os.Getenv("GOBIN")); b != "" {
return b
}
if gp := strings.TrimSpace(os.Getenv("GOPATH")); gp != "" {
// GOPATH may be a list; the first entry owns `go install` output.
if i := strings.IndexByte(gp, os.PathListSeparator); i >= 0 {
gp = gp[:i]
}
return filepath.Join(gp, "bin")
}
if home := homeDirOrEmpty(); home != "" {
return filepath.Join(home, "go", "bin")
}
return ""
}
func homeDirOrEmpty() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return home
}