Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for creating a native-image using -jar option #124

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions buildpack.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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_JAR"
description = "jar file path to pass to the native-image command. Example (native-sources/my-app.jar)"
build = true

[metadata]
pre-package = "scripts/build.sh"
include-files = [
Expand Down
39 changes: 16 additions & 23 deletions native/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -48,15 +46,20 @@ 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)
}

cr, err := libpak.NewConfigurationResolver(context.Buildpack, &b.Logger)
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err)
}

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)
}
}

if _, ok := cr.Resolve(DeprecatedConfigNativeImage); ok {
b.warn(fmt.Sprintf("$%s has been deprecated. Please use $%s instead.",
DeprecatedConfigNativeImage,
Expand Down Expand Up @@ -84,18 +87,19 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
}
}

n, err := NewNativeImage(context.Application.Path, args, compressor, manifest, context.StackID)
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)
}
n.Logger = b.Logger
result.Layers = append(result.Layers, n)

startClass, err := findStartOrMainClass(manifest)
startClass, err := n.nativeMain.Name()
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to find required manifest property\n%w", err)
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)

command := filepath.Join(context.Application.Path, startClass)
result.Processes = append(result.Processes,
libcnb.Process{Type: "native-image", Command: command, Direct: true},
Expand All @@ -121,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
}
23 changes: 23 additions & 0 deletions native/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
))
})
})
}
47 changes: 24 additions & 23 deletions native/native_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,33 +38,40 @@ 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) {
var err error

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)
}

var nativeMain nativeMain
nativeMain = newStartClassMain(applicationPath, manifest)
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{
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
Expand All @@ -73,20 +80,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 {
Expand Down Expand Up @@ -114,6 +114,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,
Expand All @@ -128,7 +129,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(),
Expand All @@ -139,15 +140,15 @@ 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(),
}); err != nil {
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)
}
}
Expand All @@ -170,14 +171,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)
Expand Down
3 changes: 1 addition & 2 deletions native/native_image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
98 changes: 98 additions & 0 deletions native/native_main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package native

import (
"fmt"
"github.com/magiconair/properties"
"os"
"path/filepath"
"strings"
)

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
jarFilePath 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 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)
}
jarFileName := strings.TrimSuffix(jarFile, ".jar")
return &jarFileMain{
jarFileName: jarFileName,
jarFile: jarFile,
jarFilePath: jarFilePath,
}, nil
}

func (m *jarFileMain) Name() (string, error ) {
return m.jarFileName, nil
}

func (m *jarFileMain) Arguments() []string {
return []string {"-jar", filepath.Join(m.jarFilePath, m.jarFile)}
}

func (m *jarFileMain) ClassPath() string {
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
}