Skip to content

Commit bf968bb

Browse files
authored
Add DownloadToGopathBin for archived files (#5)
Add DownloadToGopathBin for archived files 🚨 THIS HAS BREAKING CHANGES! * Move GOPATH related functions to github.com/carolynvs/magex/gopath * Add package github.com/carolynvs/magex/archive to handle downloading an archive to your GOPATH/bin directory. It is in it's own package so that you don't have to add the dependencies to support archives unless you are using it.
1 parent eaf835b commit bf968bb

File tree

12 files changed

+440
-140
lines changed

12 files changed

+440
-140
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.15
44

55
require (
66
github.com/magefile/mage v1.11.0
7+
github.com/mholt/archiver/v3 v3.5.0
78
github.com/pkg/errors v0.9.1
89
github.com/stretchr/testify v1.6.1
910
)

go.sum

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,38 @@
1+
github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4=
2+
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
13
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
24
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
6+
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
7+
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
8+
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
9+
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
10+
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
11+
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
12+
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
13+
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
14+
github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A=
15+
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
316
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
417
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
18+
github.com/mholt/archiver/v3 v3.5.0 h1:nE8gZIrw66cu4osS/U7UW7YDuGMHssxKutU8IfWxwWE=
19+
github.com/mholt/archiver/v3 v3.5.0/go.mod h1:qqTTPUK/HZPFgFQ/TJ3BzvTpF/dPtFVJXdQbCmeMxwc=
20+
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
21+
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
22+
github.com/pierrec/lz4/v4 v4.0.3 h1:vNQKSVZNYUEAvRY9FaUXAF1XPbSOHJtDTiP41kzDz2E=
23+
github.com/pierrec/lz4/v4 v4.0.3/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
524
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
625
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
726
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
827
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
928
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
1029
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
1130
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
31+
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
32+
github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4=
33+
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
34+
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
35+
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
1236
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1337
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1438
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=

pkg/archive/doc.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Helper methods for working with archived/compressed files.
2+
//
3+
// These functions are separated into their own package to
4+
// limit the dependencies pulled in when using magex if you
5+
// are not using archived files.
6+
package archive

pkg/archive/install.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package archive
2+
3+
import (
4+
"log"
5+
"os"
6+
"path/filepath"
7+
"runtime"
8+
9+
"github.com/carolynvs/magex/pkg/downloads"
10+
"github.com/carolynvs/magex/xplat"
11+
"github.com/mholt/archiver/v3"
12+
_ "github.com/mholt/archiver/v3"
13+
"github.com/pkg/errors"
14+
)
15+
16+
// DownloadArchiveOptions are the set of options available for DownloadToGopathBin.
17+
type DownloadArchiveOptions struct {
18+
downloads.DownloadOptions
19+
20+
// ArchiveExtensions maps from the GOOS to the expected extension. Required.
21+
// For example, windows may use .zip while darwin/linux uses .tgz.
22+
ArchiveExtensions map[string]string
23+
24+
// TargetFileTemplate specifies the path to the target binary in the archive. Required.
25+
// Supports the same templating as downloads.DownloadOptions.UrlTemplate.
26+
TargetFileTemplate string
27+
}
28+
29+
// DownloadToGopathBin downloads an archived file to GOPATH/bin.
30+
func DownloadToGopathBin(opts DownloadArchiveOptions) error {
31+
// determine the appropriate file extension based on the OS, e.g. windows gets .zip, otherwise .tgz
32+
opts.Ext = opts.ArchiveExtensions[runtime.GOOS]
33+
if opts.Ext == "" {
34+
return errors.Errorf("no archive file extension was specified for the current GOOS (%s)", runtime.GOOS)
35+
}
36+
37+
if opts.Hook == nil {
38+
opts.Hook = ExtractBinaryFromArchiveHook(opts)
39+
}
40+
41+
return downloads.DownloadToGopathBin(opts.DownloadOptions)
42+
}
43+
44+
// ExtractBinaryFromArchiveHook is the default hook for DownloadToGopathBin.
45+
func ExtractBinaryFromArchiveHook(opts DownloadArchiveOptions) downloads.PostDownloadHook {
46+
return func(archiveFile string) (binPath string, err error) {
47+
// Save the binary next to the archive file in the temp directory
48+
outDir := filepath.Dir(archiveFile)
49+
50+
// Render the name of the file in the archive
51+
opts.Ext = xplat.FileExt()
52+
targetFile, err := downloads.RenderTemplate(opts.TargetFileTemplate, opts.DownloadOptions)
53+
if err != nil {
54+
return "", errors.Wrapf(err, "error rendering TargetFileTemplate")
55+
}
56+
57+
log.Printf("extracting %s from %s...\n", targetFile, archiveFile)
58+
59+
// Extract the binary
60+
err = archiver.Extract(archiveFile, targetFile, outDir)
61+
if err != nil {
62+
return "", errors.Wrapf(err, "unable to unpack %s", archiveFile)
63+
}
64+
65+
// The extracted file may be nested depending on its position in the archive
66+
binFile := filepath.Join(outDir, targetFile)
67+
68+
// Check that file was extracted, Extract doesn't error out if you give it a missing targetFile
69+
if _, err := os.Stat(binFile); os.IsNotExist(err) {
70+
return "", errors.Errorf("could not find %s in the archive", targetFile)
71+
}
72+
73+
return binFile, nil
74+
}
75+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package archive_test
2+
3+
import (
4+
"log"
5+
6+
"github.com/carolynvs/magex/pkg/archive"
7+
"github.com/carolynvs/magex/pkg/downloads"
8+
"github.com/carolynvs/magex/pkg/gopath"
9+
)
10+
11+
func ExampleDownloadToGopathBin() {
12+
opts := archive.DownloadArchiveOptions{
13+
DownloadOptions: downloads.DownloadOptions{
14+
UrlTemplate: "https://get.helm.sh/helm-{{.VERSION}}-{{.GOOS}}-{{.GOARCH}}{{.EXT}}",
15+
Name: "helm",
16+
Version: "v3.5.3",
17+
},
18+
ArchiveExtensions: map[string]string{
19+
"darwin": ".tar.gz",
20+
"linux": ".tar.gz",
21+
"windows": ".zip",
22+
},
23+
TargetFileTemplate: "{{.GOOS}}-{{.GOARCH}}/helm{{.EXT}}",
24+
}
25+
err := archive.DownloadToGopathBin(opts)
26+
if err != nil {
27+
log.Fatal("could not download helm")
28+
}
29+
30+
// Add GOPATH/bin to PATH if necessary so that we can immediately
31+
// use the installed tool
32+
gopath.EnsureGopathBin()
33+
}

pkg/archive/install_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package archive
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"runtime"
7+
"testing"
8+
9+
"github.com/carolynvs/magex/pkg/downloads"
10+
"github.com/carolynvs/magex/pkg/gopath"
11+
"github.com/carolynvs/magex/xplat"
12+
"github.com/magefile/mage/mg"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestDownloadArchiveToGopathBin(t *testing.T) {
17+
os.Setenv(mg.VerboseEnv, "true")
18+
err, cleanup := gopath.UseTempGopath()
19+
require.NoError(t, err, "Failed to set up a temporary GOPATH")
20+
defer cleanup()
21+
22+
// gh cli unfortunately uses a different archive schema depending on the OS
23+
tmpl := "gh_{{.VERSION}}_{{.GOOS}}_{{.GOARCH}}/bin/gh{{.EXT}}"
24+
if runtime.GOOS == "windows" {
25+
tmpl = "bin/gh.exe"
26+
}
27+
28+
opts := DownloadArchiveOptions{
29+
DownloadOptions: downloads.DownloadOptions{
30+
UrlTemplate: "https://github.com/cli/cli/releases/download/v{{.VERSION}}/gh_{{.VERSION}}_{{.GOOS}}_{{.GOARCH}}{{.EXT}}",
31+
Name: "gh",
32+
Version: "1.8.1",
33+
OsReplacement: map[string]string{
34+
"darwin": "macOS",
35+
},
36+
},
37+
ArchiveExtensions: map[string]string{
38+
"linux": ".tar.gz",
39+
"darwin": ".tar.gz",
40+
"windows": ".zip",
41+
},
42+
TargetFileTemplate: tmpl,
43+
}
44+
45+
err = DownloadToGopathBin(opts)
46+
require.NoError(t, err)
47+
48+
_, err = exec.LookPath("gh" + xplat.FileExt())
49+
require.NoError(t, err)
50+
}

pkg/build.go

Lines changed: 0 additions & 15 deletions
This file was deleted.

pkg/downloads/download.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package downloads
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"io/ioutil"
7+
"log"
8+
"net/http"
9+
"os"
10+
"path/filepath"
11+
"runtime"
12+
"text/template"
13+
14+
"github.com/carolynvs/magex/pkg/gopath"
15+
"github.com/carolynvs/magex/shx"
16+
"github.com/carolynvs/magex/xplat"
17+
"github.com/pkg/errors"
18+
)
19+
20+
// PostDownloadHook is the handler called after downloading a file, which returns the absolute path to the binary.
21+
type PostDownloadHook func(archivePath string) (string, error)
22+
23+
// DownloadOptions
24+
type DownloadOptions struct {
25+
// UrlTemplate is the Go template for the URL to download. Required.
26+
// Available Template Variables:
27+
// - {{.GOOS}}
28+
// - {{.GOARCH}}
29+
// - {{.EXT}}
30+
// - {{.VERSION}}
31+
UrlTemplate string
32+
33+
// Name of the binary, excluding OS specific file extension. Required.
34+
Name string
35+
36+
// Version to replace {{.VERSION}} in the URL template. Optional depending on whether or not the version is in the UrlTemplate.
37+
Version string
38+
39+
// Ext to replace {{.EXT}} in the URL template. Optional, defaults to xplat.FileExt().
40+
Ext string
41+
42+
// OsReplacement maps from a GOOS to the os keyword used for the download. Optional, defaults to empty.
43+
OsReplacement map[string]string
44+
45+
// ArchReplacement maps from a GOARCH to the arch keyword used for the download. Optional, defaults to empty.
46+
ArchReplacement map[string]string
47+
48+
// Hook to call after downloading the file.
49+
Hook PostDownloadHook
50+
}
51+
52+
// DownloadToGopathBin takes a Go templated URL and expands template variables
53+
// - srcTemplate is the URL
54+
// - version is the version to substitute into the template
55+
// - ext is the file extension to substitute into the template
56+
//
57+
// Template Variables:
58+
// - {{.GOOS}}
59+
// - {{.GOARCH}}
60+
// - {{.EXT}}
61+
// - {{.VERSION}}
62+
func DownloadToGopathBin(opts DownloadOptions) error {
63+
src, err := RenderTemplate(opts.UrlTemplate, opts)
64+
if err != nil {
65+
return err
66+
}
67+
log.Printf("Downloading %s...", src)
68+
69+
err = gopath.EnsureGopathBin()
70+
if err != nil {
71+
return err
72+
}
73+
74+
// Download to a temp file
75+
tmpDir, err := ioutil.TempDir("", "magex")
76+
if err != nil {
77+
return errors.Wrap(err, "could not create temporary directory")
78+
}
79+
defer os.RemoveAll(tmpDir)
80+
tmpFile := filepath.Join(tmpDir, filepath.Base(src))
81+
82+
r, err := http.Get(src)
83+
if err != nil {
84+
return errors.Wrapf(err, "could not resolve %s", src)
85+
}
86+
defer r.Body.Close()
87+
88+
f, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0755)
89+
if err != nil {
90+
return errors.Wrapf(err, "could not open %s", tmpFile)
91+
}
92+
defer f.Close()
93+
94+
// Download to the temp file
95+
_, err = io.Copy(f, r.Body)
96+
if err != nil {
97+
errors.Wrapf(err, "error downloading %s", src)
98+
}
99+
f.Close()
100+
101+
// Call a hook to allow for extracting or modifying the downloaded file
102+
var tmpBin = tmpFile
103+
if opts.Hook != nil {
104+
tmpBin, err = opts.Hook(tmpFile)
105+
if err != nil {
106+
return err
107+
}
108+
}
109+
110+
// Make the binary executable
111+
err = os.Chmod(tmpBin, 0755)
112+
if err != nil {
113+
return errors.Wrapf(err, "could not make %s executable", tmpBin)
114+
}
115+
116+
// Move it to GOPATH/bin
117+
dest := filepath.Join(gopath.GetGopathBin(), opts.Name+xplat.FileExt())
118+
err = shx.Copy(tmpBin, dest)
119+
return errors.Wrapf(err, "error copying %s to %s", tmpBin, dest)
120+
}
121+
122+
// RenderTemplate takes a Go templated string and expands template variables
123+
// Available Template Variables:
124+
// - {{.GOOS}}
125+
// - {{.GOARCH}}
126+
// - {{.EXT}}
127+
// - {{.VERSION}}
128+
func RenderTemplate(tmplContents string, opts DownloadOptions) (string, error) {
129+
tmpl, err := template.New("url").Parse(tmplContents)
130+
if err != nil {
131+
return "", errors.Wrapf(err, "error parsing %s as a Go template", opts.UrlTemplate)
132+
}
133+
134+
srcData := struct {
135+
GOOS string
136+
GOARCH string
137+
EXT string
138+
VERSION string
139+
}{
140+
GOOS: runtime.GOOS,
141+
GOARCH: runtime.GOARCH,
142+
EXT: opts.Ext,
143+
VERSION: opts.Version,
144+
}
145+
146+
if overrideGoos, ok := opts.OsReplacement[runtime.GOOS]; ok {
147+
srcData.GOOS = overrideGoos
148+
}
149+
150+
if overrideGoarch, ok := opts.ArchReplacement[runtime.GOARCH]; ok {
151+
srcData.GOARCH = overrideGoarch
152+
}
153+
154+
buf := &bytes.Buffer{}
155+
err = tmpl.Execute(buf, srcData)
156+
if err != nil {
157+
return "", errors.Wrapf(err, "error rendering %s as a Go template with data: %#v", opts.UrlTemplate, srcData)
158+
}
159+
160+
return buf.String(), nil
161+
}

0 commit comments

Comments
 (0)