diff --git a/manifest/resolver.go b/manifest/resolver.go index 04709397..39026676 100644 --- a/manifest/resolver.go +++ b/manifest/resolver.go @@ -519,8 +519,16 @@ func newPackage(manifest *AnnotatedManifest, config Config, selector Selector) ( files[k] = v } } - // Verify. - if len(p.Binaries) == 0 && len(p.Apps) == 0 { + // Verify. Packages without binaries or apps are permitted as long as they + // contribute something else to the environment: lifecycle trigger actions + // or environment variables. This supports content-only packages whose + // payload is materialised via "on unpack"/"on activate" actions. Empty + // trigger blocks do not count. + triggerActions := 0 + for _, actions := range p.Triggers { + triggerActions += len(actions) + } + if len(p.Binaries) == 0 && len(p.Apps) == 0 && triggerActions == 0 && len(layerEnvars) == 0 { return p, errors.Wrapf(ErrNoBinaries, "%s: %s", manifest.Path, found) } if p.Source == "" { diff --git a/manifest/resolver_test.go b/manifest/resolver_test.go index 2688a6e1..1fb870c0 100644 --- a/manifest/resolver_test.go +++ b/manifest/resolver_test.go @@ -353,6 +353,99 @@ func TestResolver_Resolve(t *testing.T) { }, reference: "test@edge", wantErr: "failed to expand ${version} (hint: make sure all channels set a version range)", + }, { + name: "Content-only package with triggers resolves", + files: map[string]string{ + "content.hcl": ` + description = "" + on "unpack" { + copy { from = "foo/bar" to = "${root}/fizz" } + } + version "1.0.0" { + source = "www.example.com" + } + `, + }, + reference: "content", + wantPkg: manifesttest.NewPkgBuilder(config.State+"/pkg/content-1.0.0"). + WithName("content"). + WithBinaries(). + WithVersion("1.0.0"). + WithSource("www.example.com"). + WithTrigger(EventUnpack, + &CopyAction{From: "foo/bar", To: config.State + "/pkg/content-1.0.0/fizz"}, + ). + Result(), + }, { + name: "Env-only package resolves", + files: map[string]string{ + "envonly.hcl": ` + description = "" + env = { + GOPATH: "${env}/go", + } + version "1.0.0" { + source = "www.example.com" + } + `, + }, + reference: "envonly", + wantPkg: manifesttest.NewPkgBuilder(config.State + "/pkg/envonly-1.0.0"). + WithName("envonly"). + WithBinaries(). + WithVersion("1.0.0"). + WithEnvOps(&envars.Set{Name: "GOPATH", Value: config.Env + "/go"}). + WithSource("www.example.com"). + Result(), + }, { + name: "Package with only an empty trigger block is rejected", + files: map[string]string{ + "emptytrigger.hcl": ` + description = "" + on "unpack" { + } + version "1.0.0" { + source = "www.example.com" + } + `, + }, + reference: "emptytrigger", + wantErr: "memory:///emptytrigger.hcl: emptytrigger-1.0.0: no binaries or apps provided", + wantPkg: func() *Package { + p := manifesttest.NewPkgBuilder(config.State + "/pkg/emptytrigger-1.0.0"). + WithName("emptytrigger"). + WithBinaries(). + WithVersion("1.0.0"). + WithSource("www.example.com"). + WithTrigger(EventUnpack). + Result() + p.Root = "${dest}" + return p + }(), + }, { + name: "Package with no binaries, apps, triggers or env is rejected", + files: map[string]string{ + "empty.hcl": ` + description = "" + version "1.0.0" { + source = "www.example.com" + } + `, + }, + reference: "empty", + wantErr: "memory:///empty.hcl: empty-1.0.0: no binaries or apps provided", + // Resolve returns the partially resolved package alongside + // ErrNoBinaries so that Search can still surface it. + wantPkg: func() *Package { + p := manifesttest.NewPkgBuilder(config.State + "/pkg/empty-1.0.0"). + WithName("empty"). + WithBinaries(). + WithVersion("1.0.0"). + WithSource("www.example.com"). + Result() + p.Root = "${dest}" + return p + }(), }, } for _, tt := range tests {