diff --git a/README.md b/README.md index b3b6727..e46b309 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 OR if `$BP_MAVEN_ACTIVE_PROFILES` contains the `native` profile: + * A build plan entry is provided, `native-image-application`, which can be required by the `native-image` [buildpack](https://github.com/paketo-buildpacks/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..3aa900d 100644 --- a/boot/detect.go +++ b/boot/detect.go @@ -17,29 +17,96 @@ package boot import ( + "fmt" "github.com/buildpacks/libcnb" + "github.com/magiconair/properties" + "github.com/paketo-buildpacks/libjvm" + "github.com/paketo-buildpacks/libpak" + "github.com/paketo-buildpacks/libpak/bard" + "regexp" + "strconv" + "strings" ) const ( - PlanEntrySpringBoot = "spring-boot" - PlanEntryJVMApplication = "jvm-application" + PlanEntrySpringBoot = "spring-boot" + PlanEntryJVMApplication = "jvm-application" + PlanEntryNativeProcessed = "native-processed" + MavenConfigActiveProfiles = "BP_MAVEN_ACTIVE_PROFILES" ) -type Detect struct{} +type Detect struct { + Logger bard.Logger +} -func (Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) { - return libcnb.DetectResult{ +func (d Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) { + 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) + } + + cr, err := libpak.NewConfigurationResolver(context.Buildpack, nil) + if err != nil { + return libcnb.DetectResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err) + } + + mavenNativeProfileDetected := isMavenNativeProfileDetected(&cr, &d.Logger) + springBootNativeProcessedDetected := isSpringBootNativeProcessedDetected(manifest, &d.Logger) + + if springBootNativeProcessedDetected || mavenNativeProfileDetected { + 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 +} + +func isSpringBootNativeProcessedDetected(manifest *properties.Properties, logger *bard.Logger) bool { + springBootNativeProcessedString, found := manifest.Get("Spring-Boot-Native-Processed") + springBootNativeProcessed, _ := strconv.ParseBool(springBootNativeProcessedString) + detected := found && springBootNativeProcessed + if detected { + logger.Bodyf("Spring-Boot-Native-Processed MANIFEST entry was detected, activating native image") + } + return detected +} + +func isMavenNativeProfileDetected(cr *libpak.ConfigurationResolver, logger *bard.Logger) bool { + mavenActiveProfiles, _ := cr.Resolve(MavenConfigActiveProfiles) + mavenActiveProfilesAsSlice := strings.Split(mavenActiveProfiles, ",") + r, _ := regexp.Compile("^native$|^\\?native$") + + for _, profile := range mavenActiveProfilesAsSlice { + if r.MatchString(profile) { + logger.Bodyf("Maven native profile was detected in %s, activating native image", MavenConfigActiveProfiles) + return true + } + } + return false } diff --git a/boot/detect_test.go b/boot/detect_test.go index 7e10ca7..0f5373d 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,21 +36,83 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { detect boot.Detect ) - it("always passes", func() { - Expect(detect.Detect(ctx)).To(Equal(libcnb.DetectResult{ - Pass: true, - Plans: []libcnb.BuildPlan{ - { - Provides: []libcnb.BuildPlanProvide{ - {Name: "spring-boot"}, - }, - Requires: []libcnb.BuildPlanRequire{ - {Name: "jvm-application"}, - {Name: "spring-boot"}, - }, + nativeResult := 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"}, + }, + }, + }, + } + + normalResult := libcnb.DetectResult{ + Pass: true, + Plans: []libcnb.BuildPlan{ + { + Provides: []libcnb.BuildPlanProvide{ + {Name: "spring-boot"}, + }, + Requires: []libcnb.BuildPlanRequire{ + {Name: "jvm-application"}, + {Name: "spring-boot"}, }, }, - })) + }, + } + + it("always passes for standard build", func() { + Expect(os.Unsetenv("BP_MAVEN_ACTIVE_PROFILES")).To(Succeed()) + Expect(os.RemoveAll(filepath.Join(ctx.Application.Path, "META-INF"))).To(Succeed()) + Expect(detect.Detect(ctx)).To(Equal(normalResult)) + }) + + it("always passes for native build", func() { + Expect(os.Unsetenv("BP_MAVEN_ACTIVE_PROFILES")).To(Succeed()) + 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(nativeResult)) + }) + + it("using BP_MAVEN_ACTIVE_PROFILES", func() { + + Expect(os.RemoveAll(filepath.Join(ctx.Application.Path, "META-INF"))).To(Succeed()) + + Expect(os.Setenv("BP_MAVEN_ACTIVE_PROFILES", "native")).To(Succeed()) + Expect(detect.Detect(ctx)).To(Equal(nativeResult)) + + Expect(os.Setenv("BP_MAVEN_ACTIVE_PROFILES", "p1,native")).To(Succeed()) + Expect(detect.Detect(ctx)).To(Equal(nativeResult)) + + Expect(os.Setenv("BP_MAVEN_ACTIVE_PROFILES", "p1,?native")).To(Succeed()) + Expect(detect.Detect(ctx)).To(Equal(nativeResult)) + + Expect(os.Setenv("BP_MAVEN_ACTIVE_PROFILES", "native,p1")).To(Succeed()) + Expect(detect.Detect(ctx)).To(Equal(nativeResult)) + + Expect(os.Setenv("BP_MAVEN_ACTIVE_PROFILES", "?native")).To(Succeed()) + Expect(detect.Detect(ctx)).To(Equal(nativeResult)) + + Expect(os.Setenv("BP_MAVEN_ACTIVE_PROFILES", "mynative,native")).To(Succeed()) + Expect(detect.Detect(ctx)).To(Equal(nativeResult)) + + Expect(os.Setenv("BP_MAVEN_ACTIVE_PROFILES", "mynative")).To(Succeed()) + Expect(detect.Detect(ctx)).To(Equal(normalResult)) + + Expect(os.Setenv("BP_MAVEN_ACTIVE_PROFILES", "!native")).To(Succeed()) + Expect(detect.Detect(ctx)).To(Equal(normalResult)) + }) } diff --git a/boot/native_image.go b/boot/native_image.go index 671ce6a..6b3ce3e 100644 --- a/boot/native_image.go +++ b/boot/native_image.go @@ -68,10 +68,9 @@ func (n NativeImageClasspath) Contribute(layer libcnb.Layer) (libcnb.Layer, erro ) nativeImageArgFile := filepath.Join(n.ApplicationPath, "META-INF", "native-image", "argfile") - if exists, err := sherpa.Exists(nativeImageArgFile); err != nil{ + 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)) + } else if exists { layer.BuildEnvironment.Default("BP_NATIVE_IMAGE_BUILD_ARGUMENTS_FILE", nativeImageArgFile) } diff --git a/cmd/main/main.go b/cmd/main/main.go index 6fb5946..4810842 100644 --- a/cmd/main/main.go +++ b/cmd/main/main.go @@ -26,8 +26,9 @@ import ( ) func main() { + logger := bard.NewLogger(os.Stdout) libpak.Main( - boot.Detect{}, - boot.Build{Logger: bard.NewLogger(os.Stdout)}, + boot.Detect{Logger: logger}, + boot.Build{Logger: logger}, ) }