diff --git a/BUILD.yaml b/BUILD.yaml index c0cdd95..6d727de 100644 --- a/BUILD.yaml +++ b/BUILD.yaml @@ -17,6 +17,7 @@ packages: - go.sum deps: - :helloworld + - :doesntExist? argdeps: - version prep: diff --git a/README.md b/README.md index 1c093fe..6a779ed 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ description: A sentence describing what the script is good for. # contains tools you want to use in a script. deps: - some/other:package +- some/other:optional? # Env sets environment variables which are present during script execution. env: - MESSAGE=hello diff --git a/pkg/leeway/package.go b/pkg/leeway/package.go index bb6b8c4..865013c 100644 --- a/pkg/leeway/package.go +++ b/pkg/leeway/package.go @@ -124,7 +124,7 @@ func (c *Component) Git() *GitInfo { // PackageNotFoundErr is used when something references a package we don't know about type PackageNotFoundErr struct { - Package string + Package PackageDependency } func (n PackageNotFoundErr) Error() string { @@ -132,15 +132,59 @@ func (n PackageNotFoundErr) Error() string { } type packageInternal struct { - Name string `yaml:"name"` - Type PackageType `yaml:"type"` - Sources []string `yaml:"srcs"` - Dependencies []string `yaml:"deps"` - Layout map[string]string `yaml:"layout"` - ArgumentDependencies []string `yaml:"argdeps"` - Environment []string `yaml:"env"` - Ephemeral bool `yaml:"ephemeral"` - PreparationCommands [][]string `yaml:"prep"` + Name string `yaml:"name"` + Type PackageType `yaml:"type"` + Sources []string `yaml:"srcs"` + Dependencies []PackageDependency `yaml:"deps"` + Layout map[string]string `yaml:"layout"` + ArgumentDependencies []string `yaml:"argdeps"` + Environment []string `yaml:"env"` + Ephemeral bool `yaml:"ephemeral"` + PreparationCommands [][]string `yaml:"prep"` +} + +type PackageDependency string + +func (dep PackageDependency) Valid() bool { + _, pkg := dep.segs() + return pkg != "" +} + +func (dep PackageDependency) segs() (comp, pkg string) { + segs := strings.Split(dep.FullPackage(), ":") + if len(segs) != 2 { + return + } + return segs[0], segs[1] +} + +// Component returns the component of the depdency +func (dep PackageDependency) Component() string { + comp, _ := dep.segs() + return comp +} + +// FullPackage returns the name of the package this dependency refers to +func (dep PackageDependency) FullPackage() string { + return strings.TrimSuffix(string(dep), "?") +} + +// Optional returns true if the dependency is optional +func (dep PackageDependency) Optional() bool { + return strings.HasSuffix(string(dep), "?") +} + +// Relative returns true if the dependency is relative within its component +func (dep PackageDependency) Relative() bool { + return strings.HasPrefix(string(dep), ":") +} + +func (dep PackageDependency) WithComponent(comp string) PackageDependency { + _, pkg := dep.segs() + if dep.Optional() { + pkg += "?" + } + return PackageDependency(comp + ":" + pkg) } // Package is a single buildable artifact within a component @@ -168,17 +212,20 @@ func (p *Package) link(idx map[string]*Package) error { return nil } - p.dependencies = make([]*Package, len(p.Dependencies)) + p.dependencies = make([]*Package, 0, len(p.Dependencies)) p.layout = make(map[*Package]string) - for i, dep := range p.Dependencies { - deppkg, ok := idx[dep] + for _, dep := range p.Dependencies { + deppkg, ok := idx[dep.FullPackage()] if !ok { + if dep.Optional() { + continue + } return PackageNotFoundErr{dep} } - p.dependencies[i] = deppkg + p.dependencies = append(p.dependencies, deppkg) // if the user hasn't specified a layout, tie it down at this point - p.layout[deppkg], ok = p.Layout[dep] + p.layout[deppkg], ok = p.Layout[dep.FullPackage()] if !ok { p.layout[deppkg] = deppkg.FilesystemSafeName() } diff --git a/pkg/leeway/package_test.go b/pkg/leeway/package_test.go index e1953ea..41253fd 100644 --- a/pkg/leeway/package_test.go +++ b/pkg/leeway/package_test.go @@ -6,6 +6,108 @@ import ( "testing" ) +func TestPackageDependencyValid(t *testing.T) { + tests := []struct { + Input string + Expectation bool + }{ + {":foo", true}, + {":foo?", true}, + {"absolute/component:foo?", true}, + {"missing/package", false}, + {":", false}, + {"", false}, + } + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + act := PackageDependency(test.Input).Valid() + if act != test.Expectation { + t.Fatalf("expected %v, got %v", test.Expectation, act) + } + }) + } +} + +func TestPackageDependencyOptional(t *testing.T) { + tests := []struct { + Input string + Expectation bool + }{ + {":foo", false}, + {":foo?", true}, + {"absolute/component:foo", false}, + {"absolute/component:foo?", true}, + } + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + act := PackageDependency(test.Input).Optional() + if act != test.Expectation { + t.Fatalf("expected %v, got %v", test.Expectation, act) + } + }) + } +} + +func TestPackageDependencyRelative(t *testing.T) { + tests := []struct { + Input string + Expectation bool + }{ + {":foo", true}, + {":foo?", true}, + {"absolute/component:foo", false}, + {"absolute/component:foo?", false}, + } + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + act := PackageDependency(test.Input).Relative() + if act != test.Expectation { + t.Fatalf("expected %v, got %v", test.Expectation, act) + } + }) + } +} + +func TestPackageDependencyComponent(t *testing.T) { + tests := []struct { + Input string + Expectation string + }{ + {":foo", ""}, + {":foo?", ""}, + {"absolute/component:foo?", "absolute/component"}, + {"", ""}, + } + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + act := PackageDependency(test.Input).Component() + if act != test.Expectation { + t.Fatalf("expected %v, got %v", test.Expectation, act) + } + }) + } +} + +func TestPackageDependencyFullPackage(t *testing.T) { + tests := []struct { + Input string + Expectation string + }{ + {":foo", ":foo"}, + {":foo?", ":foo"}, + {"absolute/component:foo?", "absolute/component:foo"}, + {"", ""}, + } + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + act := PackageDependency(test.Input).FullPackage() + if act != test.Expectation { + t.Fatalf("expected %v, got %v", test.Expectation, act) + } + }) + } +} + func TestResolveBuiltinVariables(t *testing.T) { tests := []struct { PkgType PackageType diff --git a/pkg/leeway/scripts.go b/pkg/leeway/scripts.go index 9d8c780..f35dc56 100644 --- a/pkg/leeway/scripts.go +++ b/pkg/leeway/scripts.go @@ -37,13 +37,13 @@ const ( type Script struct { C *Component - Name string `yaml:"name"` - Description string `yaml:"description"` - Dependencies []string `yaml:"deps"` - Environment []string `yaml:"env"` - WorkdirLayout WorkdirLayout `yaml:"workdir"` - Type ScriptType `yaml:"type"` - Script string `yaml:"script"` + Name string `yaml:"name"` + Description string `yaml:"description"` + Dependencies []PackageDependency `yaml:"deps"` + Environment []string `yaml:"env"` + WorkdirLayout WorkdirLayout `yaml:"workdir"` + Type ScriptType `yaml:"type"` + Script string `yaml:"script"` dependencies []*Package } @@ -55,13 +55,16 @@ func (p *Script) FullName() string { // link connects resolves the references to the dependencies func (p *Script) link(idx map[string]*Package) error { - p.dependencies = make([]*Package, len(p.Dependencies)) - for i, dep := range p.Dependencies { - var ok bool - p.dependencies[i], ok = idx[dep] + p.dependencies = make([]*Package, 0, len(p.Dependencies)) + for _, dep := range p.Dependencies { + pkg, ok := idx[dep.FullPackage()] if !ok { + if dep.Optional() { + continue + } return PackageNotFoundErr{dep} } + p.dependencies = append(p.dependencies, pkg) } return nil } diff --git a/pkg/leeway/workspace.go b/pkg/leeway/workspace.go index a8a5746..00d3bc3 100644 --- a/pkg/leeway/workspace.go +++ b/pkg/leeway/workspace.go @@ -573,12 +573,12 @@ func filterExcludedComponents(variant *PackageVariant, c *Component) (ignoreComp for _, p := range c.Packages { for i, dep := range p.Dependencies { - segs := strings.Split(dep, ":") - if len(segs) != 2 { + comp := dep.Component() + if comp == "" { continue } - if variant.ExcludeComponent(segs[0]) { + if variant.ExcludeComponent(comp) { p.Dependencies[i] = p.Dependencies[len(p.Dependencies)-1] p.Dependencies = p.Dependencies[:len(p.Dependencies)-1] } @@ -759,11 +759,11 @@ func loadComponent(ctx context.Context, workspace *Workspace, path string, args // make all dependencies fully qualified for idx, dep := range pkg.Dependencies { - if !strings.HasPrefix(dep, ":") { + if !dep.Relative() { continue } - pkg.Dependencies[idx] = comp.Name + dep + pkg.Dependencies[idx] = dep.WithComponent(comp.Name) } // make all layout entries full qualified if pkg.Layout == nil { @@ -807,11 +807,11 @@ func loadComponent(ctx context.Context, workspace *Workspace, path string, args // make all dependencies fully qualified for idx, dep := range scr.Dependencies { - if !strings.HasPrefix(dep, ":") { + if !dep.Relative() { continue } - scr.Dependencies[idx] = comp.Name + dep + scr.Dependencies[idx] = dep.WithComponent(comp.Name) } }