initial commit [141.14.140.180,mike]

This commit is contained in:
2026-08-14 09:09:59 +02:00
commit 6a991a25a6
19 changed files with 6691 additions and 0 deletions
+267
View File
@@ -0,0 +1,267 @@
package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
)
// updateOne runs one repository end to end.
func updateOne(spec *Spec) (result, error) {
c, err := newForgeCtx(spec)
if err != nil {
return result{}, err
}
// Without --name the repository name is both what to look for and what to
// write; parseNames falls back to it on its own.
names := parseNames(spec.Name, c.repo)
goos, goarch := detectPlatform(spec)
info("Repo: %s/%s/%s [%s]", c.base, c.owner, c.repo, c.forge)
info("Platform: %s/%s", goos, goarch)
rel, list, err := c.fetchRelease(spec)
if err != nil {
return result{}, err
}
if opt.list {
if rel != nil {
list = []Release{*rel}
}
listReleases(list)
return result{status: "listed", label: names[0].out}, nil
}
pre := ""
if rel.Prerelease {
pre = " [prerelease]"
}
info("Release: %s (%s)%s", rel.TagName, rel.PublishedAt, pre)
if len(rel.Assets) == 0 {
return result{}, fmt.Errorf("release %s has no assets", rel.TagName)
}
asset, err := pickAsset(rel.Assets, names[0].src, goos, goarch, spec)
if err != nil {
return result{}, err
}
if asset == nil {
var have []string
for _, a := range rel.Assets {
have = append(have, a.Name)
}
return result{}, fmt.Errorf("no asset for %s/%s in release %s.\nAvailable:\n %s\n"+
"Select one explicitly with --asset <name> or --pattern <regex>",
goos, goarch, rel.TagName, strings.Join(have, "\n "))
}
info("Asset: %s (%s)", asset.Name, humanSize(asset.Size))
targets, err := resolveTargets(spec, names, goos)
if err != nil {
return result{}, err
}
var dests []string
for _, t := range targets {
dests = append(dests, t.dest)
}
info("Target: %s", strings.Join(dests, ", "))
stale := targets[:0:0]
for _, t := range targets {
if !targetCurrent(t, rel.TagName, asset, spec) {
stale = append(stale, t)
}
}
if len(stale) == 0 && !opt.force {
info("Already up to date (%s) - nothing to do.", rel.TagName)
return result{status: "current", label: names[0].out, msg: rel.TagName}, nil
}
if opt.force {
stale = targets
}
if opt.check {
var have []string
for _, t := range stale {
have = append(have, installedTag(t))
}
info("Update available: %s (installed: %s)", rel.TagName, strings.Join(have, ", "))
return result{status: "outdated", label: names[0].out,
msg: fmt.Sprintf("%s (have: %s)", rel.TagName, strings.Join(have, ", "))}, nil
}
dlURL, dlHdr := c.assetURL(asset)
if opt.dryRun {
fmt.Printf("[dry-run] would download: %s\n", dlURL)
for _, t := range stale {
fmt.Printf("[dry-run] would install to: %s\n", t.dest)
}
return result{status: "dry", label: names[0].out, msg: rel.TagName}, nil
}
// --- download -------------------------------------------------------------
tmpdir, err := os.MkdirTemp("", "upd-")
if err != nil {
return result{}, err
}
defer os.RemoveAll(tmpdir)
dl := filepath.Join(tmpdir, asset.Name)
info("Downloading %s ...", dlURL)
hdr := c.authHeaders()
for k, v := range dlHdr {
hdr[k] = v
}
if err := downloadTo(dl, dlURL, hdr, asset.Size); err != nil {
return result{}, err
}
if st, err := os.Stat(dl); err == nil && asset.Size > 0 && st.Size() < asset.Size {
return result{}, fmt.Errorf("incomplete download: %d of %d bytes", st.Size(), asset.Size)
}
if err := verifyDownload(c, dl, asset, rel.Assets, tmpdir); err != nil {
return result{}, err
}
// --- extract and install --------------------------------------------------
root, err := extractIfArchive(dl, tmpdir)
if err != nil {
return result{}, err
}
for _, t := range stale {
src := dl
if root != "" {
src = findInTree(root, t.name)
if src == "" {
return result{}, fmt.Errorf("binary %q not found inside %s", t.name, asset.Name)
}
}
if !looksExecutable(src) {
fmt.Fprintf(os.Stderr, "Warning: %s does not look like an executable.\n", t.name)
}
if err := installAtomic(src, t.dest); err != nil {
return result{}, err
}
stripQuarantine(t.dest)
if err := writeState(t, c, rel.TagName, asset); err != nil {
return result{}, err
}
info("Installed: %s (%s)", t.dest, rel.TagName)
if v := installedVersion(t.dest, spec); v != "" {
info("Version: %s", v)
}
}
seen := map[string]bool{}
for _, t := range stale {
dir := filepath.Dir(t.dest)
if !seen[dir] && !inPath(dir) {
fmt.Fprintf(os.Stderr, "Note: %s is not in $PATH.\n", dir)
}
seen[dir] = true
}
return result{status: "updated", label: names[0].out, msg: rel.TagName}, nil
}
func detectPlatform(spec *Spec) (string, string) {
goos, goarch := runtime.GOOS, runtime.GOARCH
if spec.OS != "" {
goos = spec.OS
}
if spec.Arch != "" {
goarch = spec.Arch
}
return goos, goarch
}
// ==============================================================================
// Checksums
// ==============================================================================
var (
sha256Re = regexp.MustCompile(`(?i)^sha256:([0-9a-f]{64})$`)
sumLineRe = regexp.MustCompile(`^([0-9a-fA-F]{64})(\s+\*?(\S+))?`)
sumsFileRe = regexp.MustCompile(`(?i)^(sha256sums?(\.txt)?|checksums?\.txt)$`)
)
func verifyDownload(c *forgeCtx, file string, asset *Asset, assets []Asset, tmpdir string) error {
var want, src string
if m := sha256Re.FindStringSubmatch(asset.Digest); m != nil {
want, src = strings.ToLower(m[1]), "asset digest"
} else {
var sum *Asset
for i := range assets {
if assets[i].Name == asset.Name+".sha256" {
sum = &assets[i]
}
}
if sum == nil {
for i := range assets {
if sumsFileRe.MatchString(assets[i].Name) {
sum = &assets[i]
break
}
}
}
if sum == nil {
return nil // nothing published to check against
}
sf := filepath.Join(tmpdir, "sums")
u, h := c.assetURL(sum)
hdr := c.authHeaders()
for k, v := range h {
hdr[k] = v
}
if err := downloadTo(sf, u, hdr, 0); err != nil {
fmt.Fprintf(os.Stderr, "Checksum file could not be downloaded, skipping verification.\n")
return nil
}
fh, err := os.Open(sf)
if err != nil {
return nil
}
defer fh.Close()
sc := bufio.NewScanner(fh)
for sc.Scan() {
// "<hash> <name>", or just "<hash>" in a single-file .sha256
m := sumLineRe.FindStringSubmatch(sc.Text())
if m == nil {
continue
}
if m[3] != "" && filepath.Base(m[3]) != asset.Name {
continue
}
want, src = strings.ToLower(m[1]), sum.Name
break
}
if want == "" {
// An unreadable or unrelated checksum file is not a failed
// verification, only a missing one.
if err := sc.Err(); err != nil {
verbose("could not read %s: %v", sum.Name, err)
}
return nil
}
}
got := fileSHA256(file)
if got != want {
return fmt.Errorf("SHA256 mismatch (%s)!\n expected: %s\n got: %s", src, want, got)
}
info("SHA256: ok (%s)", src)
return nil
}