Skip to content

Commit

Permalink
Upgrade improvements (#278)
Browse files Browse the repository at this point in the history
Upgrade improvements
  • Loading branch information
twpayne committed May 3, 2019
2 parents 467a1d6 + c86e2ba commit 696c2f6
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 34 deletions.
132 changes: 98 additions & 34 deletions cmd/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@ package cmd

import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"syscall"
Expand Down Expand Up @@ -59,6 +64,8 @@ var (
"arm64": "aarch64",
},
}

checksumRegexp = regexp.MustCompile(`\A([0-9a-f]{64})\s+(\S+)\z`)
)

var upgradeCmd = &cobra.Command{
Expand Down Expand Up @@ -155,44 +162,81 @@ func (c *Config) runUpgradeCmd(fs vfs.FS, args []string) error {
return syscall.Exec(executableFilename, []string{executableFilename, "--version"}, os.Environ())
}

func (c *Config) replaceExecutable(mutator chezmoi.Mutator, executableFilename string, releaseVersion *semver.Version, rr *github.RepositoryRelease) error {
// Find the corresponding release asset.
releaseAssetName := fmt.Sprintf("%s_%s_%s_%s.tar.gz", c.upgrade.repo, releaseVersion, runtime.GOOS, runtime.GOARCH)
var releaseAsset *github.ReleaseAsset
for _, ra := range rr.Assets {
if ra.GetName() == releaseAssetName {
releaseAsset = &ra
break
func (c *Config) getChecksums(rr *github.RepositoryRelease) (map[string][]byte, error) {
name := "checksums.txt"
releaseAsset := getReleaseAssetByName(rr, name)
if releaseAsset == nil {
return nil, fmt.Errorf("%s: cannot find release asset", name)
}

data, err := c.downloadURL(releaseAsset.GetBrowserDownloadURL())
if err != nil {
return nil, err
}

checksums := make(map[string][]byte)
s := bufio.NewScanner(bytes.NewReader(data))
for s.Scan() {
m := checksumRegexp.FindStringSubmatch(s.Text())
if m == nil {
return nil, fmt.Errorf("%q: cannot parse checksum", s.Text())
}
checksums[m[2]], _ = hex.DecodeString(m[1])
}
return checksums, s.Err()
}

func (c *Config) downloadURL(url string) ([]byte, error) {
if c.Verbose {
fmt.Fprintf(c.Stdout(), "curl -s -L %s\n", url)
}
resp, err := http.Get(url)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
_ = resp.Body.Close()
return nil, fmt.Errorf("%s: got a non-200 OK response: %d %s", url, resp.StatusCode, resp.Status)
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err := resp.Body.Close(); err != nil {
return nil, err
}
return data, nil
}

func (c *Config) replaceExecutable(mutator chezmoi.Mutator, executableFilename string, releaseVersion *semver.Version, rr *github.RepositoryRelease) error {
name := fmt.Sprintf("%s_%s_%s_%s.tar.gz", c.upgrade.repo, releaseVersion, runtime.GOOS, runtime.GOARCH)
releaseAsset := getReleaseAssetByName(rr, name)
if releaseAsset == nil {
return fmt.Errorf("%s: cannot find release asset", releaseAssetName)
return fmt.Errorf("%s: cannot find release asset", name)
}

// Download the asset.
resp, err := http.Get(releaseAsset.GetBrowserDownloadURL())
data, err := c.downloadURL(releaseAsset.GetBrowserDownloadURL())
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("%s: got a non-200 OK response: %d %s", releaseAsset.GetBrowserDownloadURL(), resp.StatusCode, resp.Status)
if err := c.verifyChecksum(rr, releaseAsset.GetName(), data); err != nil {
return err
}

// Extract the executable from the archive.
gzipr, err := gzip.NewReader(resp.Body)
gzipr, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return err
}
defer gzipr.Close()
tr := tar.NewReader(gzipr)
var data []byte
var executableData []byte
FOR:
for {
h, err := tr.Next()
switch {
case err == nil && h.Name == c.upgrade.repo:
data, err = ioutil.ReadAll(tr)
executableData, err = ioutil.ReadAll(tr)
if err != nil {
return err
}
Expand All @@ -202,8 +246,7 @@ FOR:
}
}

// Replace the executable.
return mutator.WriteFile(executableFilename, data, 0755, nil)
return mutator.WriteFile(executableFilename, executableData, 0755, nil)
}

func (c *Config) upgradePackage(fs vfs.FS, mutator chezmoi.Mutator, rr *github.RepositoryRelease, useSudo bool) error {
Expand Down Expand Up @@ -251,28 +294,20 @@ func (c *Config) upgradePackage(fs vfs.FS, mutator chezmoi.Mutator, rr *github.R
}()
}

// Download the package.
packageFilename := filepath.Join(tempDir, releaseAsset.GetName())
if c.Verbose {
fmt.Fprintf(c.Stdout(), "curl -o %s -s -L %s\n", packageFilename, releaseAsset.GetBrowserDownloadURL())
}
resp, err := http.Get(releaseAsset.GetBrowserDownloadURL())
data, err := c.downloadURL(releaseAsset.GetBrowserDownloadURL())
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("%s: got a non-200 OK response: %d %s", releaseAsset.GetBrowserDownloadURL(), resp.StatusCode, resp.Status)
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
if err := c.verifyChecksum(rr, releaseAsset.GetName(), data); err != nil {
return err
}

packageFilename := filepath.Join(tempDir, releaseAsset.GetName())
if err := mutator.WriteFile(packageFilename, data, 0644, nil); err != nil {
return err
}

// Install the package.
// Install the package from disk.
var args []string
if useSudo {
args = append(args, "sudo")
Expand All @@ -289,6 +324,22 @@ func (c *Config) upgradePackage(fs vfs.FS, mutator chezmoi.Mutator, rr *github.R
}
}

func (c *Config) verifyChecksum(rr *github.RepositoryRelease, name string, data []byte) error {
checksums, err := c.getChecksums(rr)
if err != nil {
return err
}
expectedChecksum, ok := checksums[name]
if !ok {
return fmt.Errorf("%s: checksum not found", name)
}
checksum := sha256.Sum256(data)
if !bytes.Equal(checksum[:], expectedChecksum) {
return fmt.Errorf("%s: checksum failed (want %s, got %s)", name, hex.EncodeToString(expectedChecksum), hex.EncodeToString(checksum[:]))
}
return nil
}

func getMethod(fs vfs.FS, executableFilename string) (string, error) {
info, err := fs.Stat(executableFilename)
if err != nil {
Expand All @@ -302,14 +353,18 @@ func getMethod(fs vfs.FS, executableFilename string) (string, error) {
if err != nil {
return "", err
}
executableIsInTempDir, err := vfs.Contains(fs, executableFilename, os.TempDir())
if err != nil {
return "", err
}
executableStat := info.Sys().(*syscall.Stat_t)
uid := os.Getuid()
switch runtime.GOOS {
case "darwin":
if int(executableStat.Uid) != uid {
return "", fmt.Errorf("%s: cannot upgrade executable owned by non-current user", executableFilename)
}
if executableInUserHomeDir {
if executableInUserHomeDir || executableIsInTempDir {
return methodReplaceExecutable, nil
}
return methodUpgradePackage, nil
Expand All @@ -320,7 +375,7 @@ func getMethod(fs vfs.FS, executableFilename string) (string, error) {
if executableStat.Uid != 0 {
return "", fmt.Errorf("%s: cannot upgrade executable owned by non-root user when running as root", executableFilename)
}
if executableInUserHomeDir {
if executableInUserHomeDir || executableIsInTempDir {
return methodReplaceExecutable, nil
}
return methodUpgradePackage, nil
Expand Down Expand Up @@ -363,3 +418,12 @@ func getPackageType(fs vfs.FS) (string, error) {
}
return packageTypeNone, fmt.Errorf("could not determine package type (ID=%q, ID_LIKE=%q)", osRelease["ID"], osRelease["ID_LIKE"])
}

func getReleaseAssetByName(rr *github.RepositoryRelease, name string) *github.ReleaseAsset {
for _, ra := range rr.Assets {
if ra.GetName() == name {
return &ra
}
}
return nil
}
8 changes: 8 additions & 0 deletions docs/INSTALL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Installation

One line install:

curl -sfL https://install.goreleaser.com/github.com/twpayne/chezmoi.sh | sh

Pre-built packages and binaries:

| OS | Architectures | Package location |
Expand All @@ -19,3 +23,7 @@ On macOS you can install chezmoi with Homebrew:
If you have Go installed you can install the latest version from `HEAD`:

go get -u github.com/twpayne/chezmoi

Once chezmoi is installed, you can upgrade it to the latest release with:

chezmoi upgrade

0 comments on commit 696c2f6

Please sign in to comment.