Skip to content

Commit

Permalink
Also copy .so files when doing a native-image build
Browse files Browse the repository at this point in the history
Some applications, when native-image runs, will also generate shared libraries which are required to run the application. These libraries get put next to the generated binary file. This change modifies the buildpack to copy these shared libraries after it copies the binary to the application directory.

In a nutshell, native-image runs, writes binary and shared libs to a cached layer. We then copy files from the cached layer to the application directory (i.e. /workspace) which results in them being available in the application image.

Signed-off-by: Daniel Mikusa <[email protected]>
  • Loading branch information
dmikusa committed Jul 14, 2024
1 parent 78e7383 commit 969834f
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 68 deletions.
10 changes: 4 additions & 6 deletions native/arguments.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package native

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
Expand Down Expand Up @@ -82,20 +81,20 @@ type UserFileArguments struct {
// Configure returns the inputArgs plus the additional arguments provided via argfile, setting via the '@argfile' format
func (u UserFileArguments) Configure(inputArgs []string) ([]string, string, error) {

rawArgs, err := ioutil.ReadFile(u.ArgumentsFile)
rawArgs, err := os.ReadFile(u.ArgumentsFile)
if err != nil {
return []string{}, "", fmt.Errorf("read arguments from %s\n%w", u.ArgumentsFile, err)
}

fileArgs := strings.Split(string(rawArgs), "\n")
if len(fileArgs) == 1{
if len(fileArgs) == 1 {
fileArgs = strings.Split(string(rawArgs), " ")
}

if containsArg("-jar", fileArgs) {
fileArgs = replaceJarArguments(fileArgs)
newArgList := strings.Join(fileArgs, " ")
if err = os.WriteFile(u.ArgumentsFile,[]byte(newArgList),0644); err != nil{
if err = os.WriteFile(u.ArgumentsFile, []byte(newArgList), 0644); err != nil {
return []string{}, "", fmt.Errorf("unable to write to arguments file %s\n%w", u.ArgumentsFile, err)
}
}
Expand All @@ -106,7 +105,6 @@ func (u UserFileArguments) Configure(inputArgs []string) ([]string, string, erro

}


// containsArg checks if needle is found in haystack
//
// needle and haystack entries are processed as key=val strings where only the key must match
Expand Down Expand Up @@ -230,4 +228,4 @@ func replaceJarArguments(fileArgs []string) []string {
modifiedArgs = append(modifiedArgs, inputArg)
}
return modifiedArgs
}
}
32 changes: 13 additions & 19 deletions native/arguments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package native_test

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
Expand All @@ -40,13 +39,8 @@ func testArguments(t *testing.T, context spec.G, it spec.S) {
)

it.Before(func() {
var err error

ctx.Application.Path, err = ioutil.TempDir("", "native-image-application")
Expect(err).NotTo(HaveOccurred())

ctx.Layers.Path, err = ioutil.TempDir("", "native-image-layers")
Expect(err).NotTo(HaveOccurred())
ctx.Application.Path = t.TempDir()
ctx.Layers.Path = t.TempDir()
})

it.After(func() {
Expand Down Expand Up @@ -126,10 +120,10 @@ func testArguments(t *testing.T, context spec.G, it spec.S) {
context("user arguments from file", func() {
it.Before(func() {
Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "target"), 0755)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "target", "more-stuff.txt"), []byte("more stuff"), 0644)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "target", "more-stuff-quotes.txt"), []byte(`before -jar "more stuff.jar" after -other="my path"`), 0644)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "target", "more-stuff-class.txt"), []byte(`stuff -jar stuff.jar after`), 0644)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "target", "override.txt"), []byte(`one=output`), 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "more-stuff.txt"), []byte("more stuff"), 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "more-stuff-quotes.txt"), []byte(`before -jar "more stuff.jar" after -other="my path"`), 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "more-stuff-class.txt"), []byte(`stuff -jar stuff.jar after`), 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "override.txt"), []byte(`one=output`), 0644)).To(Succeed())
})

it("has none", func() {
Expand All @@ -146,7 +140,7 @@ func testArguments(t *testing.T, context spec.G, it spec.S) {
Expect(err).ToNot(HaveOccurred())
Expect(startClass).To(Equal(""))
Expect(args).To(HaveLen(4))
Expect(args).To(Equal([]string{"one", "two", "three", fmt.Sprintf("@%s",filepath.Join(ctx.Application.Path,"target/more-stuff.txt"))}))
Expect(args).To(Equal([]string{"one", "two", "three", fmt.Sprintf("@%s", filepath.Join(ctx.Application.Path, "target/more-stuff.txt"))}))
})

it("works with quotes in the file", func() {
Expand All @@ -158,7 +152,7 @@ func testArguments(t *testing.T, context spec.G, it spec.S) {
Expect(startClass).To(Equal(""))
Expect(args).To(HaveLen(4))
Expect(args).To(Equal([]string{"one", "two", "three", fmt.Sprintf("@%s", filepath.Join(ctx.Application.Path, "target/more-stuff-quotes.txt"))}))
bits, err := ioutil.ReadFile(filepath.Join(ctx.Application.Path, "target/more-stuff-quotes.txt"))
bits, err := os.ReadFile(filepath.Join(ctx.Application.Path, "target/more-stuff-quotes.txt"))
Expect(err).ToNot(HaveOccurred())
Expect(string(bits)).To(Equal("before after -other=\"my path\""))
})
Expand All @@ -170,9 +164,9 @@ func testArguments(t *testing.T, context spec.G, it spec.S) {
Expect(err).ToNot(HaveOccurred())
Expect(args).To(HaveLen(1))
Expect(args).To(Equal([]string{
fmt.Sprintf("@%s",filepath.Join(ctx.Application.Path, "target", "more-stuff-class.txt")),
fmt.Sprintf("@%s", filepath.Join(ctx.Application.Path, "target", "more-stuff-class.txt")),
}))
bits, err := ioutil.ReadFile(filepath.Join(ctx.Application.Path, "target/more-stuff-class.txt"))
bits, err := os.ReadFile(filepath.Join(ctx.Application.Path, "target/more-stuff-class.txt"))
Expect(err).ToNot(HaveOccurred())
Expect(string(bits)).To(Equal("after"))
})
Expand Down Expand Up @@ -254,9 +248,9 @@ func testArguments(t *testing.T, context spec.G, it spec.S) {
context("jar file", func() {
it.Before(func() {
Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "target"), 0755)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "target", "found.jar"), []byte{}, 0644)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "target", "a.two"), []byte{}, 0644)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "target", "b.two"), []byte{}, 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "found.jar"), []byte{}, 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "a.two"), []byte{}, 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "target", "b.two"), []byte{}, 0644)).To(Succeed())
})

it("adds arguments", func() {
Expand Down
3 changes: 2 additions & 1 deletion native/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ package native
import (
"errors"
"fmt"
"github.com/paketo-buildpacks/libpak/sherpa"
"os"
"path/filepath"

"github.com/paketo-buildpacks/libpak/sherpa"

"github.com/paketo-buildpacks/libpak/effect"
"github.com/paketo-buildpacks/libpak/sbom"

Expand Down
64 changes: 44 additions & 20 deletions native/native_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ import (
"bytes"
"crypto/sha256"
"fmt"
"github.com/paketo-buildpacks/native-image/v5/native/slices"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/paketo-buildpacks/native-image/v5/native/slices"

"github.com/buildpacks/libcnb"
"github.com/magiconair/properties"
"github.com/paketo-buildpacks/libpak"
Expand Down Expand Up @@ -143,7 +142,7 @@ func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) {
}

n.Logger.Header("Removing bytecode")
cs, err := ioutil.ReadDir(n.ApplicationPath)
cs, err := os.ReadDir(n.ApplicationPath)
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to list children of %s\n%w", n.ApplicationPath, err)
}
Expand All @@ -154,22 +153,8 @@ func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) {
}
}

src := filepath.Join(layer.Path, startClass)
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)
}
defer in.Close()

dst := filepath.Join(n.ApplicationPath, startClass)
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)
}
defer out.Close()

if _, err := io.Copy(out, in); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to copy\n%w", err)
if err := copyFilesFromLayer(layer.Path, startClass, n.ApplicationPath); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to copy files from layer\n%w", err)
}

return layer, nil
Expand Down Expand Up @@ -225,3 +210,42 @@ func (n NativeImage) ProcessArguments(layer libcnb.Layer) ([]string, string, err
func (NativeImage) Name() string {
return "native-image"
}

// copy the main file & any `*.so` files also in the layer to the application path
func copyFilesFromLayer(layerPath string, execName string, appPath string) error {
files, err := os.ReadDir(layerPath)
if err != nil {
return fmt.Errorf("unable to list files on layer %s\n%w", layerPath, err)
}

for _, file := range files {
if file.Type().IsRegular() && (file.Name() == execName) {
src := filepath.Join(layerPath, file.Name())
dst := filepath.Join(appPath, file.Name())

if err := copyFile(src, dst); err != nil {
return fmt.Errorf("unable to copy %s to %s\n%w", src, dst, err)
}
}
if file.Type().IsRegular() && (strings.HasSuffix(file.Name(), ".so")) {
src := filepath.Join(layerPath, file.Name())
dst := filepath.Join(appPath, file.Name())

if err := copyFile(src, dst); err != nil {
return fmt.Errorf("unable to copy %s to %s\n%w", src, dst, err)
}
}
}

return nil
}

func copyFile(src string, dst string) error {
in, err := os.Open(src)
if err != nil {
return fmt.Errorf("unable to open %s\n%w", src, err)
}
defer in.Close()

return sherpa.CopyFile(in, dst)
}
61 changes: 39 additions & 22 deletions native/native_image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package native_test
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -50,27 +49,22 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) {
)

it.Before(func() {
var err error

ctx.Application.Path, err = ioutil.TempDir("", "native-image-application")
Expect(err).NotTo(HaveOccurred())

ctx.Layers.Path, err = ioutil.TempDir("", "native-image-layers")
Expect(err).NotTo(HaveOccurred())
ctx.Application.Path = t.TempDir()
ctx.Layers.Path = t.TempDir()

executor = &mocks.Executor{}

props = properties.NewProperties()

_, _, err = props.Set("Start-Class", "test-start-class")
_, _, err := props.Set("Start-Class", "test-start-class")
Expect(err).NotTo(HaveOccurred())
_, _, err = props.Set("Class-Path", "manifest-class-path")
Expect(err).NotTo(HaveOccurred())

Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "fixture-marker"), []byte{}, 0644)).To(Succeed())
Expect(os.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())
Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "META-INF"), 0755)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte{}, 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte{}, 0644)).To(Succeed())

nativeImage, err = native.NewNativeImage(ctx.Application.Path, "test-argument-1 test-argument-2", "", "none", "", props, ctx.StackID)
nativeImage.Logger = bard.NewLogger(io.Discard)
Expand All @@ -91,7 +85,9 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) {
})).Run(func(args mock.Arguments) {
exec := args.Get(0).(effect.Execution)
lastArg := exec.Args[len(exec.Args)-1]
Expect(ioutil.WriteFile(filepath.Join(layer.Path, lastArg), []byte{}, 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layer.Path, lastArg), []byte{}, 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layer.Path, "libawt.so"), []byte{}, 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layer.Path, "libawt_headless.so"), []byte{}, 0644)).To(Succeed())
}).Return(nil)

executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool {
Expand All @@ -100,7 +96,9 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) {
})).Run(func(args mock.Arguments) {
exec := args.Get(0).(effect.Execution)
lastArg := exec.Args[len(exec.Args)-1]
Expect(ioutil.WriteFile(filepath.Join(layer.Path, lastArg), []byte{}, 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layer.Path, lastArg), []byte{}, 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layer.Path, "libawt.so"), []byte{}, 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layer.Path, "libawt_headless.so"), []byte{}, 0644)).To(Succeed())
}).Return(nil)

layer, err = ctx.Layers.Layer("test-layer")
Expand Down Expand Up @@ -134,6 +132,25 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) {
"-cp", "some-classpath",
"test-start-class",
}))

Expect(filepath.Join(ctx.Application.Path, "BOOT-INF")).ToNot(BeADirectory())
Expect(filepath.Join(ctx.Application.Path, "META-INF")).ToNot(BeADirectory())

Expect(filepath.Join(layer.Path, "test-start-class")).To(BeARegularFile())
Expect(filepath.Join(layer.Path, "libawt.so")).To(BeARegularFile())
Expect(filepath.Join(layer.Path, "libawt_headless.so")).To(BeARegularFile())

info, err := os.Stat(filepath.Join(layer.Path, "test-start-class"))
Expect(err).NotTo(HaveOccurred())
fmt.Println("info.Mode().Perm(): ", info.Mode().Perm().String())
Expect(info.Mode().Perm()).To(Equal(os.FileMode(0755)))

Expect(filepath.Join(ctx.Application.Path, "test-start-class")).To(BeARegularFile())
Expect(filepath.Join(ctx.Application.Path, "libawt.so")).To(BeARegularFile())
Expect(filepath.Join(ctx.Application.Path, "libawt_headless.so")).To(BeARegularFile())
info, err = os.Stat(filepath.Join(ctx.Application.Path, "test-start-class"))
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode().Perm()).To(Equal(os.FileMode(0755)))
})
})

Expand All @@ -160,7 +177,7 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) {
it("contributes native image with Class-Path from manifest and args from a file", func() {
argsFile := filepath.Join(ctx.Application.Path, "target", "args.txt")
Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "target"), 0755)).To(Succeed())
Expect(ioutil.WriteFile(argsFile, []byte(`test-argument-1 test-argument-2`), 0644)).To(Succeed())
Expect(os.WriteFile(argsFile, []byte(`test-argument-1 test-argument-2`), 0644)).To(Succeed())

nativeImage, err := native.NewNativeImage(ctx.Application.Path, "", argsFile, "none", "", props, ctx.StackID)
nativeImage.Logger = bard.NewLogger(io.Discard)
Expand Down Expand Up @@ -209,7 +226,7 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) {
})).Run(func(args mock.Arguments) {
exec := args.Get(0).(effect.Execution)
lastArg := exec.Args[len(exec.Args)-1]
Expect(ioutil.WriteFile(filepath.Join(layer.Path, lastArg), []byte{}, 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layer.Path, lastArg), []byte{}, 0644)).To(Succeed())
}).Return(nil)

layer, err = ctx.Layers.Layer("test-layer")
Expand Down Expand Up @@ -254,7 +271,7 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) {
})).Run(func(args mock.Arguments) {
exec := args.Get(0).(effect.Execution)
lastArg := exec.Args[len(exec.Args)-1]
Expect(ioutil.WriteFile(filepath.Join(layer.Path, lastArg), []byte{}, 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layer.Path, lastArg), []byte{}, 0644)).To(Succeed())
}).Return(nil)

layer, err = ctx.Layers.Layer("test-layer")
Expand Down Expand Up @@ -316,7 +333,7 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) {
executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool {
return e.Command == "upx"
})).Run(func(args mock.Arguments) {
Expect(ioutil.WriteFile(filepath.Join(layer.Path, "test-start-class"), []byte("upx-compressed"), 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layer.Path, "test-start-class"), []byte("upx-compressed"), 0644)).To(Succeed())
}).Return(nil)

_, err := nativeImage.Contribute(layer)
Expand All @@ -331,7 +348,7 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) {
bin := filepath.Join(layer.Path, "test-start-class")
Expect(bin).To(BeARegularFile())

data, err := ioutil.ReadFile(bin)
data, err := os.ReadFile(bin)
Expect(err).ToNot(HaveOccurred())
Expect(data).To(ContainSubstring("upx-compressed"))
})
Expand All @@ -344,8 +361,8 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) {
executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool {
return e.Command == "gzexe"
})).Run(func(args mock.Arguments) {
Expect(ioutil.WriteFile(filepath.Join(layer.Path, "test-start-class"), []byte("gzexe-compressed"), 0644)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(layer.Path, "test-start-class~"), []byte("original"), 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layer.Path, "test-start-class"), []byte("gzexe-compressed"), 0644)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layer.Path, "test-start-class~"), []byte("original"), 0644)).To(Succeed())
}).Return(nil)

_, err := nativeImage.Contribute(layer)
Expand All @@ -360,7 +377,7 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) {
bin := filepath.Join(layer.Path, "test-start-class")
Expect(bin).To(BeARegularFile())

data, err := ioutil.ReadFile(bin)
data, err := os.ReadFile(bin)
Expect(err).ToNot(HaveOccurred())
Expect(data).To(ContainSubstring("gzexe-compressed"))
Expect(filepath.Join(layer.Path, "test-start-class~")).ToNot(BeAnExistingFile())
Expand All @@ -373,7 +390,7 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) {
})

it("contributes a static native image executable with dynamic libc", func() {
Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(`
Expect(os.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(`
- "test-jar.jar"
- "spring-graalvm-native-0.8.6-xxxxxx.jar"
`), 0644)).To(Succeed())
Expand Down

0 comments on commit 969834f

Please sign in to comment.