From 9d3b6b3e111eb3cc98f4abf12c6d4d71b80c8073 Mon Sep 17 00:00:00 2001 From: Juan Bustamante Date: Fri, 3 Dec 2021 09:06:33 -0500 Subject: [PATCH 1/3] WIP - Adding support for creating a native-image using -jar as an option of a Main Class Signed-off-by: Juan Bustamante --- buildpack.toml | 5 ++ native/build.go | 34 +++++++----- native/native_image.go | 49 ++++++++++-------- native/native_image_options.go | 94 ++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 35 deletions(-) create mode 100644 native/native_image_options.go diff --git a/buildpack.toml b/buildpack.toml index 1ee2653..a52cc2e 100644 --- a/buildpack.toml +++ b/buildpack.toml @@ -46,6 +46,11 @@ name = "BP_NATIVE_IMAGE_BUILD_ARGUMENTS" description = "arguments to pass to the native-image command" build = true +[[metadata.configurations]] +name = "BP_NATIVE_IMAGE_BUILD_FROM_JAR" +description = "name of the jar file (without .jar) to pass to the native-image command" +build = true + [metadata] pre-package = "scripts/build.sh" include-files = [ diff --git a/native/build.go b/native/build.go index 2fc2242..0b6e249 100644 --- a/native/build.go +++ b/native/build.go @@ -19,10 +19,8 @@ package native import ( "fmt" "path/filepath" - "github.com/paketo-buildpacks/libpak/effect" "github.com/paketo-buildpacks/libpak/sbom" - "github.com/buildpacks/libcnb" "github.com/heroku/color" "github.com/magiconair/properties" @@ -47,16 +45,27 @@ type Build struct { func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { b.Logger.Title(context.Buildpack) result := libcnb.NewBuildResult() - - manifest, err := libjvm.NewManifest(context.Application.Path) - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to read manifest in %s\n%w", context.Application.Path, err) - } + var manifest *properties.Properties + var startClass string + var buildFromJar bool cr, err := libpak.NewConfigurationResolver(context.Buildpack, &b.Logger) if err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err) } + + startClass, buildFromJar = cr.Resolve("BP_NATIVE_IMAGE_BUILD_FROM_JAR"); + if !buildFromJar { + manifest, err = libjvm.NewManifest(context.Application.Path) + if err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to read manifest in %s\n%w", context.Application.Path, err) + } + startClass, err = findStartOrMainClass(manifest) + if err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to find required manifest property\n%w", err) + } + } + if _, ok := cr.Resolve(DeprecatedConfigNativeImage); ok { b.warn(fmt.Sprintf("$%s has been deprecated. Please use $%s instead.", DeprecatedConfigNativeImage, @@ -84,18 +93,17 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { } } - n, err := NewNativeImage(context.Application.Path, args, compressor, manifest, context.StackID) + var nativeImageOptions []NativeImageOption + if buildFromJar { + nativeImageOptions = append(nativeImageOptions, WithJar(startClass)) + } + n, err := NewNativeImage(context.Application.Path, args, compressor, manifest, context.StackID, nativeImageOptions...) if err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to create native image layer\n%w", err) } n.Logger = b.Logger result.Layers = append(result.Layers, n) - startClass, err := findStartOrMainClass(manifest) - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to find required manifest property\n%w", err) - } - command := filepath.Join(context.Application.Path, startClass) result.Processes = append(result.Processes, libcnb.Process{Type: "native-image", Command: command, Direct: true}, diff --git a/native/native_image.go b/native/native_image.go index 071bcfd..36aac06 100644 --- a/native/native_image.go +++ b/native/native_image.go @@ -38,33 +38,44 @@ type NativeImage struct { Arguments []string Executor effect.Executor Logger bard.Logger - Manifest *properties.Properties + nativeMain nativeMain StackID string Compressor string } -func NewNativeImage(applicationPath string, arguments string, compressor string, manifest *properties.Properties, stackID string) (NativeImage, error) { +func NewNativeImage(applicationPath string, arguments string, compressor string, manifest *properties.Properties, stackID string, ops ...NativeImageOption) (NativeImage, error) { var err error - + var nativeMain nativeMain args, err := shellwords.Parse(arguments) if err != nil { return NativeImage{}, fmt.Errorf("unable to parse arguments from %s\n%w", arguments, err) } + nativeImageOpts := &options{} + for _, op := range ops { + if err := op(nativeImageOpts); err != nil { + return NativeImage{}, err + } + } + nativeMain = newStartClassMain(applicationPath, manifest) + if nativeImageOpts.jarFileName != "" { + nativeMain = newJarFileMain(applicationPath, nativeImageOpts.jarFileName) + } + return NativeImage{ ApplicationPath: applicationPath, Arguments: args, Executor: effect.NewExecutor(), - Manifest: manifest, + nativeMain: nativeMain, StackID: stackID, Compressor: compressor, }, nil } func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { - startClass, err := findStartOrMainClass(n.Manifest) + name, err := n.nativeMain.Name() if err != nil { - return libcnb.Layer{}, fmt.Errorf("unable to find required manifest property\n%w", err) + return libcnb.Layer{}, fmt.Errorf("unable to determine main class\n%w", err) } arguments := n.Arguments @@ -73,20 +84,13 @@ func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { arguments = append(arguments, "-H:+StaticExecutableWithDynamicLibC") } - cp := os.Getenv("CLASSPATH") - if cp == "" { - // CLASSPATH should have been done by upstream buildpacks, but just in case - cp = n.ApplicationPath - if v, ok := n.Manifest.Get("Class-Path"); ok { - cp = strings.Join([]string{cp, v}, string(filepath.ListSeparator)) - } - } + cp := n.nativeMain.ClassPath() arguments = append(arguments, - fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, startClass)), + fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, name)), "-cp", cp, - startClass, ) + arguments = append(arguments, n.nativeMain.Arguments()...) files, err := sherpa.NewFileListing(n.ApplicationPath) if err != nil { @@ -114,6 +118,7 @@ func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { } n.Logger.Bodyf("Executing native-image %s", strings.Join(arguments, " ")) + fmt.Printf("----- ARGUMENTS %s\n", strings.Join(arguments, " ")) if err := n.Executor.Execute(effect.Execution{ Command: "native-image", Args: arguments, @@ -128,7 +133,7 @@ func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { n.Logger.Bodyf("Executing %s to compress native image", n.Compressor) if err := n.Executor.Execute(effect.Execution{ Command: "upx", - Args: []string{"-q", "-9", filepath.Join(layer.Path, startClass)}, + Args: []string{"-q", "-9", filepath.Join(layer.Path, name)}, Dir: layer.Path, Stdout: n.Logger.InfoWriter(), Stderr: n.Logger.InfoWriter(), @@ -139,7 +144,7 @@ func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { n.Logger.Bodyf("Executing %s to compress native image", n.Compressor) if err := n.Executor.Execute(effect.Execution{ Command: "gzexe", - Args: []string{filepath.Join(layer.Path, startClass)}, + Args: []string{filepath.Join(layer.Path, name)}, Dir: layer.Path, Stdout: n.Logger.InfoWriter(), Stderr: n.Logger.InfoWriter(), @@ -147,7 +152,7 @@ func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { return libcnb.Layer{}, fmt.Errorf("error compressing\n%w", err) } - if err := os.Remove(filepath.Join(layer.Path, fmt.Sprintf("%s~", startClass))); err != nil { + if err := os.Remove(filepath.Join(layer.Path, fmt.Sprintf("%s~", name))); err != nil { return libcnb.Layer{}, fmt.Errorf("error removing\n%w", err) } } @@ -170,14 +175,14 @@ func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { } } - src := filepath.Join(layer.Path, startClass) + src := filepath.Join(layer.Path, name) in, err := os.Open(src) if err != nil { - return libcnb.Layer{}, fmt.Errorf("unable to open %s\n%w", filepath.Join(layer.Path, startClass), err) + return libcnb.Layer{}, fmt.Errorf("unable to open %s\n%w", filepath.Join(layer.Path, name), err) } defer in.Close() - dst := filepath.Join(n.ApplicationPath, startClass) + dst := filepath.Join(n.ApplicationPath, name) out, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755) if err != nil { return libcnb.Layer{}, fmt.Errorf("unable to open %s\n%w", dst, err) diff --git a/native/native_image_options.go b/native/native_image_options.go new file mode 100644 index 0000000..f9f6d39 --- /dev/null +++ b/native/native_image_options.go @@ -0,0 +1,94 @@ +package native + +import ( + "fmt" + "github.com/magiconair/properties" + "os" + "path/filepath" + "strings" +) + +type NativeImageOption func(*options) error + +type options struct { + jarFileName string +} + +type nativeMain interface { + Name() (string, error) + Arguments() []string + ClassPath() string +} + +type startClassMain struct { + ApplicationPath string + Manifest *properties.Properties + startClass string +} + +type jarFileMain struct { + jarFileName string + jarFile string +} + +func newStartClassMain(path string, manifest *properties.Properties) *startClassMain { + return &startClassMain{ + ApplicationPath: path, + Manifest: manifest, + } +} + +func (m *startClassMain) Name() (string, error) { + var err error + if m.startClass == "" { + m.startClass, err = findStartOrMainClass(m.Manifest) + if err != nil { + return "", fmt.Errorf("unable to find required manifest property\n%w", err) + } + } + return m.startClass, nil +} + +func (m *startClassMain) Arguments() []string { + return []string {m.startClass} +} + +func (m *startClassMain) ClassPath() string { + cp := os.Getenv("CLASSPATH") + if cp == "" { + // CLASSPATH should have been done by upstream buildpacks, but just in case + cp = m.ApplicationPath + if v, ok := m.Manifest.Get("Class-Path"); ok { + cp = strings.Join([]string{cp, v}, string(filepath.ListSeparator)) + } + } + return cp +} + +func WithJar(fileName string) NativeImageOption { + return func(i *options) error { + i.jarFileName = fileName + return nil + } +} + +func newJarFileMain (path string, fileName string) *jarFileMain { + jarFile := filepath.Join(path, fmt.Sprintf("%s.jar", fileName)) + return &jarFileMain{ + jarFileName: fileName, + jarFile: jarFile, + } +} + +func (m *jarFileMain) Name() (string, error ) { + return m.jarFileName, nil +} + +func (m *jarFileMain) Arguments() []string { + return []string {"-jar", m.jarFile} +} + +func (m *jarFileMain) ClassPath() string { + // TODO I still have doubts if this could defined by user or determined programmatically + return "workspace:/lib" +} From 31a1825d036686e1afce4081c5eb611c2d4a39f6 Mon Sep 17 00:00:00 2001 From: Juan Bustamante Date: Mon, 24 Jan 2022 15:18:22 -0500 Subject: [PATCH 2/3] WIP - fixing some issues from the review\n - Removing the nativeImageOptions because of just one parameter doen't worth it\n Signed-off-by: Juan Bustamante --- buildpack.toml | 4 +- native/build.go | 33 ++++----------- native/native_image.go | 18 ++++---- ...native_image_options.go => native_main.go} | 42 ++++++++++--------- 4 files changed, 41 insertions(+), 56 deletions(-) rename native/{native_image_options.go => native_main.go} (63%) diff --git a/buildpack.toml b/buildpack.toml index a52cc2e..d1bf0ca 100644 --- a/buildpack.toml +++ b/buildpack.toml @@ -47,8 +47,8 @@ description = "arguments to pass to the native-image command" build = true [[metadata.configurations]] -name = "BP_NATIVE_IMAGE_BUILD_FROM_JAR" -description = "name of the jar file (without .jar) to pass to the native-image command" +name = "BP_NATIVE_IMAGE_BUILD_JAR" +description = "jar file path to pass to the native-image command. Example (native-sources/my-app.jar)" build = true [metadata] diff --git a/native/build.go b/native/build.go index 0b6e249..58b3ff8 100644 --- a/native/build.go +++ b/native/build.go @@ -45,25 +45,19 @@ type Build struct { func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { b.Logger.Title(context.Buildpack) result := libcnb.NewBuildResult() - var manifest *properties.Properties - var startClass string - var buildFromJar bool cr, err := libpak.NewConfigurationResolver(context.Buildpack, &b.Logger) if err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err) } - startClass, buildFromJar = cr.Resolve("BP_NATIVE_IMAGE_BUILD_FROM_JAR"); + jarFile, buildFromJar := cr.Resolve("BP_NATIVE_IMAGE_BUILD_JAR"); + var manifest *properties.Properties if !buildFromJar { manifest, err = libjvm.NewManifest(context.Application.Path) if err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to read manifest in %s\n%w", context.Application.Path, err) } - startClass, err = findStartOrMainClass(manifest) - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to find required manifest property\n%w", err) - } } if _, ok := cr.Resolve(DeprecatedConfigNativeImage); ok { @@ -93,14 +87,16 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { } } - var nativeImageOptions []NativeImageOption - if buildFromJar { - nativeImageOptions = append(nativeImageOptions, WithJar(startClass)) - } - n, err := NewNativeImage(context.Application.Path, args, compressor, manifest, context.StackID, nativeImageOptions...) + n, err := NewNativeImage(context.Application.Path, args, compressor, manifest, context.StackID, jarFile) if err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to create native image layer\n%w", err) } + + startClass, err := n.nativeMain.Name() + if err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to determine the main or start class\n%w", err) + } + n.Logger = b.Logger result.Layers = append(result.Layers, n) @@ -129,14 +125,3 @@ func (b Build) warn(msg string) { msg, ) } - -func findStartOrMainClass(manifest *properties.Properties) (string, error) { - startClass, ok := manifest.Get("Start-Class") - if !ok { - startClass, ok = manifest.Get("Main-Class") - if !ok { - return "", fmt.Errorf("unable to read Start-Class or Main-Class from MANIFEST.MF") - } - } - return startClass, nil -} diff --git a/native/native_image.go b/native/native_image.go index 36aac06..da30bd2 100644 --- a/native/native_image.go +++ b/native/native_image.go @@ -43,23 +43,19 @@ type NativeImage struct { Compressor string } -func NewNativeImage(applicationPath string, arguments string, compressor string, manifest *properties.Properties, stackID string, ops ...NativeImageOption) (NativeImage, error) { - var err error - var nativeMain nativeMain +func NewNativeImage(applicationPath string, arguments string, compressor string, manifest *properties.Properties, stackID string, jarFile string) (NativeImage, error) { args, err := shellwords.Parse(arguments) if err != nil { return NativeImage{}, fmt.Errorf("unable to parse arguments from %s\n%w", arguments, err) } - nativeImageOpts := &options{} - for _, op := range ops { - if err := op(nativeImageOpts); err != nil { - return NativeImage{}, err - } - } + var nativeMain nativeMain nativeMain = newStartClassMain(applicationPath, manifest) - if nativeImageOpts.jarFileName != "" { - nativeMain = newJarFileMain(applicationPath, nativeImageOpts.jarFileName) + if jarFile != "" { + nativeMain, err = newJarFileMain(applicationPath, jarFile) + if err != nil { + return NativeImage{}, fmt.Errorf("unable to parse the native jar file\n%w", err) + } } return NativeImage{ diff --git a/native/native_image_options.go b/native/native_main.go similarity index 63% rename from native/native_image_options.go rename to native/native_main.go index f9f6d39..59680da 100644 --- a/native/native_image_options.go +++ b/native/native_main.go @@ -8,12 +8,6 @@ import ( "strings" ) -type NativeImageOption func(*options) error - -type options struct { - jarFileName string -} - type nativeMain interface { Name() (string, error) Arguments() []string @@ -29,6 +23,7 @@ type startClassMain struct { type jarFileMain struct { jarFileName string jarFile string + jarFilePath string } func newStartClassMain(path string, manifest *properties.Properties) *startClassMain { @@ -65,19 +60,18 @@ func (m *startClassMain) ClassPath() string { return cp } -func WithJar(fileName string) NativeImageOption { - return func(i *options) error { - i.jarFileName = fileName - return nil +func newJarFileMain (path string, file string) (*jarFileMain, error) { + jarFile := filepath.Base(file) + jarFilePath := filepath.Join(path, filepath.Dir(file)) + if ".jar" != filepath.Ext(jarFile) { + return &jarFileMain{}, fmt.Errorf("file %s has not a jar extension\n", jarFile) } -} - -func newJarFileMain (path string, fileName string) *jarFileMain { - jarFile := filepath.Join(path, fmt.Sprintf("%s.jar", fileName)) + jarFileName := strings.TrimSuffix(jarFile, ".jar") return &jarFileMain{ - jarFileName: fileName, + jarFileName: jarFileName, jarFile: jarFile, - } + jarFilePath: jarFilePath, + }, nil } func (m *jarFileMain) Name() (string, error ) { @@ -85,10 +79,20 @@ func (m *jarFileMain) Name() (string, error ) { } func (m *jarFileMain) Arguments() []string { - return []string {"-jar", m.jarFile} + return []string {"-jar", filepath.Join(m.jarFilePath, m.jarFile)} } func (m *jarFileMain) ClassPath() string { - // TODO I still have doubts if this could defined by user or determined programmatically - return "workspace:/lib" + return fmt.Sprintf("%s:/%s", m.jarFilePath, "lib") +} + +func findStartOrMainClass(manifest *properties.Properties) (string, error) { + startClass, ok := manifest.Get("Start-Class") + if !ok { + startClass, ok = manifest.Get("Main-Class") + if !ok { + return "", fmt.Errorf("unable to read Start-Class or Main-Class from MANIFEST.MF") + } + } + return startClass, nil } From d0242afe9c12a77c8aa0a26d47d99e67e0f51558 Mon Sep 17 00:00:00 2001 From: Juan Bustamante Date: Wed, 26 Jan 2022 16:16:48 -0500 Subject: [PATCH 3/3] WIP - fixing broken test and adding a test case for build with jar Signed-off-by: Juan Bustamante --- native/build_test.go | 23 +++++++++++++++++++++++ native/native_image_test.go | 3 +-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/native/build_test.go b/native/build_test.go index 47472da..653aceb 100644 --- a/native/build_test.go +++ b/native/build_test.go @@ -185,4 +185,27 @@ Start-Class: test-start-class Expect(out.String()).To(ContainSubstring("$BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS has been deprecated. Please use $BP_NATIVE_IMAGE_BUILD_ARGUMENTS instead.")) }) }) + + context("BP_NATIVE_IMAGE_BUILD_JAR", func() { + it.Before(func() { + Expect(os.Setenv("BP_NATIVE_IMAGE_BUILD_JAR", "native-sources/my-app-runner.jar")).To(Succeed()) + }) + + it.After(func() { + Expect(os.Unsetenv("BP_NATIVE_IMAGE_BUILD_JAR")).To(Succeed()) + }) + + it("contributes native image layer and process name equal to jar file name", func() { + result, err := build.Build(ctx) + Expect(err).NotTo(HaveOccurred()) + + Expect(result.Layers).To(HaveLen(1)) + Expect(result.Layers[0].(native.NativeImage).Arguments).To(BeEmpty()) + Expect(result.Processes).To(ContainElements( + libcnb.Process{Type: "native-image", Command: filepath.Join(ctx.Application.Path, "my-app-runner"), Direct: true}, + libcnb.Process{Type: "task", Command: filepath.Join(ctx.Application.Path, "my-app-runner"), Direct: true}, + libcnb.Process{Type: "web", Command: filepath.Join(ctx.Application.Path, "my-app-runner"), Direct: true, Default: true}, + )) + }) + }) } diff --git a/native/native_image_test.go b/native/native_image_test.go index 4f8cad1..f563869 100644 --- a/native/native_image_test.go +++ b/native/native_image_test.go @@ -41,7 +41,6 @@ import ( func testNativeImage(t *testing.T, context spec.G, it spec.S) { var ( Expect = NewWithT(t).Expect - ctx libcnb.BuildContext executor *mocks.Executor props *properties.Properties @@ -70,7 +69,7 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) { Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "fixture-marker"), []byte{}, 0644)).To(Succeed()) Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "BOOT-INF"), 0755)).To(Succeed()) - nativeImage, err = native.NewNativeImage(ctx.Application.Path, "test-argument-1 test-argument-2", "none", props, ctx.StackID) + nativeImage, err = native.NewNativeImage(ctx.Application.Path, "test-argument-1 test-argument-2", "none", props, ctx.StackID, "") nativeImage.Logger = bard.NewLogger(io.Discard) Expect(err).NotTo(HaveOccurred()) nativeImage.Executor = executor