initial commit [141.14.140.180,mike]
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user