From a62c08b8455c3ab11683675b0f19e7eb94946a81 Mon Sep 17 00:00:00 2001 From: Emily Casey Date: Tue, 9 Mar 2021 22:55:33 -0500 Subject: [PATCH] Contributes exanded classpath during native image builds Contribute appends application classes and entries from classpath.idx to the build time classpath. In a JVM application the Spring Boot Loader would make these modifications to the classpath when the executable JAR or WAR is launched. To set the classpath properly for a native-image build the buildpack must replicate this behavior. Signed-off-by: Emily Casey --- README.md | 19 ++-- boot/build.go | 208 +++++++++++++++++++++++--------------- boot/build_test.go | 10 +- boot/detect.go | 5 + boot/init_test.go | 1 + boot/native_image.go | 117 +++++++++++++++++++++ boot/native_image_test.go | 119 ++++++++++++++++++++++ 7 files changed, 387 insertions(+), 92 deletions(-) create mode 100644 boot/native_image.go create mode 100644 boot/native_image_test.go diff --git a/README.md b/README.md index cd4862b..3924d50 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,20 @@ The buildpack will do the following: * Contributes Spring Boot version to `org.springframework.boot.version` image label * Contributes Spring Boot configuration metadata to `org.springframework.boot.spring-configuration-metadata.json` image label +* If `/META-INF/dataflow-configuration-metadata.properties` exists + * Contributes Spring Cloud Data Flow configuration metadata to `org.springframework.cloud.dataflow.spring-configuration-metadata.json` image label * Contributes `Implementation-Title` manifest entry to `org.opencontainers.image.title` image label * Contributes `Implementation-version` manifest entry to `org.opencontainers.image.version` image label * Contributes dependency information extracted from Maven naming conventions to the image's BOM -* Contributes [Spring Cloud Bindings][b] as an application dependency - * This enables bindings-aware Spring Boot auto-configuration when [CNB bindings][c] are present during launch -* If `/META-INF/dataflow-configuration-metadata.properties` exists - * Contributes Spring Cloud Data Flow configuration metadata to `org.springframework.cloud.dataflow.spring-configuration-metadata.json` image label -* If `/META-INF/MANIFEST.MF` contains a `Spring-Boot-Layers-Index` entry - * 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 +* When contributing to a JVM application: + * Contributes [Spring Cloud Bindings][b] as an application dependency + * This enables bindings-aware Spring Boot auto-configuration when [CNB bindings][c] are present during launch + * If `/META-INF/MANIFEST.MF` contains a `Spring-Boot-Layers-Index` entry + * 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 +* 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` [b]: https://github.com/spring-cloud/spring-cloud-bindings [c]: https://github.com/buildpacks/spec/blob/main/extensions/bindings.md diff --git a/boot/build.go b/boot/build.go index db5dd67..61bee79 100644 --- a/boot/build.go +++ b/boot/build.go @@ -19,18 +19,28 @@ package boot import ( "bytes" "encoding/json" + "errors" "fmt" "os" "path/filepath" "strings" "github.com/buildpacks/libcnb" + "github.com/magiconair/properties" "github.com/paketo-buildpacks/libjvm" "github.com/paketo-buildpacks/libpak" "github.com/paketo-buildpacks/libpak/bard" "gopkg.in/yaml.v3" ) +const ( + LabelSpringBootVersion = "org.springframework.boot.version" + LabelImageTitle = "org.opencontainers.image.title" + LabelImageVersion = "org.opencontainers.image.version" + LabelBootConfigurationMetadata = "org.springframework.boot.spring-configuration-metadata.json" + LabelDataFlowConfigurationMetadata = "org.springframework.cloud.dataflow.spring-configuration-metadata.json" +) + type Build struct { Logger bard.Logger } @@ -43,6 +53,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { version, ok := manifest.Get("Spring-Boot-Version") if !ok { + // this isn't a boot app, return without printing title return libcnb.BuildResult{}, nil } @@ -62,71 +73,17 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { } dc.Logger = b.Logger - lib, ok := manifest.Get("Spring-Boot-Lib") - if !ok { - return libcnb.BuildResult{}, fmt.Errorf("manifest does not container Spring-Boot-Lib") - } - - result.Labels = append(result.Labels, libcnb.Label{Key: "org.springframework.boot.version", Value: version}) - - if s, ok := manifest.Get("Implementation-Title"); ok { - result.Labels = append(result.Labels, libcnb.Label{Key: "org.opencontainers.image.title", Value: s}) - } - - if s, ok := manifest.Get("Implementation-Version"); ok { - result.Labels = append(result.Labels, libcnb.Label{Key: "org.opencontainers.image.version", Value: s}) - } - - c, err := NewConfigurationMetadataFromPath(context.Application.Path) + // add labels + result.Labels, err = labels(context, manifest) if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to read configuration metadata from %s\n%w", context.Application.Path, err) + return libcnb.BuildResult{}, err } - file := filepath.Join(lib, "*.jar") - files, err := filepath.Glob(file) - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to glob %s\n%w", file, err) - } - - for _, file := range files { - d, err := NewConfigurationMetadataFromJAR(file) - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to read configuration metadata from %s\n%w", file, err) - } - - c.Groups = append(c.Groups, d.Groups...) - c.Properties = append(c.Properties, d.Properties...) - c.Hints = append(c.Hints, d.Hints...) - } - - if len(c.Groups) > 0 || len(c.Properties) > 0 || len(c.Hints) > 0 { - b := &bytes.Buffer{} - if err := json.NewEncoder(b).Encode(c); err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to encode configuration metadata\n%w", err) - } - - result.Labels = append(result.Labels, libcnb.Label{ - Key: "org.springframework.boot.spring-configuration-metadata.json", - Value: strings.TrimSpace(b.String()), - }) - } - - c, err = NewDataFlowConfigurationMetadata(context.Application.Path, c) - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to generate data flow configuration metadata\n%w", err) - } - if len(c.Groups) > 0 || len(c.Properties) > 0 || len(c.Hints) > 0 { - b := &bytes.Buffer{} - if err := json.NewEncoder(b).Encode(c); err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to encode configuration metadata\n%w", err) - } - - result.Labels = append(result.Labels, libcnb.Label{ - Key: "org.springframework.cloud.dataflow.spring-configuration-metadata.json", - Value: strings.TrimSpace(b.String()), - }) + // add dependencies to BOM + lib, ok := manifest.Get("Spring-Boot-Lib") + if !ok { + return libcnb.BuildResult{}, fmt.Errorf("manifest does not container Spring-Boot-Lib") } - d, err := libjvm.NewMavenJARListing(filepath.Join(context.Application.Path, lib)) if err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to generate dependencies from %s\n%w", context.Application.Path, err) @@ -137,6 +94,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { Launch: true, }) + // validate generations gv, err := NewGenerationValidator(filepath.Join(context.Buildpack.Path, "spring-generations.toml")) if err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to create generation validator\n%w", err) @@ -147,38 +105,30 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { return libcnb.BuildResult{}, fmt.Errorf("unable to validate spring-boot version\n%w", err) } - ni := false + buildNativeImage := false if n, ok, err := pr.Resolve("spring-boot"); err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to resolve spring-boot plan entry\n%w", err) } else if ok { if v, ok := n.Metadata["native-image"].(bool); ok { - ni = v + buildNativeImage = v } } - if !ni { - classes, ok := manifest.Get("Spring-Boot-Classes") - if !ok { - return libcnb.BuildResult{}, fmt.Errorf("manifest does not contain Spring-Boot-Classes") - } - - wr, err := NewWebApplicationResolver(classes, lib) + if buildNativeImage { + // set CLASSPATH for native image build + classpathLayer, err := NewNativeImageClasspath(context.Application.Path, manifest) if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to create WebApplicationTypeResolver\n%w", err) + return libcnb.BuildResult{}, fmt.Errorf("unable to create NativeImageClasspath\n%w", err) } - + classpathLayer.Logger = b.Logger + result.Layers = append(result.Layers, classpathLayer) + } else { + // contribute Spring Cloud Bindings h, be := libpak.NewHelperLayer(context.Buildpack, "spring-cloud-bindings") h.Logger = b.Logger result.Layers = append(result.Layers, h) result.BOM.Entries = append(result.BOM.Entries, be) - at, err := NewWebApplicationType(context.Application.Path, wr) - if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to create WebApplicationType\n%w", err) - } - at.Logger = b.Logger - result.Layers = append(result.Layers, at) - dep, err := dr.Resolve("spring-cloud-bindings", "") if err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to find dependency\n%w", err) @@ -189,6 +139,25 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { result.Layers = append(result.Layers, bindingsLayer) result.BOM.Entries = append(result.BOM.Entries, be) + // configure JVM for application type + classes, ok := manifest.Get("Spring-Boot-Classes") + if !ok { + return libcnb.BuildResult{}, fmt.Errorf("manifest does not contain Spring-Boot-Classes") + } + + wr, err := NewWebApplicationResolver(classes, lib) + if err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to create WebApplicationTypeResolver\n%w", err) + } + + at, err := NewWebApplicationType(context.Application.Path, wr) + if err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to create WebApplicationType\n%w", err) + } + at.Logger = b.Logger + result.Layers = append(result.Layers, at) + + // slice app dir if index, ok := manifest.Get("Spring-Boot-Layers-Index"); ok { b.Logger.Header("Creating slices from layers index") @@ -215,3 +184,84 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { return result, nil } + +func labels(context libcnb.BuildContext, manifest *properties.Properties) ([]libcnb.Label, error) { + var labels []libcnb.Label + + if s, ok := manifest.Get("Spring-Boot-Version"); ok { + labels = append(labels, libcnb.Label{Key: LabelSpringBootVersion, Value: s}) + } + + if s, ok := manifest.Get("Implementation-Title"); ok { + labels = append(labels, libcnb.Label{Key: LabelImageTitle, Value: s}) + } + + if s, ok := manifest.Get("Implementation-Version"); ok { + labels = append(labels, libcnb.Label{Key: LabelImageVersion, Value: s}) + } + + mdLabels, err := configurationMetadataLabels(context.Application.Path, manifest) + if err != nil { + return nil, fmt.Errorf("unable to generate data flow configuration metadata\n%w", err) + } + labels = append(labels, mdLabels...) + + return labels, nil +} + +func configurationMetadataLabels(appDir string, manifest *properties.Properties) ([]libcnb.Label, error) { + var labels []libcnb.Label + + md, err := NewConfigurationMetadataFromPath(appDir) + if err != nil { + return nil, fmt.Errorf("unable to read configuration metadata from %s\n%w", appDir, err) + } + + lib, ok := manifest.Get("Spring-Boot-Lib") + if !ok { + return nil, errors.New("manifest does not container Spring-Boot-Lib") + } + file := filepath.Join(lib, "*.jar") + files, err := filepath.Glob(file) + if err != nil { + return nil, fmt.Errorf("unable to glob %s\n%w", file, err) + } + + for _, file := range files { + jarMD, err := NewConfigurationMetadataFromJAR(file) + if err != nil { + return nil, fmt.Errorf("unable to read configuration metadata from %s\n%w", file, err) + } + + md.Groups = append(md.Groups, jarMD.Groups...) + md.Properties = append(md.Properties, jarMD.Properties...) + md.Hints = append(md.Hints, jarMD.Hints...) + } + if len(md.Groups) > 0 || len(md.Properties) > 0 || len(md.Hints) > 0 { + b := &bytes.Buffer{} + if err := json.NewEncoder(b).Encode(md); err != nil { + return nil, fmt.Errorf("unable to encode configuration metadata\n%w", err) + } + labels = append(labels, libcnb.Label{ + Key: LabelBootConfigurationMetadata, + Value: strings.TrimSpace(b.String()), + }) + } + + md, err = NewDataFlowConfigurationMetadata(appDir, md) + if err != nil { + return nil, fmt.Errorf("unable to generate data flow configuration metadata\n%w", err) + } + if len(md.Groups) > 0 || len(md.Properties) > 0 || len(md.Hints) > 0 { + b := &bytes.Buffer{} + if err := json.NewEncoder(b).Encode(md); err != nil { + return nil, fmt.Errorf("unable to encode configuration metadata\n%w", err) + } + labels = append(labels, libcnb.Label{ + Key: LabelDataFlowConfigurationMetadata, + Value: strings.TrimSpace(b.String()), + }) + } + + return labels, err +} diff --git a/boot/build_test.go b/boot/build_test.go index 16a7f2b..e8545fa 100644 --- a/boot/build_test.go +++ b/boot/build_test.go @@ -202,8 +202,8 @@ Spring-Boot-Lib: BOOT-INF/lib Expect(result.Layers).To(HaveLen(3)) Expect(result.Layers[0].Name()).To(Equal("helper")) Expect(result.Layers[0].(libpak.HelperLayerContributor).Names).To(Equal([]string{"spring-cloud-bindings"})) - Expect(result.Layers[1].Name()).To(Equal("web-application-type")) - Expect(result.Layers[2].Name()).To(Equal("spring-cloud-bindings")) + Expect(result.Layers[1].Name()).To(Equal("spring-cloud-bindings")) + Expect(result.Layers[2].Name()).To(Equal("web-application-type")) Expect(result.BOM.Entries).To(HaveLen(3)) Expect(result.BOM.Entries[0].Name).To(Equal("dependencies")) @@ -248,7 +248,7 @@ Spring-Boot-Layers-Index: layers.idx }) }) - it("adds no layers to the result", func() { + it("sets the CLASSPATH for the native image build", func() { Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "META-INF", "MANIFEST.MF"), []byte(` Spring-Boot-Version: 1.1.1 Spring-Boot-Classes: BOOT-INF/classes @@ -258,7 +258,8 @@ Spring-Boot-Lib: BOOT-INF/lib result, err := build.Build(ctx) Expect(err).NotTo(HaveOccurred()) - Expect(result.Layers).To(HaveLen(0)) + Expect(result.Layers).To(HaveLen(1)) + Expect(result.Layers[0].Name()).To(Equal("Class Path")) }) it("adds no slices to the result", func() { @@ -273,6 +274,5 @@ Spring-Boot-Lib: BOOT-INF/lib Expect(result.Slices).To(HaveLen(0)) }) - }) } diff --git a/boot/detect.go b/boot/detect.go index 371b28f..1e48747 100644 --- a/boot/detect.go +++ b/boot/detect.go @@ -20,6 +20,11 @@ import ( "github.com/buildpacks/libcnb" ) +const ( + PlanEntrySpringBoot = "spring-boot" + PlanEntryJVMApplication = "jvm-application" +) + type Detect struct{} func (Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) { diff --git a/boot/init_test.go b/boot/init_test.go index d1ae540..17fff40 100644 --- a/boot/init_test.go +++ b/boot/init_test.go @@ -32,5 +32,6 @@ func TestUnit(t *testing.T) { suite("SpringCloudBindings", testSpringCloudBindings) suite("WebApplicationType", testWebApplicationType) suite("WebApplicationTypeResolver", testWebApplicationTypeResolver) + suite("NativeImage", testNativeImage) suite.Run(t) } diff --git a/boot/native_image.go b/boot/native_image.go new file mode 100644 index 0000000..3a36976 --- /dev/null +++ b/boot/native_image.go @@ -0,0 +1,117 @@ +/* + * Copyright 2018-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package boot + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/buildpacks/libcnb" + "github.com/magiconair/properties" + "github.com/paketo-buildpacks/libpak" + "github.com/paketo-buildpacks/libpak/bard" + "gopkg.in/yaml.v3" +) + +type NativeImageClasspath struct { + Logger bard.Logger + ApplicationPath string + Manifest *properties.Properties +} + +func NewNativeImageClasspath(appDir string, manifest *properties.Properties) (NativeImageClasspath, error) { + return NativeImageClasspath{ + ApplicationPath: appDir, + Manifest: manifest, + }, nil +} + +// Contribute appends application classes and entries from classpath.idx to the build time classpath. +// +// In a JVM application the Spring Boot Loader would make these modifications to the classpath when the executable JAR +// or WAR is launched. To set the classpath properly for a native-image build the buildpack must replicate this behavior. +func (n NativeImageClasspath) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { + cp, err := n.classpathEntries() + if err != nil { + return libcnb.Layer{}, fmt.Errorf("failed to compute classpath\n%w", err) + } + expectedMetadata := map[string][]string{ + "classpath": cp, + } + + lc := libpak.NewLayerContributor("Class Path", expectedMetadata, libcnb.LayerTypes{ + Build: true, + }) + lc.Logger = n.Logger + + return lc.Contribute(layer, func() (libcnb.Layer, error) { + layer.BuildEnvironment.Append( + "CLASSPATH", + string(filepath.ListSeparator), + strings.Join(cp, string(filepath.ListSeparator)), + ) + return layer, nil + }) +} + +func (NativeImageClasspath) Name() string { + return "Class Path" +} + +func (n NativeImageClasspath) classpathEntries() ([]string, error) { + var cp []string + + 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.3.0.M4 -> 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.3.0.M3 or >= 2.4.2 classpath.idx contains a list of relative paths to jars + cp = append(cp, filepath.Join(n.ApplicationPath, l)) + } + } + return cp, nil +} diff --git a/boot/native_image_test.go b/boot/native_image_test.go new file mode 100644 index 0000000..74389df --- /dev/null +++ b/boot/native_image_test.go @@ -0,0 +1,119 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package boot_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/buildpacks/libcnb" + "github.com/magiconair/properties" + . "github.com/onsi/gomega" + "github.com/sclevine/spec" + + "github.com/paketo-buildpacks/spring-boot/boot" +) + +func testNativeImage(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + appDir string + contributor boot.NativeImageClasspath + manifest *properties.Properties + layer libcnb.Layer + layerDir string + ) + + it.Before(func() { + var err error + appDir, err = ioutil.TempDir("", "native-image-application") + Expect(err).NotTo(HaveOccurred()) + + layerDir, err = ioutil.TempDir("", "classpath-layer") + Expect(err).NotTo(HaveOccurred()) + layers := &libcnb.Layers{Path: layerDir} + layer, err = layers.Layer("test-layer") + Expect(err).NotTo(HaveOccurred()) + + manifest = properties.NewProperties() + + _, _, err = manifest.Set("Start-Class", "test-start-class") + Expect(err).NotTo(HaveOccurred()) + _, _, err = manifest.Set("Spring-Boot-Classes", "BOOT-INF/classes/") + Expect(err).NotTo(HaveOccurred()) + _, _, err = manifest.Set("Spring-Boot-Classpath-Index", "BOOT-INF/classpath.idx") + Expect(err).NotTo(HaveOccurred()) + _, _, err = manifest.Set("Spring-Boot-Lib", "BOOT-INF/lib/") + Expect(err).NotTo(HaveOccurred()) + + Expect(os.MkdirAll(filepath.Join(appDir, "BOOT-INF"), 0755)).To(Succeed()) + + contributor, err = boot.NewNativeImageClasspath(appDir, manifest) + Expect(err).NotTo(HaveOccurred()) + Expect(err).NotTo(HaveOccurred()) + + Expect(err).NotTo(HaveOccurred()) + }) + + context("classpath.idx contains a list of jar", func() { + it.Before(func() { + Expect(ioutil.WriteFile(filepath.Join(appDir, "BOOT-INF", "classpath.idx"), []byte(` +- "some.jar" +- "other.jar" +`), 0644)).To(Succeed()) + }) + + it("sets CLASSPATH for build", func() { + layer, err := contributor.Contribute(layer) + Expect(err).NotTo(HaveOccurred()) + + Expect(layer.BuildEnvironment["CLASSPATH.append"]).To(Equal(strings.Join([]string{ + filepath.Join(appDir, "BOOT-INF", "classes"), + filepath.Join(appDir, "BOOT-INF", "lib", "some.jar"), + filepath.Join(appDir, "BOOT-INF", "lib", "other.jar"), + }, ":"))) + Expect(layer.LayerTypes.Build).To(BeTrue()) + Expect(layer.LayerTypes.Launch).To(BeFalse()) + }) + }) + + context("classpath.idx contains a list of relative paths to jars", func() { + it.Before(func() { + Expect(ioutil.WriteFile(filepath.Join(appDir, "BOOT-INF", "classpath.idx"), []byte(` +- "some/path/some.jar" +- "some/path/other.jar" +`), 0644)).To(Succeed()) + }) + + it("sets CLASSPATH for build", func() { + layer, err := contributor.Contribute(layer) + Expect(err).NotTo(HaveOccurred()) + + Expect(layer.BuildEnvironment["CLASSPATH.append"]).To(Equal(strings.Join([]string{ + filepath.Join(appDir, "BOOT-INF", "classes"), + filepath.Join(appDir, "some", "path", "some.jar"), + filepath.Join(appDir, "some", "path", "other.jar"), + }, ":"))) + Expect(layer.LayerTypes.Build).To(BeTrue()) + Expect(layer.LayerTypes.Launch).To(BeFalse()) + }) + }) +}