Skip to content

Commit 7cb55f0

Browse files
author
Christian Weichel
committed
[env-mf] Introduce an environment manifest to capture the build env
fixes #29
1 parent 2b7c54d commit 7cb55f0

File tree

8 files changed

+224
-16
lines changed

8 files changed

+224
-16
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,23 @@ This workspace has a (nonsensical) `nogo` variant that, when enabled, excludes a
201201
It also changes the config of all Go packages to include the `-tags foo` flag. You can explore the effects of a variant using `collect` and `describe`, e.g. `leeway --variant nogo collect files` vs `leeway collect files`.
202202
You can list all variants in a workspace using `leeway collect variants`.
203203

204+
## Environment Manifest
205+
Leeway does not control the environment in which it builds the packages, but assumes that all required tools are available already (e.g. `go` or `yarn`).
206+
This however can lead to subtle failure modes where a package built in one enviroment ends up being used in another, because no matter of the environment they were built in, they get the same version.
207+
208+
To prevent such issues, leeway computes an _environment manifest_ which contains the versions of the tools used, as well as some platform information.
209+
The entries in that manifest depend on the package types used by that workspace, e.g. if only `Go` packages exist in the workspace, only `go version`, [GOOS and GOARCH](https://golang.org/pkg/runtime/#pkg-constants) will be part of the manifest.
210+
You can inspect a workspace's environment manifest using `leeway describe environment-manifest`.
211+
212+
You can add your own entries to a workspace's environment manifest in the `WORKSPACE.yaml` like so:
213+
```YAML
214+
environmentManifest:
215+
- name: gcc
216+
command: ["gcc", "--version"]
217+
```
218+
219+
Using this mechanism you can also overwrite the default manifest entries, e.g. "go" or "yarn".
220+
204221
## Nested Workspaces
205222
Leeway has some experimental support for nested workspaces, e.g. a structure like this one:
206223
```

WORKSPACE.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
defaultTarget: "//:app"
22
defaultArgs:
33
version: dev
4+
environmentManifest:
5+
- name: "node"
6+
command: ["node", "--version"]
7+
- name: "yarn"
8+
command: ["yarn", "--version"]
49
variants:
510
- name: nogit
611
srcs:
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
6+
log "github.com/sirupsen/logrus"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
// describeEnvironmentManifestCmd represents the describeManifest command
11+
var describeEnvironmentManifestCmd = &cobra.Command{
12+
Use: "environment-manifest",
13+
Short: "Prints the environment manifest of a workspace",
14+
Run: func(cmd *cobra.Command, args []string) {
15+
ws, err := getWorkspace()
16+
if err != nil {
17+
log.WithError(err).Fatal("cannot load workspace")
18+
}
19+
20+
err = ws.EnvironmentManifest.Write(os.Stdout)
21+
if err != nil {
22+
log.Fatal(err)
23+
}
24+
},
25+
}
26+
27+
func init() {
28+
describeCmd.AddCommand(describeEnvironmentManifestCmd)
29+
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
defaultArgs:
2-
message: "hello world"
2+
message: "hello world"
3+
environmentManifest:
4+
- name: "foobar"
5+
command: ["echo", "foobar"]

pkg/leeway/package.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ func (v *PackageVariant) ResolveSources(workspace *Workspace, loc string) (incl
569569

570570
func resolveSources(workspace *Workspace, loc string, globs []string, includeDirs bool) (res []string, err error) {
571571
for _, glb := range globs {
572-
srcs, err := doublestar.Glob(loc, glb, workspace.ShouldIngoreSource)
572+
srcs, err := doublestar.Glob(loc, glb, workspace.ShouldIgnoreSource)
573573
if err != nil {
574574
return nil, err
575575
}
@@ -582,7 +582,7 @@ func resolveSources(workspace *Workspace, loc string, globs []string, includeDir
582582
if !includeDirs && stat.IsDir() {
583583
continue
584584
}
585-
if workspace.ShouldIngoreSource(src) {
585+
if workspace.ShouldIgnoreSource(src) {
586586
continue
587587
}
588588
res = append(res, src)
@@ -785,6 +785,10 @@ func (p *Package) WriteVersionManifest(out io.Writer) error {
785785
return xerrors.Errorf("package is not linked")
786786
}
787787

788+
envhash, err := p.C.W.EnvironmentManifest.Hash()
789+
if err != nil {
790+
return err
791+
}
788792
defhash, err := p.DefinitionHash()
789793
if err != nil {
790794
return err
@@ -798,6 +802,10 @@ func (p *Package) WriteVersionManifest(out io.Writer) error {
798802
if err != nil {
799803
return err
800804
}
805+
_, err = fmt.Fprintf(out, "environment: %s\n", envhash)
806+
if err != nil {
807+
return err
808+
}
801809
_, err = fmt.Fprintf(out, "definition: %s\n", defhash)
802810
if err != nil {
803811
return err

pkg/leeway/workspace.go

Lines changed: 144 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
package leeway
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/hex"
57
"fmt"
8+
"io"
69
"io/ioutil"
710
"os"
11+
"os/exec"
812
"path/filepath"
913
"reflect"
14+
"runtime"
1015
"runtime/trace"
1116
"sort"
1217
"strings"
1318
"sync"
19+
"time"
1420

1521
"github.com/imdario/mergo"
22+
"github.com/minio/highwayhash"
1623
log "github.com/sirupsen/logrus"
1724
"github.com/typefox/leeway/pkg/doublestar"
1825
"golang.org/x/sync/errgroup"
@@ -23,9 +30,10 @@ import (
2330
// Workspace is the root container of all compoments. All components are named relative
2431
// to the origin of this workspace.
2532
type Workspace struct {
26-
DefaultTarget string `yaml:"defaultTarget,omitempty"`
27-
ArgumentDefaults map[string]string `yaml:"defaultArgs,omitempty"`
28-
Variants []*PackageVariant `yaml:"variants,omitempty"`
33+
DefaultTarget string `yaml:"defaultTarget,omitempty"`
34+
ArgumentDefaults map[string]string `yaml:"defaultArgs,omitempty"`
35+
Variants []*PackageVariant `yaml:"variants,omitempty"`
36+
EnvironmentManifest EnvironmentManifest `yaml:"environmentManifest,omitempty"`
2937

3038
Origin string `yaml:"-"`
3139
Components map[string]*Component `yaml:"-"`
@@ -36,13 +44,79 @@ type Workspace struct {
3644
ignores []string
3745
}
3846

39-
// ShouldIngoreComponent returns true if a file should be ignored for a component listing
40-
func (ws *Workspace) ShouldIngoreComponent(path string) bool {
41-
return ws.ShouldIngoreSource(path)
47+
// EnvironmentManifest is a collection of environment manifest entries
48+
type EnvironmentManifest []EnvironmentManifestEntry
49+
50+
// Write writes the manifest to the writer
51+
func (mf EnvironmentManifest) Write(out io.Writer) error {
52+
for _, e := range mf {
53+
_, err := fmt.Fprintf(out, "%s: %s\n", e.Name, e.Value)
54+
if err != nil {
55+
return err
56+
}
57+
}
58+
return nil
59+
}
60+
61+
// Hash produces the hash of this manifest
62+
func (mf EnvironmentManifest) Hash() (string, error) {
63+
key, err := hex.DecodeString(contentHashKey)
64+
if err != nil {
65+
return "", err
66+
}
67+
68+
hash, err := highwayhash.New(key)
69+
if err != nil {
70+
return "", err
71+
}
72+
73+
err = mf.Write(hash)
74+
if err != nil {
75+
return "", err
76+
}
77+
78+
return hex.EncodeToString(hash.Sum(nil)), nil
79+
}
80+
81+
// EnvironmentManifestEntry represents an entry in the environment manifest
82+
type EnvironmentManifestEntry struct {
83+
Name string `yaml:"name"`
84+
Command []string `yaml:"command"`
85+
86+
Value string `yaml:"-"`
87+
Builtin bool `yaml:"-"`
88+
}
89+
90+
const (
91+
builtinEnvManifestGOOS = "goos"
92+
builtinEnvManifestGOARCH = "goarch"
93+
)
94+
95+
var defaultEnvManifestEntries = map[PackageType]EnvironmentManifest{
96+
"": []EnvironmentManifestEntry{
97+
{Name: "os", Command: []string{builtinEnvManifestGOOS}, Builtin: true},
98+
{Name: "arch", Command: []string{builtinEnvManifestGOARCH}, Builtin: true},
99+
},
100+
GenericPackage: []EnvironmentManifestEntry{},
101+
DockerPackage: []EnvironmentManifestEntry{
102+
{Name: "docker", Command: []string{"docker", "version", "--format", "{{.Client.Version}} {{.Server.Version}}"}},
103+
},
104+
GoPackage: []EnvironmentManifestEntry{
105+
{Name: "go", Command: []string{"go", "version"}},
106+
},
107+
YarnPackage: []EnvironmentManifestEntry{
108+
{Name: "yarn", Command: []string{"yarn", "-v"}},
109+
{Name: "node", Command: []string{"node", "--version"}},
110+
},
111+
}
112+
113+
// ShouldIgnoreComponent returns true if a file should be ignored for a component listing
114+
func (ws *Workspace) ShouldIgnoreComponent(path string) bool {
115+
return ws.ShouldIgnoreSource(path)
42116
}
43117

44-
// ShouldIngoreSource returns true if a file should be ignored for a source listing
45-
func (ws *Workspace) ShouldIngoreSource(path string) bool {
118+
// ShouldIgnoreSource returns true if a file should be ignored for a source listing
119+
func (ws *Workspace) ShouldIgnoreSource(path string) bool {
46120
for _, ptn := range ws.ignores {
47121
if strings.Contains(path, ptn) {
48122
return true
@@ -199,7 +273,7 @@ func loadWorkspace(ctx context.Context, path string, args Arguments, variant str
199273
}
200274
ignores = strings.Split(string(fc), "\n")
201275
}
202-
otherWS, err := doublestar.Glob(workspace.Origin, "**/WORKSPACE.yaml", workspace.ShouldIngoreSource)
276+
otherWS, err := doublestar.Glob(workspace.Origin, "**/WORKSPACE.yaml", workspace.ShouldIgnoreSource)
203277
if err != nil {
204278
return Workspace{}, err
205279
}
@@ -212,7 +286,7 @@ func loadWorkspace(ctx context.Context, path string, args Arguments, variant str
212286
ignores = append(ignores, dir)
213287
}
214288
workspace.ignores = ignores
215-
log.WithField("ingores", workspace.ignores).Debug("computed workspace ignores")
289+
log.WithField("ignores", workspace.ignores).Debug("computed workspace ignores")
216290

217291
if len(opts.ArgumentDefaults) > 0 {
218292
if workspace.ArgumentDefaults == nil {
@@ -245,17 +319,26 @@ func loadWorkspace(ctx context.Context, path string, args Arguments, variant str
245319
workspace.Components = make(map[string]*Component)
246320
workspace.Packages = make(map[string]*Package)
247321
workspace.Scripts = make(map[string]*Script)
322+
packageTypesUsed := make(map[PackageType]struct{})
248323
for _, comp := range comps {
249324
workspace.Components[comp.Name] = comp
250325

251326
for _, pkg := range comp.Packages {
252327
workspace.Packages[pkg.FullName()] = pkg
328+
packageTypesUsed[pkg.Type] = struct{}{}
253329
}
254330
for _, script := range comp.Scripts {
255331
workspace.Scripts[script.FullName()] = script
256332
}
257333
}
258334

335+
// with all packages loaded we can compute the env manifest, becuase now we know which package types are actually
336+
// used, hence know the default env manifest entries.
337+
workspace.EnvironmentManifest, err = buildEnvironmentManifest(workspace.EnvironmentManifest, packageTypesUsed)
338+
if err != nil {
339+
return Workspace{}, err
340+
}
341+
259342
// now that we have all components/packages, we can link things
260343
if opts != nil && opts.PrelinkModifier != nil {
261344
opts.PrelinkModifier(workspace.Packages)
@@ -298,6 +381,55 @@ func loadWorkspace(ctx context.Context, path string, args Arguments, variant str
298381
return workspace, nil
299382
}
300383

384+
// buildEnvironmentManifest executes the commands of an env manifest and updates the values
385+
func buildEnvironmentManifest(entries EnvironmentManifest, pkgtpes map[PackageType]struct{}) (res EnvironmentManifest, err error) {
386+
t0 := time.Now()
387+
388+
envmf := make(map[string]EnvironmentManifestEntry, len(entries))
389+
for _, e := range defaultEnvManifestEntries[""] {
390+
envmf[e.Name] = e
391+
}
392+
for tpe := range pkgtpes {
393+
for _, e := range defaultEnvManifestEntries[tpe] {
394+
envmf[e.Name] = e
395+
}
396+
}
397+
for _, e := range entries {
398+
e := e
399+
envmf[e.Name] = e
400+
}
401+
402+
for k, e := range envmf {
403+
if e.Builtin {
404+
switch e.Command[0] {
405+
case builtinEnvManifestGOARCH:
406+
e.Value = runtime.GOARCH
407+
case builtinEnvManifestGOOS:
408+
e.Value = runtime.GOOS
409+
}
410+
res = append(res, e)
411+
continue
412+
}
413+
414+
out := bytes.NewBuffer(nil)
415+
cmd := exec.Command(e.Command[0], e.Command[1:]...)
416+
cmd.Stdout = out
417+
err := cmd.Run()
418+
if err != nil {
419+
return nil, xerrors.Errorf("cannot resolve env manifest entry %v: %w", k, err)
420+
}
421+
e.Value = strings.TrimSpace(out.String())
422+
423+
res = append(res, e)
424+
}
425+
426+
sort.Slice(res, func(i, j int) bool { return res[i].Name < res[j].Name })
427+
428+
log.WithField("time", time.Since(t0).String()).WithField("res", res).Debug("built environment manifest")
429+
430+
return
431+
}
432+
301433
// FindWorkspace looks for a WORKSPACE.yaml file within the path. If multiple such files are found,
302434
// an error is returned.
303435
func FindWorkspace(path string, args Arguments, variant string) (Workspace, error) {
@@ -309,7 +441,7 @@ func discoverComponents(ctx context.Context, workspace *Workspace, args Argument
309441
defer trace.StartRegion(context.Background(), "discoverComponents").End()
310442

311443
path := workspace.Origin
312-
pths, err := doublestar.Glob(path, "**/BUILD.yaml", workspace.ShouldIngoreSource)
444+
pths, err := doublestar.Glob(path, "**/BUILD.yaml", workspace.ShouldIgnoreSource)
313445
if err != nil {
314446
return nil, err
315447
}
@@ -318,7 +450,7 @@ func discoverComponents(ctx context.Context, workspace *Workspace, args Argument
318450
cchan := make(chan *Component, 20)
319451

320452
for _, pth := range pths {
321-
if workspace.ShouldIngoreComponent(pth) {
453+
if workspace.ShouldIgnoreComponent(pth) {
322454
continue
323455
}
324456

pkg/leeway/workspace_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io/ioutil"
77
"os"
88
"path/filepath"
9+
"strings"
910
"testing"
1011
)
1112

@@ -72,6 +73,19 @@ func TestFixtureLoadWorkspace(t *testing.T) {
7273
StdoutSub: "hello root",
7374
ExitCode: 0,
7475
},
76+
{
77+
Name: "environment manifest",
78+
T: t,
79+
Args: []string{"describe", "-w", "fixtures/nested-ws/wsa", "environment-manifest"},
80+
Eval: func(t *testing.T, stdout, stderr string) {
81+
for _, k := range []string{"os", "arch", "foobar"} {
82+
if !strings.Contains(stdout, fmt.Sprintf("%s: ", k)) {
83+
t.Errorf("missing %s entry in environment manifest", k)
84+
}
85+
}
86+
},
87+
ExitCode: 0,
88+
},
7589
}
7690

7791
for _, test := range tests {

pkg/vet/docker_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ ADD from-some-pkg--build/hello.txt hello.txt`,
6262
// defer os.RemoveAll(tmpdir)
6363

6464
var pkgdeps string
65-
failOnErr(ioutil.WriteFile(filepath.Join(tmpdir, "WORKSPACE.yaml"), nil, 0644))
65+
failOnErr(ioutil.WriteFile(filepath.Join(tmpdir, "WORKSPACE.yaml"), []byte("environmentManifest:\n - name: \"docker\"\n command: [\"echo\"]"), 0644))
6666
for _, dep := range test.Deps {
6767
segs := strings.Split(dep, ":")
6868
loc := filepath.Join(tmpdir, segs[0])

0 commit comments

Comments
 (0)