diff --git a/README.md b/README.md index b3b6727..d40fa2a 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ The buildpack will do the following: * Contributes application slices as defined by the layer's index * If the application is a reactive web application * Configures `$BPL_JVM_THREAD_COUNT` to 50 +* If `/META-INF/MANIFEST.MF` contains a `Spring-Boot-Native-Processed` entry: + * A build plan entry is provided, `native-image-application`, which can be required by `native-image` to automatically trigger a native image build * When contributing to a native image application: * Adds classes from the executable JAR and entries from `classpath.idx` to the build-time class path, so they are available to `native-image` diff --git a/boot/build.go b/boot/build.go index 7913e4f..604fff0 100644 --- a/boot/build.go +++ b/boot/build.go @@ -121,6 +121,12 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { } } + if _, ok, err := pr.Resolve("native-processed"); err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to resolve native-processed plan entry\n%w", err) + } else if ok { + buildNativeImage = true + } + if buildNativeImage { // set CLASSPATH for native image build classpathLayer, err := NewNativeImageClasspath(context.Application.Path, manifest) diff --git a/boot/build_test.go b/boot/build_test.go index 9eb3aea..b03c539 100644 --- a/boot/build_test.go +++ b/boot/build_test.go @@ -318,6 +318,31 @@ Spring-Boot-Lib: BOOT-INF/lib }) }) + context("when a native-processed BuildPlanEntry is found with a native-image sub entry", func() { + it.Before(func() { + ctx.Plan.Entries = append(ctx.Plan.Entries, libcnb.BuildpackPlanEntry{ + Name: "native-processed", + Metadata: map[string]interface{}{"native-image": true}, + }) + }) + + it("contributes a native image build", func() { + Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` +Spring-Boot-Version: 1.1.1 +Spring-Boot-Classes: BOOT-INF/classes +Spring-Boot-Lib: BOOT-INF/lib +`), 0644)).To(Succeed()) + + result, err := build.Build(ctx) + Expect(err).NotTo(HaveOccurred()) + + Expect(result.Layers).To(HaveLen(1)) + Expect(result.Layers[0].Name()).To(Equal("Class Path")) + Expect(result.Slices).To(HaveLen(0)) + }) + + }) + context("set BP_SPRING_CLOUD_BINDINGS_DISABLED to true", func() { it.Before(func() { Expect(os.Setenv("BP_SPRING_CLOUD_BINDINGS_DISABLED", "true")).To(Succeed()) diff --git a/boot/detect.go b/boot/detect.go index 1e48747..3eea601 100644 --- a/boot/detect.go +++ b/boot/detect.go @@ -17,29 +17,58 @@ package boot import ( + "fmt" "github.com/buildpacks/libcnb" + "github.com/paketo-buildpacks/libjvm" + "strconv" ) const ( - PlanEntrySpringBoot = "spring-boot" - PlanEntryJVMApplication = "jvm-application" + PlanEntrySpringBoot = "spring-boot" + PlanEntryJVMApplication = "jvm-application" + PlanEntryNativeProcessed = "native-processed" ) type Detect struct{} func (Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) { - return libcnb.DetectResult{ + result := libcnb.DetectResult{ Pass: true, Plans: []libcnb.BuildPlan{ { Provides: []libcnb.BuildPlanProvide{ - {Name: "spring-boot"}, + {Name: PlanEntrySpringBoot}, }, Requires: []libcnb.BuildPlanRequire{ - {Name: "jvm-application"}, - {Name: "spring-boot"}, + {Name: PlanEntryJVMApplication}, + {Name: PlanEntrySpringBoot}, }, }, }, - }, nil + } + manifest, err := libjvm.NewManifest(context.Application.Path) + if err != nil { + return libcnb.DetectResult{}, fmt.Errorf("unable to read manifest in %s\n%w", context.Application.Path, err) + } + + springBootNativeProcessedString, ok := manifest.Get("Spring-Boot-Native-Processed") + springBootNativeProcessed, err := strconv.ParseBool(springBootNativeProcessedString) + if ok && springBootNativeProcessed { + result = libcnb.DetectResult{ + Pass: true, + Plans: []libcnb.BuildPlan{ + { + Provides: []libcnb.BuildPlanProvide{ + {Name: PlanEntrySpringBoot}, + {Name: PlanEntryNativeProcessed}, + }, + Requires: []libcnb.BuildPlanRequire{ + {Name: PlanEntryJVMApplication}, + {Name: PlanEntrySpringBoot}, + }, + }, + }, + } + } + return result, nil } diff --git a/boot/detect_test.go b/boot/detect_test.go index 7e10ca7..6992f1f 100644 --- a/boot/detect_test.go +++ b/boot/detect_test.go @@ -17,6 +17,8 @@ package boot_test import ( + "os" + "path/filepath" "testing" "github.com/buildpacks/libcnb" @@ -34,7 +36,8 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { detect boot.Detect ) - it("always passes", func() { + it("always passes for standard build", func() { + Expect(os.RemoveAll(filepath.Join(ctx.Application.Path, "META-INF"))).To(Succeed()) Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ Pass: true, Plans: []libcnb.BuildPlan{ @@ -51,4 +54,29 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { })) }) + it("always passes for native build", func() { + Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "META-INF"), 0755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` +Spring-Boot-Version: 1.1.1 +Spring-Boot-Classes: BOOT-INF/classes +Spring-Boot-Lib: BOOT-INF/lib +Spring-Boot-Native-Processed: true +`), 0644)).To(Succeed()) + Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ + Pass: true, + Plans: []libcnb.BuildPlan{ + { + Provides: []libcnb.BuildPlanProvide{ + {Name: "spring-boot"}, + {Name: "native-processed"}, + }, + Requires: []libcnb.BuildPlanRequire{ + {Name: "jvm-application"}, + {Name: "spring-boot"}, + }, + }, + }, + })) + }) + } diff --git a/boot/native_image.go b/boot/native_image.go index 671ce6a..3a36976 100644 --- a/boot/native_image.go +++ b/boot/native_image.go @@ -17,7 +17,6 @@ package boot import ( "fmt" - "github.com/paketo-buildpacks/libpak/sherpa" "os" "path/filepath" "strings" @@ -66,15 +65,6 @@ func (n NativeImageClasspath) Contribute(layer libcnb.Layer) (libcnb.Layer, erro string(filepath.ListSeparator), strings.Join(cp, string(filepath.ListSeparator)), ) - - nativeImageArgFile := filepath.Join(n.ApplicationPath, "META-INF", "native-image", "argfile") - if exists, err := sherpa.Exists(nativeImageArgFile); err != nil{ - return libcnb.Layer{}, fmt.Errorf("unable to check for native-image arguments file at %s\n%w", nativeImageArgFile, err) - } else if exists{ - lc.Logger.Bodyf(fmt.Sprintf("native args file %s", nativeImageArgFile)) - layer.BuildEnvironment.Default("BP_NATIVE_IMAGE_BUILD_ARGUMENTS_FILE", nativeImageArgFile) - } - return layer, nil }) } diff --git a/boot/native_image_test.go b/boot/native_image_test.go index c825e4f..5a78ee7 100644 --- a/boot/native_image_test.go +++ b/boot/native_image_test.go @@ -116,24 +116,4 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) { Expect(layer.LayerTypes.Launch).To(BeFalse()) }) }) - context("Boot @argfile is found", func() { - it.Before(func() { - Expect(ioutil.WriteFile(filepath.Join(appDir, "BOOT-INF", "classpath.idx"), []byte(` -- "some.jar" -- "other.jar" -`), 0644)).To(Succeed()) - Expect(os.MkdirAll(filepath.Join(appDir, "META-INF", "native-image"), 0755)).To(Succeed()) - Expect(ioutil.WriteFile(filepath.Join(appDir, "META-INF", "native-image", "argfile"), []byte("file-data"), 0644)).To(Succeed()) - }) - - it("ensures BP_NATIVE_IMAGE_BUILD_ARGUMENTS_FILE is set when argfile is found", func() { - layer, err := contributor.Contribute(layer) - Expect(err).NotTo(HaveOccurred()) - - Expect(layer.BuildEnvironment["BP_NATIVE_IMAGE_BUILD_ARGUMENTS_FILE.default"]).To(Equal( - filepath.Join(appDir, "META-INF", "native-image", "argfile"))) - Expect(layer.LayerTypes.Build).To(BeTrue()) - Expect(layer.LayerTypes.Launch).To(BeFalse()) - }) - }) }