forked from bwplotka/bingo
/
get.go
351 lines (305 loc) · 9.61 KB
/
get.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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
// Copyright (c) Bartłomiej Płotka @bwplotka
// Licensed under the Apache License 2.0.
package main
import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/bwplotka/bingo/pkg/bingo"
"github.com/bwplotka/bingo/pkg/gomodcmd"
"github.com/pkg/errors"
)
type getConfig struct {
runner *gomodcmd.Runner
modDir string
relModDir string
update gomodcmd.GetUpdatePolicy
name string
// target name or target package path, optionally with Version(s).
rawTarget string
}
func get(
ctx context.Context,
logger *log.Logger,
c getConfig,
) (err error) {
if c.rawTarget == "" {
// Empty means all.
if c.name != "" {
return errors.New("name cannot by specified if no target was given")
}
modFiles, err := bingoModFiles(c.modDir)
if err != nil {
return err
}
for _, m := range modFiles {
mc := c
mc.rawTarget, _ = bingo.NameFromModFile(m)
if err := get(ctx, logger, mc); err != nil {
return err
}
}
return nil
}
var modVersions []string
s := strings.Split(c.rawTarget, "@")
nameOrPackage := s[0]
if len(s) > 1 {
modVersions = strings.Split(s[1], ",")
}
if len(modVersions) > 1 {
for _, v := range modVersions {
if v == "none" {
return errors.Errorf("none is not allowed when there are more than one specified Version, got: %v", modVersions)
}
}
}
if len(modVersions) == 0 {
modVersions = append(modVersions, "")
}
pkgPath := nameOrPackage
name := nameOrPackage
if !strings.Contains(nameOrPackage, "/") {
// Binary referenced by name, get full package name if module file exists.
pkgPath, err = packagePathFromBinaryName(nameOrPackage, c.modDir)
if err != nil {
return err
}
if c.name != "" && c.name != name {
// Rename requested. Remove old mod(s) in this case, but only at the end.
defer func() { _ = removeAllGlob(filepath.Join(c.modDir, name+".*")) }()
}
} else {
// Binary referenced by path, get default name from package path.
name = path.Base(pkgPath)
}
if c.name != "" {
name = c.name
}
if name == strings.TrimSuffix(fakeRootModFileName, ".mod") {
return errors.New("requested binary with name `go`. This is impossible, choose different name using -name flag.")
}
binModFiles, err := filepath.Glob(filepath.Join(c.modDir, name+".*.mod"))
if err != nil {
return err
}
binModFiles = append([]string{filepath.Join(c.modDir, name+".mod")}, binModFiles...)
if modVersions[0] == "none" {
// none means we no longer want to Version this package.
// NOTE: We don't remove binaries.
return removeAllGlob(filepath.Join(c.modDir, name+".*"))
}
for i, v := range modVersions {
if err := getOne(ctx, logger, c, i, v, pkgPath, name); err != nil {
return errors.Wrapf(err, "%d: getting %s", i, v)
}
}
// Remove unused mod files.
for i := len(binModFiles); i > 0 && i > len(modVersions); i-- {
if err := os.RemoveAll(filepath.Join(c.modDir, fmt.Sprintf("%s.%d.mod", name, i-1))); err != nil {
return err
}
}
return nil
}
func cleanGoGetTmpFiles(modDir string) error {
// Remove all sum and tmp files
if err := removeAllGlob(filepath.Join(modDir, "*.sum")); err != nil {
return err
}
return removeAllGlob(filepath.Join(modDir, "*.tmp.*"))
}
func getOne(
ctx context.Context,
logger *log.Logger,
c getConfig,
i int,
version string,
pkgPath string,
name string,
) (err error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Minute)
defer cancel()
// The out module file we generate/maintain keep in modDir.
outModFile := filepath.Join(c.modDir, name+".mod")
if i > 0 {
outModFile = filepath.Join(c.modDir, fmt.Sprintf("%s.%d.mod", name, i))
}
// Cleanup all for fresh start.
if err := cleanGoGetTmpFiles(c.modDir); err != nil {
return err
}
if err := ensureModDirExists(logger, c.relModDir); err != nil {
return errors.Wrap(err, "ensure mod dir")
}
// Set up tmp file that we will work on for now.
// This is to avoid partial updates.
tmpModFile := filepath.Join(c.modDir, name+".tmp.mod")
emptyModFile, err := createTmpModFileFromExisting(ctx, c.runner, outModFile, tmpModFile)
if err != nil {
return errors.Wrap(err, "create tmp mod file")
}
runnable := c.runner.With(ctx, tmpModFile, c.modDir)
if version != "" || emptyModFile || c.update != gomodcmd.NoUpdatePolicy {
// Steps 1 & 2: Resolve and download (if needed) thanks to 'go get' on our separate .mod file.
targetWithVer := pkgPath
if version != "" {
targetWithVer = fmt.Sprintf("%s@%s", pkgPath, version)
}
if err := runnable.GetD(c.update, targetWithVer); err != nil {
return errors.Wrap(err, "go get -d")
}
}
if err := bingo.EnsureModMeta(tmpModFile, pkgPath); err != nil {
return errors.Wrap(err, "ensuring meta")
}
// Check if path is pointing to non-buildable package. Fail it is non-buildable. Hacky!
if listOutput, err := runnable.List("-f={{.Name}}", pkgPath); err != nil {
return err
} else if !strings.HasSuffix(listOutput, "main") {
return errors.Errorf("package %s is non-main (go list output %q), nothing to get and build", pkgPath, listOutput)
}
// Refetch Version to ensure we have correct one.
_, version, err = bingo.ModDirectPackage(tmpModFile, nil)
if err != nil {
return errors.Wrap(err, "get direct package")
}
// We were working on tmp file, do atomic rename.
if err := os.Rename(tmpModFile, outModFile); err != nil {
return errors.Wrap(err, "rename")
}
// Step 3: Build and install.
return c.runner.With(ctx, outModFile, c.modDir).Build(pkgPath, fmt.Sprintf("%s-%s", name, version))
}
func packagePathFromBinaryName(binary string, modDir string) (string, error) {
currModFile := filepath.Join(modDir, binary+".mod")
// Get full import path from module file which has module and encoded sub path.
if _, err := os.Stat(currModFile); err != nil {
if os.IsNotExist(err) {
return "", errors.Errorf("binary %q was not installed before. Use full package name to install it", binary)
}
return "", err
}
m, _, err := bingo.ModDirectPackage(currModFile, nil)
if err != nil {
return "", errors.Wrapf(err, "binary %q was installed, but go modules %s is malformed. Use full package name to reinstall it", binary, currModFile)
}
return m, nil
}
const modREADMEFmt = `# Project Development Dependencies.
This is directory which stores Go modules with pinned buildable package that is used within this repository, managed by https://github.com/bwplotka/bingo.
* Run ` + "`" + "bingo get" + "`" + ` to install all tools having each own module file in this directory.
* Run ` + "`" + "bingo get <tool>" + "`" + ` to install <tool> that have own module file in this directory.
* For Makefile: Make sure to put ` + "`" + "include %s/" + bingo.MakefileBinVarsName + "`" + ` in your Makefile, then use $(<upper case tool name>) variable where <tool> is the %s/<tool>.mod.
* For shell: Run ` + "`" + "source %s/" + bingo.EnvBinVarsName + "`" + ` to source all environment variable for each tool
* See https://github.com/bwplotka/bingo or -h on how to add, remove or change binaries dependencies.
## Requirements
* Go 1.14+
`
const gitignore = `
# Ignore everything
*
# But not these files:
!.gitignore
!*.mod
!README.md
!Variables.mk
!variables.env
*tmp.mod
`
func ensureModDirExists(logger *log.Logger, relModDir string) error {
_, err := os.Stat(relModDir)
if err != nil {
if !os.IsNotExist(err) {
return errors.Wrapf(err, "stat bingo module dir %s", relModDir)
}
logger.Printf("Bingo not used before here, creating directory for pinned modules for you at %s\n", relModDir)
if err := os.MkdirAll(relModDir, os.ModePerm); err != nil {
return errors.Wrapf(err, "create moddir %s", relModDir)
}
}
// Hack against:
// "A file named go.mod must still be present in order to determine the module root directory, but it is not accessed."
// Ref: https://golang.org/doc/go1.14#go-flags
// TODO(bwplotka): Remove it: https://github.com/bwplotka/bingo/issues/20
if err := ioutil.WriteFile(
filepath.Join(relModDir, fakeRootModFileName),
[]byte("module _ // Fake go.mod auto-created by 'bingo' for go -moddir compatibility with non-Go projects. Commit this file, together with other .mod files."),
os.ModePerm,
); err != nil {
return err
}
// README.
if err := ioutil.WriteFile(
filepath.Join(relModDir, "README.md"),
[]byte(fmt.Sprintf(modREADMEFmt, relModDir, relModDir, relModDir)),
os.ModePerm,
); err != nil {
return err
}
// gitignore.
return ioutil.WriteFile(
filepath.Join(relModDir, ".gitignore"),
[]byte(gitignore),
os.ModePerm,
)
}
func createTmpModFileFromExisting(ctx context.Context, r *gomodcmd.Runner, modFile, tmpModFile string) (emptyModFile bool, _ error) {
if err := os.RemoveAll(tmpModFile); err != nil {
return false, errors.Wrap(err, "rm")
}
_, err := os.Stat(modFile)
if err != nil && !os.IsNotExist(err) {
return false, errors.Wrapf(err, "stat module file %s", modFile)
}
if err == nil {
return false, copyFile(modFile, tmpModFile)
}
return true, errors.Wrap(r.With(ctx, tmpModFile, filepath.Dir(modFile)).ModInit("_"), "mod init")
}
func copyFile(src, dst string) error {
source, err := os.Open(src)
if err != nil {
return err
}
// TODO(bwplotka): Check those errors in defer.
defer source.Close()
destination, err := os.Create(dst)
if err != nil {
return err
}
defer destination.Close()
buf := make([]byte, 1024)
for {
n, err := source.Read(buf)
if err != nil && err != io.EOF {
return err
}
if n == 0 {
break
}
if _, err := destination.Write(buf[:n]); err != nil {
return err
}
}
return nil
}
func removeAllGlob(glob string) error {
files, err := filepath.Glob(glob)
if err != nil {
return err
}
for _, f := range files {
if err := os.RemoveAll(f); err != nil {
return err
}
}
return nil
}