From cc0a400349060967d7279e40ccec21fcd6f0f450 Mon Sep 17 00:00:00 2001 From: Daniel Mikusa Date: Mon, 16 May 2022 13:52:59 -0400 Subject: [PATCH 1/2] Support SDKMAN RC files This PR includes support to read a '.sdkmanrc' file if it's present at the root of the application. The version order is (from lowest to highest priority): Java buildpack default (11), Maven MANIFEST.MF properties, SDKMAN RC file, and BP_JVM_VERSION. Higher priority locations override lower priority locations. Note that Maven MANIFEST.MF entries only apply to pre-compiled assets (as MANIFEST.MF won't exist in a source build) and SDKMAN RC only applies when building from source (as it won't likely exist in a pre-compiled asset). The buildpack will read the '.sdkmanrc' file, look for the 'java' component (if multiple, it picks the first one), and takes the major Java version indicated in the entry. It ignore minor/patch versions, it also ignores the vendor (the vendor is set based on the buildpack you use). The buildpack ignores other components. Signed-off-by: Daniel Mikusa --- build.go | 2 +- init_test.go | 1 + jvm_version.go | 86 ++++++++++++++++++------- jvm_version_test.go | 81 +++++++++++++++++------- sdkman.go | 76 +++++++++++++++++++++++ sdkman_test.go | 148 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 349 insertions(+), 45 deletions(-) create mode 100644 sdkman.go create mode 100644 sdkman_test.go diff --git a/build.go b/build.go index 2781740..9f59352 100644 --- a/build.go +++ b/build.go @@ -55,7 +55,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { cl := NewCertificateLoader() cl.Logger = b.Logger.BodyWriter() - jvmVersion := JVMVersion{Logger: b.Logger} + jvmVersion := NewJVMVersion(b.Logger) v, err := jvmVersion.GetJVMVersion(context.Application.Path, cr) if err != nil { return libcnb.BuildResult{}, fmt.Errorf("unable to determine jvm version\n%w", err) diff --git a/init_test.go b/init_test.go index d508464..5a9c777 100644 --- a/init_test.go +++ b/init_test.go @@ -41,6 +41,7 @@ func TestUnit(t *testing.T) { suite("NewManifest", testNewManifest) suite("NewManifestFromJAR", testNewManifestFromJAR) suite("MavenJARListing", testMavenJARListing) + suite("SDKMAN", testSDKMAN) suite("Versions", testVersions) suite("JVMVersions", testJVMVersion) suite.Run(t) diff --git a/jvm_version.go b/jvm_version.go index ed8034b..0050fb4 100644 --- a/jvm_version.go +++ b/jvm_version.go @@ -1,6 +1,10 @@ package libjvm import ( + "errors" + "fmt" + "os" + "path/filepath" "strings" "github.com/heroku/color" @@ -12,36 +16,76 @@ type JVMVersion struct { Logger bard.Logger } -func (jvmVersion JVMVersion) GetJVMVersion(appPath string, cr libpak.ConfigurationResolver) (string, error) { +func NewJVMVersion(logger bard.Logger) JVMVersion { + return JVMVersion{Logger: logger} +} + +func (j JVMVersion) GetJVMVersion(appPath string, cr libpak.ConfigurationResolver) (string, error) { version, explicit := cr.Resolve("BP_JVM_VERSION") + if explicit { + f := color.New(color.Faint) + j.Logger.Body(f.Sprintf("Using Java version %s from BP_JVM_VERSION", version)) + return version, nil + } - if !explicit { - manifest, err := NewManifest(appPath) - if err != nil { - return version, err - } + sdkmanrcJavaVersion, err := readJavaVersionFromSDKMANRCFile(appPath) + if err != nil { + return "", fmt.Errorf("unable to read Java version from SDMANRC file\n%w", err) + } - javaVersion := "" + if len(sdkmanrcJavaVersion) > 0 { + sdkmanrcJavaMajorVersion := extractMajorVersion(sdkmanrcJavaVersion) + f := color.New(color.Faint) + j.Logger.Body(f.Sprintf("Using Java version %s extracted from .sdkmanrc", sdkmanrcJavaMajorVersion)) + return sdkmanrcJavaMajorVersion, nil + } - buildJdkSpecVersion, ok := manifest.Get("Build-Jdk-Spec") - if ok { - javaVersion = buildJdkSpecVersion - } + mavenJavaVersion, err := readJavaVersionFromMavenMetadata(appPath) + if err != nil { + return "", fmt.Errorf("unable to read Java version from Maven metadata\n%w", err) + } - buildJdkVersion, ok := manifest.Get("Build-Jdk") - if ok { - javaVersion = buildJdkVersion - } + if len(mavenJavaVersion) > 0 { + mavenJavaMajorVersion := extractMajorVersion(mavenJavaVersion) + f := color.New(color.Faint) + j.Logger.Body(f.Sprintf("Using Java version %s extracted from MANIFEST.MF", mavenJavaMajorVersion)) + return mavenJavaMajorVersion, nil + } + + f := color.New(color.Faint) + j.Logger.Body(f.Sprintf("Using buildpack default Java version %s", version)) + return version, nil +} + +func readJavaVersionFromSDKMANRCFile(appPath string) (string, error) { + components, err := ReadSDKMANRC(filepath.Join(appPath, ".sdkmanrc")) + if err != nil && errors.Is(err, os.ErrNotExist) { + return "", nil + } else if err != nil { + return "", err + } - if len(javaVersion) > 0 { - javaVersionFromMaven := extractMajorVersion(javaVersion) - f := color.New(color.Faint) - jvmVersion.Logger.Body(f.Sprintf("Using Java version %s extracted from MANIFEST.MF", javaVersionFromMaven)) - return javaVersionFromMaven, nil + for _, component := range components { + if component.Type == "java" { + return component.Version, nil } } - return version, nil + return "", nil +} + +func readJavaVersionFromMavenMetadata(appPath string) (string, error) { + manifest, err := NewManifest(appPath) + if err != nil { + return "", err + } + + javaVersion, ok := manifest.Get("Build-Jdk-Spec") + if !ok { + javaVersion, _ = manifest.Get("Build-Jdk") + } + + return javaVersion, nil } func extractMajorVersion(version string) string { diff --git a/jvm_version_test.go b/jvm_version_test.go index 2c4b6db..022fb52 100644 --- a/jvm_version_test.go +++ b/jvm_version_test.go @@ -17,13 +17,14 @@ package libjvm_test import ( - "github.com/paketo-buildpacks/libpak" - "github.com/paketo-buildpacks/libpak/bard" "io/ioutil" "os" "path/filepath" "testing" + "github.com/paketo-buildpacks/libpak" + "github.com/paketo-buildpacks/libpak/bard" + "github.com/buildpacks/libcnb" . "github.com/onsi/gomega" "github.com/sclevine/spec" @@ -40,6 +41,11 @@ func testJVMVersion(t *testing.T, context spec.G, it spec.S) { ) it.Before(func() { + var err error + + appPath, err = ioutil.TempDir("", "application") + Expect(err).NotTo(HaveOccurred()) + buildpack = libcnb.Buildpack{ Metadata: map[string]interface{}{ "configurations": []map[string]interface{}{ @@ -53,6 +59,10 @@ func testJVMVersion(t *testing.T, context spec.G, it spec.S) { logger = bard.NewLogger(ioutil.Discard) }) + it.After(func() { + Expect(os.RemoveAll(appPath)).To(Succeed()) + }) + it("detecting JVM version from default", func() { jvmVersion := libjvm.JVMVersion{Logger: logger} @@ -85,13 +95,7 @@ func testJVMVersion(t *testing.T, context spec.G, it spec.S) { context("detecting JVM version", func() { it.Before(func() { - temp, err := prepareAppWithEntry("Build-Jdk: 1.8") - Expect(err).ToNot(HaveOccurred()) - appPath = temp - }) - - it.After(func() { - os.RemoveAll(appPath) + Expect(prepareAppWithEntry(appPath, "Build-Jdk: 1.8")).ToNot(HaveOccurred()) }) it("from manifest via Build-Jdk-Spec", func() { @@ -108,14 +112,11 @@ func testJVMVersion(t *testing.T, context spec.G, it spec.S) { context("detecting JVM version", func() { it.Before(func() { Expect(os.Setenv("BP_JVM_VERSION", "17")).To(Succeed()) - temp, err := prepareAppWithEntry("Build-Jdk: 1.8") - Expect(err).ToNot(HaveOccurred()) - appPath = temp + Expect(prepareAppWithEntry(appPath, "Build-Jdk: 1.8")).ToNot(HaveOccurred()) }) it.After(func() { Expect(os.Unsetenv("BP_JVM_VERSION")).To(Succeed()) - os.RemoveAll(appPath) }) it("prefers environment variable over manifest", func() { @@ -129,22 +130,56 @@ func testJVMVersion(t *testing.T, context spec.G, it spec.S) { }) }) + context("detecting JVM version", func() { + var sdkmanrcFile string + + it.Before(func() { + sdkmanrcFile = filepath.Join(appPath, ".sdkmanrc") + Expect(ioutil.WriteFile(sdkmanrcFile, []byte(`java=17.0.2-tem`), 0644)).To(Succeed()) + }) + + it("from .sdkmanrc file", func() { + jvmVersion := libjvm.JVMVersion{Logger: logger} + + cr, err := libpak.NewConfigurationResolver(buildpack, &logger) + Expect(err).ToNot(HaveOccurred()) + version, err := jvmVersion.GetJVMVersion(appPath, cr) + Expect(err).ToNot(HaveOccurred()) + Expect(version).To(Equal("17")) + }) + }) + + context("detecting JVM version", func() { + var sdkmanrcFile string + + it.Before(func() { + sdkmanrcFile = filepath.Join(appPath, ".sdkmanrc") + Expect(ioutil.WriteFile(sdkmanrcFile, []byte(`java=17.0.2-tem +java=11.0.2-tem`), 0644)).To(Succeed()) + }) + + it("picks first from .sdkmanrc file if there are multiple", func() { + jvmVersion := libjvm.JVMVersion{Logger: logger} + + cr, err := libpak.NewConfigurationResolver(buildpack, &logger) + Expect(err).ToNot(HaveOccurred()) + version, err := jvmVersion.GetJVMVersion(appPath, cr) + Expect(err).ToNot(HaveOccurred()) + Expect(version).To(Equal("17")) + }) + }) } -func prepareAppWithEntry(entry string) (string, error) { - temp, err := ioutil.TempDir("", "jre-app") - if err != nil { - return "", err - } - err = os.Mkdir(filepath.Join(temp, "META-INF"), 0744) +func prepareAppWithEntry(appPath, entry string) error { + err := os.Mkdir(filepath.Join(appPath, "META-INF"), 0744) if err != nil { - return "", err + return err } - manifest := filepath.Join(temp, "META-INF", "MANIFEST.MF") + manifest := filepath.Join(appPath, "META-INF", "MANIFEST.MF") manifestContent := []byte(entry) err = ioutil.WriteFile(manifest, manifestContent, 0644) if err != nil { - return "", err + return err } - return temp, nil + return nil } diff --git a/sdkman.go b/sdkman.go new file mode 100644 index 0000000..8f79c56 --- /dev/null +++ b/sdkman.go @@ -0,0 +1,76 @@ +/* + * Copyright 2018-2022 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 libjvm + +import ( + "fmt" + "io/ioutil" + "strings" +) + +// SDKInfo represents the information from each line in the `.sdkmanrc` file +type SDKInfo struct { + Type string + Version string + Vendor string +} + +// ReadSDKMANRC reads the `.sdkmanrc` format file from path and retuns the list of SDKS in it +func ReadSDKMANRC(path string) ([]SDKInfo, error) { + sdkmanrcContents, err := ioutil.ReadFile(path) + if err != nil { + return []SDKInfo{}, fmt.Errorf("unable to read SDKMANRC file at %s\n%w", path, err) + } + + sdks := []SDKInfo{} + for _, line := range strings.Split(string(sdkmanrcContents), "\n") { + if strings.TrimSpace(line) == "" { + continue + } + + parts := strings.SplitN(line, "#", 2) // strip comments + if len(parts) != 1 && len(parts) != 2 { + return []SDKInfo{}, fmt.Errorf("unable to strip comments from %q resulted in %q", line, parts) + } + + if strings.TrimSpace(parts[0]) != "" { + kv := strings.SplitN(parts[0], "=", 2) // split key=value + if len(kv) != 2 { + return []SDKInfo{}, fmt.Errorf("unable to split key/value from %q resulted in %q", parts[0], kv) + } + + versionAndVendor := []string{"", ""} + if strings.TrimSpace(kv[1]) != "" { + versionAndVendor = strings.SplitN(kv[1], "-", 2) // split optional vendor name + if len(versionAndVendor) == 1 { + versionAndVendor = append(versionAndVendor, "") + } + if len(versionAndVendor) != 2 { + return []SDKInfo{}, fmt.Errorf("unable to split vendor from %q resulted in %q", kv[1], versionAndVendor) + } + } + + sdks = append(sdks, SDKInfo{ + Type: kv[0], + Version: strings.TrimSpace(versionAndVendor[0]), + Vendor: strings.TrimSpace(versionAndVendor[1]), + }) + } + } + + return sdks, nil +} diff --git a/sdkman_test.go b/sdkman_test.go new file mode 100644 index 0000000..6866e86 --- /dev/null +++ b/sdkman_test.go @@ -0,0 +1,148 @@ +/* + * Copyright 2018-2022 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 libjvm_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "github.com/onsi/gomega" + "github.com/paketo-buildpacks/libjvm" + "github.com/sclevine/spec" +) + +func testSDKMAN(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + path string + ) + + it.Before(func() { + var err error + path, err = ioutil.TempDir("", "sdkman") + Expect(err).NotTo(HaveOccurred()) + }) + + it.After(func() { + Expect(os.RemoveAll(path)).To(Succeed()) + }) + + it("parses single entry sdkmanrc file", func() { + sdkmanrcFile := filepath.Join(path, "sdkmanrc") + Expect(ioutil.WriteFile(sdkmanrcFile, []byte(`java=17.0.2-tem`), 0644)).To(Succeed()) + + res, err := libjvm.ReadSDKMANRC(sdkmanrcFile) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]libjvm.SDKInfo{ + {Type: "java", Version: "17.0.2", Vendor: "tem"}, + })) + }) + + it("parses multiple entry sdkmanrc and doesn't care if there's overlap", func() { + sdkmanrcFile := filepath.Join(path, "sdkmanrc") + Expect(ioutil.WriteFile(sdkmanrcFile, []byte(`java=11.0.2-tem +java=17.0.2-tem`), 0644)).To(Succeed()) + + res, err := libjvm.ReadSDKMANRC(sdkmanrcFile) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]libjvm.SDKInfo{ + {Type: "java", Version: "11.0.2", Vendor: "tem"}, + {Type: "java", Version: "17.0.2", Vendor: "tem"}, + })) + }) + + context("handles comments and whitespace", func() { + it("ignores full-line comments", func() { + sdkmanrcFile := filepath.Join(path, "sdkmanrc") + Expect(ioutil.WriteFile(sdkmanrcFile, []byte(`# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +java=17.0.2-tem + # has some leading whitespace`), 0644)).To(Succeed()) + + res, err := libjvm.ReadSDKMANRC(sdkmanrcFile) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]libjvm.SDKInfo{ + {Type: "java", Version: "17.0.2", Vendor: "tem"}, + })) + }) + + it("ignores trailing-line comments", func() { + sdkmanrcFile := filepath.Join(path, "sdkmanrc") + Expect(ioutil.WriteFile(sdkmanrcFile, []byte(`java=17.0.2-tem # comment`), 0644)).To(Succeed()) + + res, err := libjvm.ReadSDKMANRC(sdkmanrcFile) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]libjvm.SDKInfo{ + {Type: "java", Version: "17.0.2", Vendor: "tem"}, + })) + }) + + it("ignores empty lines", func() { + sdkmanrcFile := filepath.Join(path, "sdkmanrc") + Expect(ioutil.WriteFile(sdkmanrcFile, []byte(` +# Enable auto-env through the sdkman_auto_env config + +java=17.0.2-tem + +`), 0644)).To(Succeed()) + + res, err := libjvm.ReadSDKMANRC(sdkmanrcFile) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]libjvm.SDKInfo{ + {Type: "java", Version: "17.0.2", Vendor: "tem"}, + })) + }) + }) + + context("handles malformed key/values", func() { + it("parses an empty value", func() { + sdkmanrcFile := filepath.Join(path, "sdkmanrc") + Expect(ioutil.WriteFile(sdkmanrcFile, []byte(`java=`), 0644)).To(Succeed()) + + res, err := libjvm.ReadSDKMANRC(sdkmanrcFile) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]libjvm.SDKInfo{ + {Type: "java", Version: "", Vendor: ""}, + })) + }) + + it("parses with an empty key", func() { + sdkmanrcFile := filepath.Join(path, "sdkmanrc") + Expect(ioutil.WriteFile(sdkmanrcFile, []byte(`=foo-vend`), 0644)).To(Succeed()) + + res, err := libjvm.ReadSDKMANRC(sdkmanrcFile) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]libjvm.SDKInfo{ + {Type: "", Version: "foo", Vendor: "vend"}, + })) + }) + + it("parses with an empty vendor", func() { + sdkmanrcFile := filepath.Join(path, "sdkmanrc") + Expect(ioutil.WriteFile(sdkmanrcFile, []byte(`foo=bar`), 0644)).To(Succeed()) + + res, err := libjvm.ReadSDKMANRC(sdkmanrcFile) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]libjvm.SDKInfo{ + {Type: "foo", Version: "bar", Vendor: ""}, + })) + }) + }) +} From 083f3bb8f9fa76b2c53d4b5710250a05ba1d8283 Mon Sep 17 00:00:00 2001 From: Daniel Mikusa Date: Wed, 18 May 2022 08:49:11 -0400 Subject: [PATCH 2/2] Force lowercase and trim whitespace when parsing SDKMAN rc file Signed-off-by: Daniel Mikusa --- sdkman.go | 4 ++-- sdkman_test.go | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/sdkman.go b/sdkman.go index 8f79c56..e7ec995 100644 --- a/sdkman.go +++ b/sdkman.go @@ -65,9 +65,9 @@ func ReadSDKMANRC(path string) ([]SDKInfo, error) { } sdks = append(sdks, SDKInfo{ - Type: kv[0], + Type: strings.ToLower(strings.TrimSpace(kv[0])), Version: strings.TrimSpace(versionAndVendor[0]), - Vendor: strings.TrimSpace(versionAndVendor[1]), + Vendor: strings.ToLower(strings.TrimSpace(versionAndVendor[1])), }) } } diff --git a/sdkman_test.go b/sdkman_test.go index 6866e86..266f9e4 100644 --- a/sdkman_test.go +++ b/sdkman_test.go @@ -55,6 +55,17 @@ func testSDKMAN(t *testing.T, context spec.G, it spec.S) { })) }) + it("parses single entry sdkmanrc file and forces lowercase", func() { + sdkmanrcFile := filepath.Join(path, "sdkmanrc") + Expect(ioutil.WriteFile(sdkmanrcFile, []byte(` jAva = 17.0.2-TEM `), 0644)).To(Succeed()) + + res, err := libjvm.ReadSDKMANRC(sdkmanrcFile) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal([]libjvm.SDKInfo{ + {Type: "java", Version: "17.0.2", Vendor: "tem"}, + })) + }) + it("parses multiple entry sdkmanrc and doesn't care if there's overlap", func() { sdkmanrcFile := filepath.Join(path, "sdkmanrc") Expect(ioutil.WriteFile(sdkmanrcFile, []byte(`java=11.0.2-tem