diff --git a/native/native_image.go b/native/native_image.go index 494d7ab..f335784 100644 --- a/native/native_image.go +++ b/native/native_image.go @@ -40,7 +40,6 @@ type NativeImage struct { ApplicationPath string Arguments []string Executor effect.Executor - LayerContributor libpak.LayerContributor Logger bard.Logger Manifest *properties.Properties StackID string @@ -49,89 +48,59 @@ type NativeImage struct { func NewNativeImage(applicationPath string, arguments string, manifest *properties.Properties, stackID string) (NativeImage, error) { var err error - expected := map[string]interface{}{} - - expected["arguments"], err = shellwords.Parse(arguments) + args, err := shellwords.Parse(arguments) if err != nil { return NativeImage{}, fmt.Errorf("unable to parse arguments from %s\n%w", arguments, err) } - expected["files"], err = sherpa.NewFileListing(applicationPath) - if err != nil { - return NativeImage{}, fmt.Errorf("unable to create file listing for %s\n%w", applicationPath, err) - } - - n := NativeImage{ + return NativeImage{ ApplicationPath: applicationPath, - Arguments: expected["arguments"].([]string), + Arguments: args, Executor: effect.NewExecutor(), - LayerContributor: libpak.NewLayerContributor("Native Image", expected), Manifest: manifest, StackID: stackID, - } - - return n, nil + }, nil } func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { - n.LayerContributor.Logger = n.Logger - startClass, ok := n.Manifest.Get("Start-Class") if !ok { return libcnb.Layer{}, fmt.Errorf("manifest does not contain Start-Class") } - layer, err := n.LayerContributor.Contribute(layer, func() (libcnb.Layer, error) { - cp := []string{n.ApplicationPath} - - s, ok := n.Manifest.Get("Spring-Boot-Classes") - if !ok { - return libcnb.Layer{}, fmt.Errorf("manifest does not contain Spring-Boot-Classes") - } - cp = append(cp, filepath.Join(n.ApplicationPath, s)) - - s, ok = n.Manifest.Get("Spring-Boot-Classpath-Index") - if !ok { - return libcnb.Layer{}, fmt.Errorf("manifest does not contain Spring-Boot-Classpath-Index") - } - - file := filepath.Join(n.ApplicationPath, s) - in, err := os.Open(file) - if err != nil { - return libcnb.Layer{}, fmt.Errorf("unable to open %s\n%w", file, err) - } - defer in.Close() - - var libs []string - if err := yaml.NewDecoder(in).Decode(&libs); err != nil { - return libcnb.Layer{}, fmt.Errorf("unable to decode %s\n%w", file, err) - } + cp, err := n.classpath() + if err != nil { + return libcnb.Layer{}, fmt.Errorf("failed to compute classpath\n%w", err) + } - s, ok = n.Manifest.Get("Spring-Boot-Lib") - if !ok { - return libcnb.Layer{}, fmt.Errorf("manifest does not contain Spring-Boot-Lib") - } + if !n.hasSpringNative(cp) { + return libcnb.Layer{}, errors.New("application is missing required 'spring-native' dependency") + } - for _, l := range libs { - cp = append(cp, filepath.Join(n.ApplicationPath, s, l)) - } + arguments := n.Arguments - if !n.hasSpringNative(libs) { - return libcnb.Layer{}, errors.New("application is missing required 'spring-native' dependency") - } + if n.StackID == libpak.TinyStackID { + arguments = append(arguments, "-H:+StaticExecutableWithDynamicLibC") + } - arguments := n.Arguments + arguments = append(arguments, + fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, startClass)), + "-cp", strings.Join(cp, ":"), + startClass, + ) - if n.StackID == libpak.TinyStackID { - arguments = append(arguments, "-H:+StaticExecutableWithDynamicLibC") - } + files, err := sherpa.NewFileListing(n.ApplicationPath) + if err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to create file listing for %s\n%w", n.ApplicationPath, err) + } - arguments = append(arguments, - fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, startClass)), - "-cp", strings.Join(cp, ":"), - startClass, - ) + contributor := libpak.NewLayerContributor("Native Image", map[string]interface{}{ + "files": files, + "arguments": arguments, + }) + contributor.Logger = n.Logger + layer, err = contributor.Contribute(layer, func() (libcnb.Layer, error) { if err := n.Executor.Execute(effect.Execution{ Command: "native-image", Args: []string{"--version"}, @@ -171,17 +140,17 @@ func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { } } - file := filepath.Join(layer.Path, startClass) - in, err := os.Open(file) + 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", file, err) + return libcnb.Layer{}, fmt.Errorf("unable to open %s\n%w", filepath.Join(layer.Path, startClass), err) } defer in.Close() - file = filepath.Join(n.ApplicationPath, startClass) - out, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755) + 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", file, err) + return libcnb.Layer{}, fmt.Errorf("unable to open %s\n%w", dst, err) } defer out.Close() @@ -192,6 +161,49 @@ func (n NativeImage) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { return layer, nil } +func (n NativeImage) classpath() ([]string, error) { + cp := []string{n.ApplicationPath} + + classesDir, ok := n.Manifest.Get("Spring-Boot-Classes") + if !ok { + return nil, fmt.Errorf("manifest does not contain Spring-Boot-Classes") + } + cp = append(cp, filepath.Join(n.ApplicationPath, classesDir)) + + classpathIdx, ok := n.Manifest.Get("Spring-Boot-Classpath-Index") + if !ok { + return nil, fmt.Errorf("manifest does not contain Spring-Boot-Classpath-Index") + } + + file := filepath.Join(n.ApplicationPath, classpathIdx) + in, err := os.Open(filepath.Join(n.ApplicationPath, classpathIdx)) + if err != nil { + return nil, fmt.Errorf("unable to open %s\n%w", file, err) + } + defer in.Close() + + var libs []string + if err := yaml.NewDecoder(in).Decode(&libs); err != nil { + return nil, fmt.Errorf("unable to decode %s\n%w", file, err) + } + + libDir, ok := n.Manifest.Get("Spring-Boot-Lib") + if !ok { + return nil, fmt.Errorf("manifest does not contain Spring-Boot-Lib") + } + + for _, l := range libs { + if dir, _ := filepath.Split(l); dir == "" { + // In Spring Boot version < 2.4.2 classpath.idx contains a list of jars + cp = append(cp, filepath.Join(n.ApplicationPath, libDir, l)) + } else { + // In Spring Boot version >= 2.4.2 classpath.idx contains a list of relative paths to jars + cp = append(cp, filepath.Join(n.ApplicationPath, l)) + } + } + return cp, nil +} + func (NativeImage) Name() string { return "native-image" } diff --git a/native/native_image_test.go b/native/native_image_test.go index b943741..857fb05 100644 --- a/native/native_image_test.go +++ b/native/native_image_test.go @@ -40,8 +40,11 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) { var ( Expect = NewWithT(t).Expect - ctx libcnb.BuildContext - executor *mocks.Executor + ctx libcnb.BuildContext + executor *mocks.Executor + props *properties.Properties + nativeImage native.NativeImage + layer libcnb.Layer ) it.Before(func() { @@ -54,175 +57,194 @@ func testNativeImage(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) executor = &mocks.Executor{} - }) - - it.After(func() { - Expect(os.RemoveAll(ctx.Application.Path)).To(Succeed()) - Expect(os.RemoveAll(ctx.Layers.Path)).To(Succeed()) - }) - context("has neither spring-native nor spring-graalvm-native dependency", func() { - it("fails", func() { - m := properties.NewProperties() - _, _, err := m.Set("Start-Class", "test-start-class") - Expect(err).NotTo(HaveOccurred()) - _, _, err = m.Set("Spring-Boot-Classes", "BOOT-INF/classes/") - Expect(err).NotTo(HaveOccurred()) - _, _, err = m.Set("Spring-Boot-Classpath-Index", "BOOT-INF/classpath.idx") - Expect(err).NotTo(HaveOccurred()) - _, _, err = m.Set("Spring-Boot-Lib", "BOOT-INF/lib/") - Expect(err).NotTo(HaveOccurred()) + props = properties.NewProperties() - Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "fixture-marker"), []byte{}, 0644)).To(Succeed()) + _, _, err = props.Set("Start-Class", "test-start-class") + Expect(err).NotTo(HaveOccurred()) + _, _, err = props.Set("Spring-Boot-Classes", "BOOT-INF/classes/") + Expect(err).NotTo(HaveOccurred()) + _, _, err = props.Set("Spring-Boot-Classpath-Index", "BOOT-INF/classpath.idx") + Expect(err).NotTo(HaveOccurred()) + _, _, err = props.Set("Spring-Boot-Lib", "BOOT-INF/lib/") + Expect(err).NotTo(HaveOccurred()) - Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "BOOT-INF"), 0755)).To(Succeed()) - Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(` -- "test-jar.jar"`), 0644)).To(Succeed()) + 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()) - n, err := native.NewNativeImage(ctx.Application.Path, "test-argument-1 test-argument-2", m, ctx.StackID) - Expect(err).NotTo(HaveOccurred()) - n.Executor = executor + nativeImage, err = native.NewNativeImage(ctx.Application.Path, "test-argument-1 test-argument-2", props, ctx.StackID) + Expect(err).NotTo(HaveOccurred()) + nativeImage.Executor = executor - layer, err := ctx.Layers.Layer("test-layer") - Expect(err).NotTo(HaveOccurred()) + executor.On("Execute", mock.Anything).Run(func(args mock.Arguments) { + Expect(ioutil.WriteFile(filepath.Join(layer.Path, "test-start-class"), []byte{}, 0644)).To(Succeed()) + }).Return(nil) - executor.On("Execute", mock.Anything).Run(func(args mock.Arguments) { - Expect(ioutil.WriteFile(filepath.Join(layer.Path, "test-start-class"), []byte{}, 0644)).To(Succeed()) - }).Return(nil) + layer, err = ctx.Layers.Layer("test-layer") + Expect(err).NotTo(HaveOccurred()) + }) - layer, err = n.Contribute(layer) - Expect(err).To(HaveOccurred()) - }) + it.After(func() { + Expect(os.RemoveAll(ctx.Application.Path)).To(Succeed()) + Expect(os.RemoveAll(ctx.Layers.Path)).To(Succeed()) }) - context("has spring-native dependency", func() { - it("it builds a native image", func() { - m := properties.NewProperties() - _, _, err := m.Set("Start-Class", "test-start-class") - Expect(err).NotTo(HaveOccurred()) - _, _, err = m.Set("Spring-Boot-Classes", "BOOT-INF/classes/") - Expect(err).NotTo(HaveOccurred()) - _, _, err = m.Set("Spring-Boot-Classpath-Index", "BOOT-INF/classpath.idx") - Expect(err).NotTo(HaveOccurred()) - _, _, err = m.Set("Spring-Boot-Lib", "BOOT-INF/lib/") - Expect(err).NotTo(HaveOccurred()) + context("classpath.idx contains a list of jar", func() { + context("neither spring-native nor spring-graalvm-native dependency", func() { + it.Before(func() { + Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(` +- "test-jar.jar"`), 0644)).To(Succeed()) + }) - Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "fixture-marker"), []byte{}, 0644)).To(Succeed()) + it("fails", func() { + _, err := nativeImage.Contribute(layer) + Expect(err).To(HaveOccurred()) + }) + }) - Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "BOOT-INF"), 0755)).To(Succeed()) - Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(` + context("spring-native dependency", func() { + it.Before(func() { + Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(` - "test-jar.jar" -- "spring-graalvm-native-0.8.6-xxxxxx.jar" +- "spring-native-0.8.6-xxxxxx.jar" `), 0644)).To(Succeed()) - - n, err := native.NewNativeImage(ctx.Application.Path, "test-argument-1 test-argument-2", m, ctx.StackID) - Expect(err).NotTo(HaveOccurred()) - n.Executor = executor - - layer, err := ctx.Layers.Layer("test-layer") - Expect(err).NotTo(HaveOccurred()) - - executor.On("Execute", mock.Anything).Run(func(args mock.Arguments) { - Expect(ioutil.WriteFile(filepath.Join(layer.Path, "test-start-class"), []byte{}, 0644)).To(Succeed()) - }).Return(nil) - - layer, err = n.Contribute(layer) - Expect(err).NotTo(HaveOccurred()) - - execution := executor.Calls[1].Arguments[0].(effect.Execution) - Expect(execution.Args[4]).To(Equal(strings.Join([]string{ - ctx.Application.Path, - filepath.Join(ctx.Application.Path, "BOOT-INF", "classes"), - filepath.Join(ctx.Application.Path, "BOOT-INF", "lib", "test-jar.jar"), - filepath.Join(ctx.Application.Path, "BOOT-INF", "lib", "spring-graalvm-native-0.8.6-xxxxxx.jar"), - }, ":"))) + }) + + it("contributes native image", func() { + _, err := nativeImage.Contribute(layer) + Expect(err).NotTo(HaveOccurred()) + + execution := executor.Calls[1].Arguments[0].(effect.Execution) + Expect(execution.Args).To(Equal([]string{ + "test-argument-1", + "test-argument-2", + fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, "test-start-class")), + "-cp", + strings.Join([]string{ + filepath.Join(ctx.Application.Path), + filepath.Join(ctx.Application.Path, "BOOT-INF", "classes"), + filepath.Join(ctx.Application.Path, "BOOT-INF", "lib", "test-jar.jar"), + filepath.Join(ctx.Application.Path, "BOOT-INF", "lib", "spring-native-0.8.6-xxxxxx.jar"), + }, ":"), + "test-start-class", + })) + }) }) - }) - - context("has spring-graalvm-native dependency", func() { - it("it builds a native image", func() { - m := properties.NewProperties() - _, _, err := m.Set("Start-Class", "test-start-class") - Expect(err).NotTo(HaveOccurred()) - _, _, err = m.Set("Spring-Boot-Classes", "BOOT-INF/classes/") - Expect(err).NotTo(HaveOccurred()) - _, _, err = m.Set("Spring-Boot-Classpath-Index", "BOOT-INF/classpath.idx") - Expect(err).NotTo(HaveOccurred()) - _, _, err = m.Set("Spring-Boot-Lib", "BOOT-INF/lib/") - Expect(err).NotTo(HaveOccurred()) - - 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()) - Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(` + context("spring-graalvm-native dependency", func() { + it.Before(func() { + Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(` - "test-jar.jar" - "spring-graalvm-native-0.8.0-20200729.130845-95.jar" `), 0644)).To(Succeed()) + }) + + it("contributes native image", func() { + _, err := nativeImage.Contribute(layer) + Expect(err).NotTo(HaveOccurred()) + + execution := executor.Calls[1].Arguments[0].(effect.Execution) + Expect(execution.Args).To(Equal([]string{ + "test-argument-1", + "test-argument-2", + fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, "test-start-class")), + "-cp", + strings.Join([]string{ + filepath.Join(ctx.Application.Path), + filepath.Join(ctx.Application.Path, "BOOT-INF", "classes"), + filepath.Join(ctx.Application.Path, "BOOT-INF", "lib", "test-jar.jar"), + filepath.Join(ctx.Application.Path, "BOOT-INF", "lib", "spring-graalvm-native-0.8.0-20200729.130845-95.jar"), + }, ":"), + "test-start-class", + })) + }) + }) + }) - n, err := native.NewNativeImage(ctx.Application.Path, "test-argument-1 test-argument-2", m, ctx.StackID) - Expect(err).NotTo(HaveOccurred()) - n.Executor = executor - - layer, err := ctx.Layers.Layer("test-layer") - Expect(err).NotTo(HaveOccurred()) + context("classpath.idx contains a list of relative paths to jar", func() { + context("has neither spring-native nor spring-graalvm-native dependency", func() { + it.Before(func() { + Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(` +- "test-jar.jar"`), 0644)).To(Succeed()) + }) - executor.On("Execute", mock.Anything).Run(func(args mock.Arguments) { - Expect(ioutil.WriteFile(filepath.Join(layer.Path, "test-start-class"), []byte{}, 0644)).To(Succeed()) - }).Return(nil) + it("fails", func() { + _, err := nativeImage.Contribute(layer) + Expect(err).To(HaveOccurred()) + }) + }) - layer, err = n.Contribute(layer) - Expect(err).NotTo(HaveOccurred()) + context("spring-native dependency", func() { + it.Before(func() { + Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(` +- "some/path/test-jar.jar" +- "some/path/spring-native-0.8.6-xxxxxx.jar" +`), 0644)).To(Succeed()) + }) + + it("contributes native image", func() { + _, err := nativeImage.Contribute(layer) + Expect(err).NotTo(HaveOccurred()) + + execution := executor.Calls[1].Arguments[0].(effect.Execution) + Expect(execution.Args).To(Equal([]string{ + "test-argument-1", + "test-argument-2", + fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, "test-start-class")), + "-cp", + strings.Join([]string{ + filepath.Join(ctx.Application.Path), + filepath.Join(ctx.Application.Path, "BOOT-INF", "classes"), + filepath.Join(ctx.Application.Path, "some", "path", "test-jar.jar"), + filepath.Join(ctx.Application.Path, "some", "path", "spring-native-0.8.6-xxxxxx.jar"), + }, ":"), + "test-start-class", + })) + }) + }) - execution := executor.Calls[1].Arguments[0].(effect.Execution) - Expect(execution.Args[4]).To(Equal(strings.Join([]string{ - ctx.Application.Path, - filepath.Join(ctx.Application.Path, "BOOT-INF", "classes"), - filepath.Join(ctx.Application.Path, "BOOT-INF", "lib", "test-jar.jar"), - filepath.Join(ctx.Application.Path, "BOOT-INF", "lib", "spring-graalvm-native-0.8.0-20200729.130845-95.jar"), - }, ":"))) + context("spring-graalvm-native dependency", func() { + it.Before(func() { + Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(` +- "some/path/test-jar.jar" +- "some/path/spring-graalvm-native-0.8.0-20200729.130845-95.jar" +`), 0644)).To(Succeed()) + }) + + it("contributes native image", func() { + _, err := nativeImage.Contribute(layer) + Expect(err).NotTo(HaveOccurred()) + + execution := executor.Calls[1].Arguments[0].(effect.Execution) + Expect(execution.Args).To(Equal([]string{ + "test-argument-1", + "test-argument-2", + fmt.Sprintf("-H:Name=%s", filepath.Join(layer.Path, "test-start-class")), + "-cp", + strings.Join([]string{ + filepath.Join(ctx.Application.Path), + filepath.Join(ctx.Application.Path, "BOOT-INF", "classes"), + filepath.Join(ctx.Application.Path, "some", "path", "test-jar.jar"), + filepath.Join(ctx.Application.Path, "some", "path", "spring-graalvm-native-0.8.0-20200729.130845-95.jar"), + }, ":"), + "test-start-class", + })) + }) }) }) context("tiny stack", func() { it.Before(func() { - ctx.StackID = libpak.TinyStackID + nativeImage.StackID = libpak.TinyStackID }) it("contributes native image", func() { - m := properties.NewProperties() - _, _, err := m.Set("Start-Class", "test-start-class") - Expect(err).NotTo(HaveOccurred()) - _, _, err = m.Set("Spring-Boot-Classes", "BOOT-INF/classes/") - Expect(err).NotTo(HaveOccurred()) - _, _, err = m.Set("Spring-Boot-Classpath-Index", "BOOT-INF/classpath.idx") - Expect(err).NotTo(HaveOccurred()) - _, _, err = m.Set("Spring-Boot-Lib", "BOOT-INF/lib/") - Expect(err).NotTo(HaveOccurred()) - - 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()) - Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "BOOT-INF", "classpath.idx"), []byte(` -- "test-jar.jar"`), 0644)).To(Succeed()) - - Expect(os.MkdirAll(filepath.Join(ctx.Application.Path, "BOOT-INF"), 0755)).To(Succeed()) Expect(ioutil.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()) - - n, err := native.NewNativeImage(ctx.Application.Path, "test-argument-1 test-argument-2", m, ctx.StackID) - Expect(err).NotTo(HaveOccurred()) - n.Executor = executor - - layer, err := ctx.Layers.Layer("test-layer") - Expect(err).NotTo(HaveOccurred()) - - executor.On("Execute", mock.Anything).Run(func(args mock.Arguments) { - Expect(ioutil.WriteFile(filepath.Join(layer.Path, "test-start-class"), []byte{}, 0644)).To(Succeed()) - }).Return(nil) - - layer, err = n.Contribute(layer) + var err error + layer, err := nativeImage.Contribute(layer) Expect(err).NotTo(HaveOccurred()) Expect(layer.Cache).To(BeTrue())