/
cli.go
208 lines (181 loc) · 4.49 KB
/
cli.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
package cli
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"runtime"
"strings"
"time"
"github.com/blang/semver"
"github.com/exercism/cli/debug"
update "github.com/inconshreveable/go-update"
)
var (
// BuildOS is the operating system (GOOS) used during the build process.
BuildOS string
// BuildARM is the ARM version (GOARM) used during the build process.
BuildARM string
// BuildARCH is the architecture (GOARCH) used during the build process.
BuildARCH string
)
var (
osMap = map[string]string{
"darwin": "mac",
"linux": "linux",
"windows": "windows",
}
archMap = map[string]string{
"amd64": "64bit",
"386": "32bit",
"arm": "arm",
}
)
var (
// TimeoutInSeconds is the timeout the default HTTP client will use.
TimeoutInSeconds = 60
// HTTPClient is the client used to make HTTP calls in the cli package.
HTTPClient = &http.Client{Timeout: time.Duration(TimeoutInSeconds) * time.Second}
// ReleaseURL is the endpoint that provides information about cli releases.
ReleaseURL = "https://api.github.com/repos/exercism/cli/releases"
)
// Updater is a simple upgradable file interface.
type Updater interface {
IsUpToDate() (bool, error)
Upgrade() error
}
// CLI is information about the CLI itself.
type CLI struct {
Version string
LatestRelease *Release
}
// New creates a CLI, setting it to a particular version.
func New(version string) *CLI {
return &CLI{
Version: version,
}
}
// IsUpToDate compares the current version to that of the latest release.
func (c *CLI) IsUpToDate() (bool, error) {
if c.LatestRelease == nil {
if err := c.fetchLatestRelease(); err != nil {
return false, err
}
}
rv, err := semver.Make(c.LatestRelease.Version())
if err != nil {
return false, fmt.Errorf("unable to parse latest version (%s): %s", c.LatestRelease.Version(), err)
}
cv, err := semver.Make(c.Version)
if err != nil {
return false, fmt.Errorf("unable to parse current version (%s): %s", c.Version, err)
}
return cv.GTE(rv), nil
}
// Upgrade allows the user to upgrade to the latest version of the CLI.
func (c *CLI) Upgrade() error {
var (
OS = osMap[runtime.GOOS]
ARCH = archMap[runtime.GOARCH]
)
if OS == "" || ARCH == "" {
return fmt.Errorf("unable to upgrade: OS %s ARCH %s", OS, ARCH)
}
buildName := fmt.Sprintf("%s-%s", OS, ARCH)
if BuildARCH == "arm" {
if BuildARM == "" {
return fmt.Errorf("unable to upgrade: arm version not found")
}
buildName = fmt.Sprintf("%s-v%s", buildName, BuildARM)
}
var downloadRC *bytes.Reader
for _, a := range c.LatestRelease.Assets {
if strings.Contains(a.Name, buildName) {
debug.Printf("Downloading %s\n", a.Name)
var err error
downloadRC, err = a.download()
if err != nil {
return fmt.Errorf("error downloading executable: %s", err)
}
break
}
}
if downloadRC == nil {
return fmt.Errorf("no executable found for %s/%s%s", BuildOS, BuildARCH, BuildARM)
}
bin, err := extractBinary(downloadRC, OS)
if err != nil {
return err
}
defer bin.Close()
return update.Apply(bin, update.Options{})
}
func (c *CLI) fetchLatestRelease() error {
latestReleaseURL := fmt.Sprintf("%s/%s", ReleaseURL, "latest")
resp, err := HTTPClient.Get(latestReleaseURL)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode > 399 {
msg := "failed to get the latest release\n"
for k, v := range resp.Header {
msg += fmt.Sprintf("\n %s:\n %s", k, v)
}
return fmt.Errorf(msg)
}
var rel Release
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
return err
}
c.LatestRelease = &rel
return nil
}
func extractBinary(source *bytes.Reader, os string) (binary io.ReadCloser, err error) {
if os == "windows" {
zr, err := zip.NewReader(source, int64(source.Len()))
if err != nil {
return nil, err
}
for _, f := range zr.File {
info := f.FileInfo()
if info.IsDir() || !strings.HasSuffix(f.Name, ".exe") {
continue
}
return f.Open()
}
} else {
gr, err := gzip.NewReader(source)
if err != nil {
return nil, err
}
defer gr.Close()
tr := tar.NewReader(gr)
for {
_, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
tmpfile, err := ioutil.TempFile("", "temp-exercism")
if err != nil {
return nil, err
}
if _, err = io.Copy(tmpfile, tr); err != nil {
return nil, err
}
if _, err := tmpfile.Seek(0, 0); err != nil {
return nil, err
}
binary = tmpfile
}
}
return binary, nil
}