diff --git a/buildpack.toml b/buildpack.toml index 31de8e5..0f554b8 100644 --- a/buildpack.toml +++ b/buildpack.toml @@ -60,6 +60,12 @@ api = "0.7" default = "false" description = "use maven daemon" name = "BP_MAVEN_DAEMON_ENABLED" + + [[metadata.configurations]] + build = true + default = "false" + description = "distribute maven binary" + name = "BP_MAVEN_COMMAND" [[metadata.configurations]] build = true diff --git a/maven/build.go b/maven/build.go index a44c3c6..71a0fe2 100644 --- a/maven/build.go +++ b/maven/build.go @@ -38,6 +38,11 @@ import ( "github.com/paketo-buildpacks/libpak/bindings" ) +const ( + Command = "Command" + RunBuild = "RunBuild" +) + type Build struct { Logger bard.Logger ApplicationFactory ApplicationFactory @@ -49,95 +54,72 @@ type ApplicationFactory interface { cache libbs.Cache, command string, bom *libcnb.BOM, applicationPath string, bomScanner sbom.SBOMScanner) (libbs.Application, error) } -func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { - b.Logger.Title(context.Buildpack) - result := libcnb.NewBuildResult() - - cr, err := libpak.NewConfigurationResolver(context.Buildpack, &b.Logger) - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err) - } - +func install(b Build, context libcnb.BuildContext, artifact string, securityArgs []string) (string, libcnb.LayerContributor, libcnb.BOMEntry, error) { dr, err := libpak.NewDependencyResolver(context) if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to create dependency resolver\n%w", err) + return "", nil, libcnb.BOMEntry{}, fmt.Errorf("unable to create dependency resolver\n%w", err) } dc, err := libpak.NewDependencyCache(context) if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to create dependency cache\n%w", err) + return "", nil, libcnb.BOMEntry{}, fmt.Errorf("unable to create dependency cache\n%w", err) } dc.Logger = b.Logger - command := "" - if cr.ResolveBool("BP_MAVEN_DAEMON_ENABLED") { - dep, err := dr.Resolve("mvnd", "") - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to find dependency\n%w", err) - } + dep, err := dr.Resolve(artifact, "") + if err != nil { + return "", nil, libcnb.BOMEntry{}, fmt.Errorf("unable to find dependency\n%w", err) + } - dist, be := NewMvndDistribution(dep, dc) + if artifact == "maven" { + dist, be := NewDistribution(dep, dc) + dist.SecurityArgs = securityArgs dist.Logger = b.Logger - result.Layers = append(result.Layers, dist) - result.BOM.Entries = append(result.BOM.Entries, be) - - command = filepath.Join(context.Layers.Path, dist.Name(), "bin", "mvnd") - } else { - command = filepath.Join(context.Application.Path, "mvnw") - if _, err := os.Stat(command); os.IsNotExist(err) { - dep, err := dr.Resolve("maven", "") - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to find dependency\n%w", err) - } - - dist, be := NewDistribution(dep, dc) - dist.Logger = b.Logger - result.Layers = append(result.Layers, dist) - result.BOM.Entries = append(result.BOM.Entries, be) + command := filepath.Join(context.Layers.Path, dist.Name(), "bin", "mvn") + return command, dist, be, nil + } + dist, be := NewDistribution(dep, dc) + dist.SecurityArgs = securityArgs + dist.Logger = b.Logger + command := filepath.Join(context.Layers.Path, dist.Name(), "bin", "mvnd") + return command, dist, be, nil +} - command = filepath.Join(context.Layers.Path, dist.Name(), "bin", "mvn") - } else if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to stat %s\n%w", command, err) - } else { - if err := os.Chmod(command, 0755); err != nil { - b.Logger.Bodyf("WARNING: unable to chmod %s:\n%s", command, err) - } +func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { + b.Logger.Title(context.Buildpack) + result := libcnb.NewBuildResult() - if err = b.CleanMvnWrapper(command); err != nil { - b.Logger.Bodyf("WARNING: unable to clean mvnw file: %s\n%s", command, err) - } + pr := libpak.PlanEntryResolver{ + Plan: context.Plan, + } + runBuild := true + entry, ok, err := pr.Resolve(PlanEntryMaven) + if ok && err == nil { + if runBuildValue, ok := entry.Metadata[RunBuild].(bool); ok { + runBuild = runBuildValue } } - - u, err := user.Current() - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to determine user home directory\n%w", err) + mavenCommand := "" + if command, ok := entry.Metadata[Command].(string); ok { + mavenCommand = command } - c := libbs.Cache{Path: filepath.Join(u.HomeDir, ".m2")} - c.Logger = b.Logger - result.Layers = append(result.Layers, c) - - args, err := libbs.ResolveArguments("BP_MAVEN_BUILD_ARGUMENTS", cr) + cr, err := libpak.NewConfigurationResolver(context.Buildpack, &b.Logger) if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to resolve build arguments\n%w", err) - } - - pomFile, userSet := cr.Resolve("BP_MAVEN_POM_FILE") - if userSet { - args = append([]string{"--file", pomFile}, args...) + return libcnb.BuildResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err) } - if !b.TTY && !contains(args, []string{"-B", "--batch-mode"}) { - // terminal is not tty, and the user did not set batch mode; let's set it - args = append([]string{"--batch-mode"}, args...) + // no install requested and no build requested + if mavenCommand == "" && !runBuild { + return libcnb.BuildResult{}, nil } md := map[string]interface{}{} + securityArgs := []string{} if binding, ok, err := bindings.ResolveOne(context.Platform.Bindings, bindings.OfType("maven")); err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to resolve binding\n%w", err) } else if ok { - args, err = handleMavenSettings(binding, args, md) + securityArgs, err = handleMavenSettings(binding, securityArgs, md) if err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to process maven settings from binding\n%w", err) } @@ -148,31 +130,103 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { } } - art := libbs.ArtifactResolver{ - ArtifactConfigurationKey: "BP_MAVEN_BUILT_ARTIFACT", - ConfigurationResolver: cr, - ModuleConfigurationKey: "BP_MAVEN_BUILT_MODULE", - InterestingFileDetector: libbs.JARInterestingFileDetector{}, + if mavenCommand == "maven" || mavenCommand == "mvnd" { + cmd, layer, bomEntry, err := install(b, context, mavenCommand, securityArgs) + if cmd == "" { + return libcnb.BuildResult{}, fmt.Errorf("unable to install dependency\n%w", err) + } + result.Layers = append(result.Layers, layer) + result.BOM.Entries = append(result.BOM.Entries, bomEntry) } - bomScanner := sbom.NewSyftCLISBOMScanner(context.Layers, effect.NewExecutor(), b.Logger) - - a, err := b.ApplicationFactory.NewApplication( - md, - args, - art, - c, - command, - result.BOM, - context.Application.Path, - bomScanner, - ) + u, err := user.Current() if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to create application layer\n%w", err) + return libcnb.BuildResult{}, fmt.Errorf("unable to determine user home directory\n%w", err) } - a.Logger = b.Logger - result.Layers = append(result.Layers, a) + c := libbs.Cache{Path: filepath.Join(u.HomeDir, ".m2")} + c.Logger = b.Logger + + if runBuild { + command := "" + if cr.ResolveBool("BP_MAVEN_DAEMON_ENABLED") && mavenCommand != "mvnd" { + cmd, layer, bomEntry, err := install(b, context, "mvnd", securityArgs) + if err != nil { + return libcnb.BuildResult{}, err + } + result.Layers = append(result.Layers, layer) + result.BOM.Entries = append(result.BOM.Entries, bomEntry) + command = cmd + } else { + command = filepath.Join(context.Application.Path, "mvnw") + if _, err := os.Stat(command); os.IsNotExist(err) && mavenCommand != "maven" { + cmd, layer, bomEntry, err := install(b, context, "maven", securityArgs) + if err != nil { + return libcnb.BuildResult{}, err + } + result.Layers = append(result.Layers, layer) + result.BOM.Entries = append(result.BOM.Entries, bomEntry) + command = cmd + } else if err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to stat %s\n%w", command, err) + } else { + if err := os.Chmod(command, 0755); err != nil { + b.Logger.Bodyf("WARNING: unable to chmod %s:\n%s", command, err) + } + + if err = b.CleanMvnWrapper(command); err != nil { + b.Logger.Bodyf("WARNING: unable to clean mvnw file: %s\n%s", command, err) + } + } + } + + result.Layers = append(result.Layers, c) + + args, err := libbs.ResolveArguments("BP_MAVEN_BUILD_ARGUMENTS", cr) + if err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to resolve build arguments\n%w", err) + } + + pomFile, userSet := cr.Resolve("BP_MAVEN_POM_FILE") + if userSet { + args = append([]string{"--file", pomFile}, args...) + } + + if !b.TTY && !contains(args, []string{"-B", "--batch-mode"}) { + // terminal is not tty, and the user did not set batch mode; let's set it + args = append([]string{"--batch-mode"}, args...) + } + + args = append(securityArgs, args...) + + art := libbs.ArtifactResolver{ + ArtifactConfigurationKey: "BP_MAVEN_BUILT_ARTIFACT", + ConfigurationResolver: cr, + ModuleConfigurationKey: "BP_MAVEN_BUILT_MODULE", + InterestingFileDetector: libbs.JARInterestingFileDetector{}, + } + + bomScanner := sbom.NewSyftCLISBOMScanner(context.Layers, effect.NewExecutor(), b.Logger) + + a, err := b.ApplicationFactory.NewApplication( + md, + args, + art, + c, + command, + result.BOM, + context.Application.Path, + bomScanner, + ) + if err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to create application layer\n%w", err) + } + + a.Logger = b.Logger + result.Layers = append(result.Layers, a) + } else { + result.Layers = append(result.Layers, c) + } return result, nil } diff --git a/maven/build_test.go b/maven/build_test.go index 7867420..464e59d 100644 --- a/maven/build_test.go +++ b/maven/build_test.go @@ -513,6 +513,160 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { _, err := mavenBuild.Build(ctx) Expect(err).NotTo(HaveOccurred()) }) + + context("distribute binaries only", func() { + it.Before(func() { + entry := libcnb.BuildpackPlanEntry{ + Name: maven.PlanEntryMaven, + Metadata: map[string]interface{}{ + maven.RunBuild: false, + maven.Command: "maven", + }, + } + ctx.Plan.Entries = append(ctx.Plan.Entries, entry) + }) + + it.After(func() { + _, ctx.Plan.Entries = ctx.Plan.Entries[len(ctx.Plan.Entries)-1], ctx.Plan.Entries[:len(ctx.Plan.Entries)-1] + }) + + it("contributes distribution for API 0.7+", func() { + ctx.Buildpack.Metadata["dependencies"] = []map[string]interface{}{ + { + "id": "maven", + "version": "1.1.1", + "stacks": []interface{}{"test-stack-id"}, + "cpes": []string{"cpe:2.3:a:apache:maven:3.8.3:*:*:*:*:*:*:*"}, + "purl": "pkg:generic/apache-maven@3.8.3", + }, + } + ctx.StackID = "test-stack-id" + + result, err := mavenBuild.Build(ctx) + Expect(err).NotTo(HaveOccurred()) + + Expect(result.Layers).To(HaveLen(2)) + Expect(result.Layers[0].Name()).To(Equal("maven")) + Expect(result.Layers[1].Name()).To(Equal("cache")) + + Expect(result.BOM.Entries).To(HaveLen(1)) + Expect(result.BOM.Entries[0].Name).To(Equal("maven")) + Expect(result.BOM.Entries[0].Build).To(BeTrue()) + Expect(result.BOM.Entries[0].Launch).To(BeFalse()) + }) + + it("contributes distribution for API <=0.6", func() { + ctx.Buildpack.Metadata["dependencies"] = []map[string]interface{}{ + { + "id": "maven", + "version": "1.1.1", + "stacks": []interface{}{"test-stack-id"}, + }, + } + ctx.StackID = "test-stack-id" + ctx.Buildpack.API = "0.6" + + result, err := mavenBuild.Build(ctx) + Expect(err).NotTo(HaveOccurred()) + + Expect(result.Layers).To(HaveLen(2)) + Expect(result.Layers[0].Name()).To(Equal("maven")) + Expect(result.Layers[1].Name()).To(Equal("cache")) + + Expect(result.BOM.Entries).To(HaveLen(1)) + Expect(result.BOM.Entries[0].Name).To(Equal("maven")) + Expect(result.BOM.Entries[0].Build).To(BeTrue()) + Expect(result.BOM.Entries[0].Launch).To(BeFalse()) + }) + }) + + context("distribute binary build with another", func() { + it.Before(func() { + entry := libcnb.BuildpackPlanEntry{ + Name: maven.PlanEntryMaven, + Metadata: map[string]interface{}{ + maven.RunBuild: true, + maven.Command: "maven", + }, + } + ctx.Plan.Entries = append(ctx.Plan.Entries, entry) + os.Setenv("BP_MAVEN_DAEMON_ENABLED", "1") + }) + + it.After(func() { + os.Unsetenv("BP_MAVEN_DAEMON_ENABLED") + _, ctx.Plan.Entries = ctx.Plan.Entries[len(ctx.Plan.Entries)-1], ctx.Plan.Entries[:len(ctx.Plan.Entries)-1] + }) + + it("contributes distribution for API 0.7+", func() { + ctx.Buildpack.Metadata["dependencies"] = []map[string]interface{}{ + { + "id": "maven", + "version": "1.1.1", + "stacks": []interface{}{"test-stack-id"}, + "cpes": []string{"cpe:2.3:a:apache:maven:3.8.3:*:*:*:*:*:*:*"}, + "purl": "pkg:generic/apache-maven@3.8.3", + }, + { + "id": "mvnd", + "version": "1.1.1", + "stacks": []interface{}{"test-stack-id"}, + }, + } + ctx.StackID = "test-stack-id" + + result, err := mavenBuild.Build(ctx) + Expect(err).NotTo(HaveOccurred()) + + Expect(result.Layers).To(HaveLen(4)) + Expect(result.Layers[0].Name()).To(Equal("maven")) + Expect(result.Layers[1].Name()).To(Equal("mvnd")) + Expect(result.Layers[2].Name()).To(Equal("cache")) + Expect(result.Layers[3].Name()).To(Equal("application")) + + Expect(result.BOM.Entries).To(HaveLen(2)) + Expect(result.BOM.Entries[0].Name).To(Equal("maven")) + Expect(result.BOM.Entries[0].Build).To(BeTrue()) + Expect(result.BOM.Entries[0].Launch).To(BeFalse()) + Expect(result.BOM.Entries[1].Name).To(Equal("mvnd")) + Expect(result.BOM.Entries[1].Build).To(BeTrue()) + Expect(result.BOM.Entries[1].Launch).To(BeFalse()) + }) + + it("contributes distribution for API <=0.6", func() { + ctx.Buildpack.Metadata["dependencies"] = []map[string]interface{}{ + { + "id": "maven", + "version": "1.1.1", + "stacks": []interface{}{"test-stack-id"}, + }, + { + "id": "mvnd", + "version": "1.1.1", + "stacks": []interface{}{"test-stack-id"}, + }, + } + ctx.StackID = "test-stack-id" + ctx.Buildpack.API = "0.6" + + result, err := mavenBuild.Build(ctx) + Expect(err).NotTo(HaveOccurred()) + + Expect(result.Layers).To(HaveLen(4)) + Expect(result.Layers[0].Name()).To(Equal("maven")) + Expect(result.Layers[1].Name()).To(Equal("mvnd")) + Expect(result.Layers[2].Name()).To(Equal("cache")) + Expect(result.Layers[3].Name()).To(Equal("application")) + + Expect(result.BOM.Entries).To(HaveLen(2)) + Expect(result.BOM.Entries[0].Name).To(Equal("maven")) + Expect(result.BOM.Entries[0].Build).To(BeTrue()) + Expect(result.BOM.Entries[0].Launch).To(BeFalse()) + Expect(result.BOM.Entries[1].Name).To(Equal("mvnd")) + Expect(result.BOM.Entries[1].Build).To(BeTrue()) + Expect(result.BOM.Entries[1].Launch).To(BeFalse()) + }) + }) } type FakeApplicationFactory struct{} diff --git a/maven/detect.go b/maven/detect.go index d259fb5..891688d 100644 --- a/maven/detect.go +++ b/maven/detect.go @@ -43,16 +43,30 @@ func (Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) return libcnb.DetectResult{}, err } + provide_maven := libcnb.DetectResult{ + Pass: true, + Plans: []libcnb.BuildPlan{ + { + Provides: []libcnb.BuildPlanProvide{ + {Name: PlanEntryMaven}, + }, + Requires: []libcnb.BuildPlanRequire{ + {Name: PlanEntryMaven}, + }, + }, + }, + } + pomFile, _ := cr.Resolve("BP_MAVEN_POM_FILE") file := filepath.Join(context.Application.Path, pomFile) _, err = os.Stat(file) if os.IsNotExist(err) { - return libcnb.DetectResult{Pass: false}, nil + return provide_maven, nil } else if err != nil { return libcnb.DetectResult{}, fmt.Errorf("unable to determine if %s exists\n%w", file, err) } - return libcnb.DetectResult{ + result := libcnb.DetectResult{ Pass: true, Plans: []libcnb.BuildPlan{ { @@ -67,5 +81,6 @@ func (Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) }, }, }, - }, nil + } + return result, nil } diff --git a/maven/detect_test.go b/maven/detect_test.go index a46fbb1..e9b8413 100644 --- a/maven/detect_test.go +++ b/maven/detect_test.go @@ -48,9 +48,21 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { Expect(os.RemoveAll(ctx.Application.Path)).To(Succeed()) }) - it("fails without pom.xml", func() { + it("provides maven without pom.xml", func() { os.Setenv("BP_MAVEN_POM_FILE", "pom.xml") - Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{})) + Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ + Pass: true, + Plans: []libcnb.BuildPlan{ + { + Provides: []libcnb.BuildPlanProvide{ + {Name: maven.PlanEntryMaven}, + }, + Requires: []libcnb.BuildPlanRequire{ + {Name: maven.PlanEntryMaven}, + }, + }, + }, + })) }) it("passes with pom.xml", func() { diff --git a/maven/distribution.go b/maven/distribution.go index 7c085a5..c02b3b1 100644 --- a/maven/distribution.go +++ b/maven/distribution.go @@ -19,6 +19,7 @@ package maven import ( "fmt" "os" + "strings" "github.com/buildpacks/libcnb" "github.com/paketo-buildpacks/libpak" @@ -29,10 +30,12 @@ import ( type Distribution struct { LayerContributor libpak.DependencyLayerContributor Logger bard.Logger + SecurityArgs []string } func NewDistribution(dependency libpak.BuildpackDependency, cache libpak.DependencyCache) (Distribution, libcnb.BOMEntry) { contributor, entry := libpak.NewDependencyLayer(dependency, cache, libcnb.LayerTypes{ + Build: true, Cache: true, }) return Distribution{LayerContributor: contributor}, entry @@ -47,6 +50,9 @@ func (d Distribution) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { return libcnb.Layer{}, fmt.Errorf("unable to expand Maven\n%w", err) } + if len(d.SecurityArgs) > 0 { + layer.BuildEnvironment.Override("MAVEN_OPTS", strings.Join(d.SecurityArgs, " ")) + } return layer, nil }) } diff --git a/maven/distribution_test.go b/maven/distribution_test.go index e8f0604..8eae9c5 100644 --- a/maven/distribution_test.go +++ b/maven/distribution_test.go @@ -56,6 +56,7 @@ func testDistribution(t *testing.T, context spec.G, it spec.S) { dc := libpak.DependencyCache{CachePath: "testdata"} d, _ := maven.NewDistribution(dep, dc) + d.SecurityArgs = []string{"test", "test"} layer, err := ctx.Layers.Layer("test-layer") Expect(err).NotTo(HaveOccurred()) @@ -64,6 +65,7 @@ func testDistribution(t *testing.T, context spec.G, it spec.S) { Expect(layer.Cache).To(BeTrue()) Expect(filepath.Join(layer.Path, "fixture-marker")).To(BeARegularFile()) + Expect(layer.BuildEnvironment["MAVEN_OPTS.override"]).To(Equal("test test")) }) } diff --git a/maven/mvnd_distribution.go b/maven/mvnd_distribution.go index e5c20e9..b8f186a 100644 --- a/maven/mvnd_distribution.go +++ b/maven/mvnd_distribution.go @@ -19,6 +19,7 @@ package maven import ( "fmt" "os" + "strings" "github.com/buildpacks/libcnb" "github.com/paketo-buildpacks/libpak" @@ -29,10 +30,12 @@ import ( type MvndDistribution struct { LayerContributor libpak.DependencyLayerContributor Logger bard.Logger + SecurityArgs []string } func NewMvndDistribution(dependency libpak.BuildpackDependency, cache libpak.DependencyCache) (MvndDistribution, libcnb.BOMEntry) { contributor, entry := libpak.NewDependencyLayer(dependency, cache, libcnb.LayerTypes{ + Build: true, Cache: true, }) return MvndDistribution{LayerContributor: contributor}, entry @@ -47,6 +50,9 @@ func (d MvndDistribution) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { return libcnb.Layer{}, fmt.Errorf("unable to expand Maven\n%w", err) } + if len(d.SecurityArgs) > 0 { + layer.BuildEnvironment.Override("MAVEN_OPTS", strings.Join(d.SecurityArgs, " ")) + } return layer, nil }) } diff --git a/maven/mvnd_distribution_test.go b/maven/mvnd_distribution_test.go index a4c496c..8e6137a 100644 --- a/maven/mvnd_distribution_test.go +++ b/maven/mvnd_distribution_test.go @@ -56,6 +56,7 @@ func testMvndDistribution(t *testing.T, context spec.G, it spec.S) { dc := libpak.DependencyCache{CachePath: "testdata"} d, _ := maven.NewMvndDistribution(dep, dc) + d.SecurityArgs = []string{"test", "test"} layer, err := ctx.Layers.Layer("test-layer") Expect(err).NotTo(HaveOccurred()) @@ -64,6 +65,7 @@ func testMvndDistribution(t *testing.T, context spec.G, it spec.S) { Expect(layer.Cache).To(BeTrue()) Expect(filepath.Join(layer.Path, "fixture-marker")).To(BeARegularFile()) + Expect(layer.BuildEnvironment["MAVEN_OPTS.override"]).To(Equal("test test")) }) }