268 lines
6.7 KiB
Go
268 lines
6.7 KiB
Go
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
|
|
}
|