forked from hashicorp/otto
/
project.go
171 lines (145 loc) · 4.38 KB
/
project.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
package hashitools
import (
"bytes"
"fmt"
"io"
"log"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/hashicorp/go-checkpoint"
"github.com/hashicorp/go-version"
)
//go:generate go-bindata -pkg=hashitools -nomemcopy -nometadata ./data/...
var (
versionRe = regexp.MustCompile(`v?(\d+\.\d+\.[^\s]+)`)
)
// Project represents a HashiCorp Go project and provides various operations
// around that.
type Project struct {
// Name is the name of the project, all lowercase
Name string
// Installer is the installer for this project
Installer Installer
// MinVersion is the minimum version of this project that Otto
// can use to function. This will be used with `InstallIfNeeded`
// to prompt the user to install.
MinVersion *version.Version
}
// InstallIfNeeded will check if installation of this project is required
// and will invoke the installer if needed.
func (p *Project) InstallIfNeeded() error {
log.Printf("[DEBUG] installIfNeeded: %s", p.Name)
// Start grabbing the latest version as early as possible since
// this requires a network call. We might as well do it while we're
// doing a subprocess.
latestCh := make(chan *version.Version, 1)
errCh := make(chan error, 1)
go func() {
latest, err := p.LatestVersion()
if err != nil {
errCh <- err
}
latestCh <- latest
}()
// Grab the version we have installed
installed, err := p.Version()
if err != nil {
return err
}
if installed == nil {
log.Printf("[DEBUG] installIfNeeded: %s not installed", p.Name)
} else {
log.Printf("[DEBUG] installIfNeeded: %s installed: %s", p.Name, installed)
}
// Wait for the latest
var latest *version.Version
select {
case latest = <-latestCh:
case err := <-errCh:
log.Printf("[ERROR] error checking latest version: %s", err)
}
log.Printf("[DEBUG] installIfNeeded: %s latest: %s", p.Name, latest)
log.Printf("[DEBUG] installIfNeeded: %s min: %s", p.Name, p.MinVersion)
// Determine if we require an install
installRequired := installed == nil
if installed != nil {
if installed.LessThan(p.MinVersion) {
installRequired = true
}
// TODO: updates
}
// No install required? Exit out.
if !installRequired {
log.Printf("[DEBUG] installIfNeeded: %s no installation needed", p.Name)
return nil
}
// We need to install! Ask the installer to verify for us
ok, err := p.Installer.InstallAsk(installed, p.MinVersion, latest)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("Installation cancelled")
}
// Install
return p.Installer.Install(latest)
}
// Latest version returns the latest version of this project.
func (p *Project) LatestVersion() (*version.Version, error) {
check, err := checkpoint.Check(&checkpoint.CheckParams{
Product: p.Name,
Force: true,
})
if err != nil {
return nil, err
}
return version.NewVersion(check.CurrentVersion)
}
// Version reads the version of this project.
func (p *Project) Version() (*version.Version, error) {
path := p.Path()
if !filepath.IsAbs(path) {
// Look for it on the path first if we don't have a full path to it
_, err := exec.LookPath(path)
if err != nil {
if execErr, ok := err.(*exec.Error); ok && execErr.Err == exec.ErrNotFound {
return nil, nil
}
return nil, err
}
}
// Grab the version
var stdout, buf bytes.Buffer
cmd := exec.Command(path, "--version")
cmd.Stdout = io.MultiWriter(&stdout, &buf)
cmd.Stderr = &buf
runErr := cmd.Run()
// Match the version out before we check for a run error, since some `project
// --version` commands can return a non-zero exit code.
matches := versionRe.FindStringSubmatch(stdout.String())
if len(matches) == 0 {
if runErr != nil {
return nil, fmt.Errorf(
"Error checking %s version: %s\n\n%s",
p.Name, runErr, buf.String())
}
return nil, fmt.Errorf(
"unable to find %s version in output: %q", p.Name, buf.String())
}
// This is a weird quirk: our version lib follows semver strictly
// but Vagrant in particular uses ".dev" instead "-dev" to end the
// version for Rubygems. Fix that up with duct tape.
matches[1] = strings.Replace(matches[1], ".dev", "-dev", 1)
return version.NewVersion(matches[1])
}
// Path returns the path to this project. This will check if the project
// binary is pre-installed in our installation directory and use that path.
// Otherwise, it will return the raw project name.
func (p *Project) Path() string {
if p := p.Installer.Path(); p != "" {
return p
}
return p.Name
}