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 or --pattern ", 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() { // " ", or just "" 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 }