Skip to content

Commit

Permalink
Support SDKMAN RC files
Browse files Browse the repository at this point in the history
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 <dmikusa@vmware.com>
  • Loading branch information
Daniel Mikusa committed May 16, 2022
1 parent 743dda2 commit cc0a400
Show file tree
Hide file tree
Showing 6 changed files with 349 additions and 45 deletions.
2 changes: 1 addition & 1 deletion build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
86 changes: 65 additions & 21 deletions jvm_version.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package libjvm

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/heroku/color"
Expand All @@ -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 {
Expand Down
81 changes: 58 additions & 23 deletions jvm_version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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{}{
Expand All @@ -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}

Expand Down Expand Up @@ -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() {
Expand All @@ -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() {
Expand All @@ -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
}
76 changes: 76 additions & 0 deletions sdkman.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit cc0a400

Please sign in to comment.