diff --git a/buildpack.toml b/buildpack.toml index 68d0477..66b679b 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.dependencies]] cpes = ["cpe:2.3:a:apache:maven:3.8.6:*:*:*:*:*:*:*"] diff --git a/maven/build.go b/maven/build.go index 4968664..86a5293 100644 --- a/maven/build.go +++ b/maven/build.go @@ -42,6 +42,13 @@ const ( RunBuild = "RunBuild" ) +var ( + artifacts = map[string]string{ + "mvn": "maven", + "mvnd": "mvnd", + } +) + type Build struct { Logger bard.Logger ApplicationFactory ApplicationFactory @@ -53,26 +60,39 @@ 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) (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 + dep, err := dr.Resolve(artifact, "") + if err != nil { + return "", nil, libcnb.BOMEntry{}, fmt.Errorf("unable to find dependency\n%w", err) + } + + if artifact == "maven" { + dist, be := NewDistribution(dep, dc) + dist.Logger = b.Logger + command := filepath.Join(context.Layers.Path, dist.Name(), "bin", "mvn") + return command, dist, be, nil + } + dist, be := NewDistribution(dep, dc) + dist.Logger = b.Logger + command := filepath.Join(context.Layers.Path, dist.Name(), "bin", "mvnd") + return command, dist, be, nil +} + +func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { + b.Logger.Title(context.Buildpack) + result := libcnb.NewBuildResult() + pr := libpak.PlanEntryResolver{ Plan: context.Plan, } @@ -84,45 +104,24 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { } } - bpMavenCommand, _ := cr.Resolve(BpMavenCommand) - command := "" - if cr.ResolveBool("BP_MAVEN_DAEMON_ENABLED") || bpMavenCommand == "mvnd" { - dep, err := dr.Resolve("mvnd", "") - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to find dependency\n%w", err) - } - - dist, be := NewMvndDistribution(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", "mvnd") - } else { - command = filepath.Join(context.Application.Path, "mvnw") - if _, err := os.Stat(command); os.IsNotExist(err) || bpMavenCommand == "mvn" { - 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) + cr, err := libpak.NewConfigurationResolver(context.Buildpack, &b.Logger) + if err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err) + } - 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) - } + bpMavenCommand, _ := cr.Resolve(BpMavenCommand) + // no install requested and no build requested + if bpMavenCommand == "" && runBuild == false { + return libcnb.BuildResult{}, nil + } - if err = b.CleanMvnWrapper(command); err != nil { - b.Logger.Bodyf("WARNING: unable to clean mvnw file: %s\n%s", command, err) - } + if bpMavenCommand == "mvn" || bpMavenCommand == "mvnd" { + cmd, layer, bomEntry, err := install(b, context, artifacts[bpMavenCommand]) + 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) } u, err := user.Current() @@ -132,9 +131,42 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { c := libbs.Cache{Path: filepath.Join(u.HomeDir, ".m2")} c.Logger = b.Logger - result.Layers = append(result.Layers, c) if runBuild { + command := "" + if cr.ResolveBool("BP_MAVEN_DAEMON_ENABLED") && bpMavenCommand != "mvnd" { + cmd, layer, bomEntry, err := install(b, context, "mvnd") + 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) && bpMavenCommand != "mvn" { + cmd, layer, bomEntry, err := install(b, context, "maven") + 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) @@ -185,7 +217,10 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { 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 496e44a..f6b12b3 100644 --- a/maven/build_test.go +++ b/maven/build_test.go @@ -458,6 +458,12 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { }, } ctx.Plan.Entries = append(ctx.Plan.Entries, entry) + os.Setenv("BP_MAVEN_COMMAND", "mvn") + }) + + it.After(func() { + os.Unsetenv("BP_MAVEN_COMMAND") + _, 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() { @@ -509,6 +515,95 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { 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, + }, + } + ctx.Plan.Entries = append(ctx.Plan.Entries, entry) + os.Setenv("BP_MAVEN_COMMAND", "mvn") + os.Setenv("BP_MAVEN_DAEMON_ENABLED", "1") + }) + + it.After(func() { + os.Unsetenv("BP_MAVEN_COMMAND") + 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{}