From 7f113cd33a86494a7599642822aea6e02bb533af Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Thu, 9 Jul 2026 10:36:18 +0000 Subject: [PATCH 1/3] fix(java-cfenv): add the mirrored jar to the classpath + guard the cloud-profile artifact main now ships java-cfenv-all (the aggregate module carrying CloudProfileApplicationListener), mirrored as java-cfenv___.jar (underscores). But Finalize globbed `java-cfenv-*.jar` (hyphen), which does not match the underscore mirror name, so the jar was installed under deps/ but never added to the profile.d CLASSPATH -- the `cloud` profile still would not activate at runtime. - Finalize: glob `java-cfenv*.jar` so both the Maven name (java-cfenv-all-.jar) and the CF mirror name match. - Add a Finalize test using the real underscore mirror filename. - Add a network-gated artifact test (build tag cfenv_artifact) asserting the shipped jar registers CloudProfileApplicationListener in spring.factories. Refs #1349. --- src/java/frameworks/java_cf_env.go | 4 +- .../frameworks/java_cf_env_artifact_test.go | 163 ++++++++++++++++++ src/java/frameworks/java_cf_env_test.go | 24 +++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 src/java/frameworks/java_cf_env_artifact_test.go diff --git a/src/java/frameworks/java_cf_env.go b/src/java/frameworks/java_cf_env.go index 48bf3f91f2..7bb41fdf36 100644 --- a/src/java/frameworks/java_cf_env.go +++ b/src/java/frameworks/java_cf_env.go @@ -86,7 +86,9 @@ func (j *JavaCfEnvFramework) Supply() error { func (j *JavaCfEnvFramework) Finalize() error { // Add the JAR to additional libraries (classpath) javaCfEnvDir := filepath.Join(j.context.Stager.DepDir(), "java_cf_env") - jarPattern := filepath.Join(javaCfEnvDir, "java-cfenv-*.jar") + // Match both the Maven name (java-cfenv-all-.jar) and the CF mirror name + // (java-cfenv___.jar, underscores) — a hyphen-only glob misses the latter. + jarPattern := filepath.Join(javaCfEnvDir, "java-cfenv*.jar") matches, err := filepath.Glob(jarPattern) if err != nil || len(matches) == 0 { diff --git a/src/java/frameworks/java_cf_env_artifact_test.go b/src/java/frameworks/java_cf_env_artifact_test.go new file mode 100644 index 0000000000..64f8f1f766 --- /dev/null +++ b/src/java/frameworks/java_cf_env_artifact_test.go @@ -0,0 +1,163 @@ +//go:build cfenv_artifact + +// This test verifies a *necessary condition* for the buildpack's automatic "cloud" +// Spring profile behaviour: the java-cfenv artifact the manifest ships must register +// io.pivotal.cfenv.profile.CloudProfileApplicationListener as a Spring ApplicationListener +// (via META-INF/spring.factories). That listener lives only in the java-cfenv-all module; +// the bare java-cfenv core module does not carry it, which silently drops the "cloud" +// profile (see cloudfoundry/java-buildpack#1349). +// +// It downloads each shipped jar, verifies its SHA-256 against the manifest (which also +// guards against duplicate/mismatched mirror entries), then inspects spring.factories. +// +// It is network-bound, so it is excluded from the default unit suite by the cfenv_artifact +// build tag. Run explicitly: +// +// go test -tags cfenv_artifact ./src/java/frameworks/ +package frameworks_test + +import ( + "archive/zip" + "crypto/sha256" + "encoding/hex" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "gopkg.in/yaml.v2" +) + +const cloudProfileListener = "io.pivotal.cfenv.profile.CloudProfileApplicationListener" + +type manifestDependency struct { + Name string `yaml:"name"` + Version string `yaml:"version"` + URI string `yaml:"uri"` + SHA256 string `yaml:"sha256"` +} + +type manifestFile struct { + Dependencies []manifestDependency `yaml:"dependencies"` +} + +var _ = Describe("Java CF Env artifact", func() { + var deps []manifestDependency + + BeforeEach(func() { + manifestPath, err := filepath.Abs(filepath.Join("..", "..", "..", "manifest.yml")) + Expect(err).NotTo(HaveOccurred()) + + raw, err := os.ReadFile(manifestPath) + Expect(err).NotTo(HaveOccurred()) + + var m manifestFile + Expect(yaml.Unmarshal(raw, &m)).To(Succeed()) + + for _, d := range m.Dependencies { + if d.Name == "java-cfenv" && d.URI != "" { + deps = append(deps, d) + } + } + Expect(deps).NotTo(BeEmpty(), "no java-cfenv dependency entries with a uri found in manifest.yml") + }) + + It("ships a jar that registers the cloud-profile ApplicationListener", func() { + for _, dep := range deps { + By("checking java-cfenv " + dep.Version) + + jar := downloadToTemp(dep.URI) + defer os.Remove(jar) + + Expect(fileSHA256(jar)).To(Equal(dep.SHA256), + "java-cfenv %s: downloaded bytes do not match manifest sha256 (%s)", dep.Version, dep.URI) + + factories, ok := readZipEntry(jar, "META-INF/spring.factories") + Expect(ok).To(BeTrue(), + "java-cfenv %s: jar has no META-INF/spring.factories — this is the bare core module, "+ + "which cannot auto-activate the 'cloud' profile; ship java-cfenv-all instead", dep.Version) + + Expect(factories).To(ContainSubstring(cloudProfileListener), + "java-cfenv %s: spring.factories does not register %s — the 'cloud' profile will not be "+ + "activated automatically; ship java-cfenv-all instead", dep.Version, cloudProfileListener) + + Expect(registersAsApplicationListener(factories, cloudProfileListener)).To(BeTrue(), + "java-cfenv %s: %s is present but not registered under "+ + "org.springframework.context.ApplicationListener", dep.Version, cloudProfileListener) + } + }) +}) + +// registersAsApplicationListener reports whether class is listed under the +// org.springframework.context.ApplicationListener key, accounting for backslash +// line continuations used in spring.factories files. +func registersAsApplicationListener(factories, class string) bool { + const key = "org.springframework.context.ApplicationListener" + joined := strings.ReplaceAll(factories, "\\\n", "") + for _, line := range strings.Split(joined, "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, key+"=") { + return strings.Contains(line, class) + } + } + return false +} + +func downloadToTemp(uri string) string { + GinkgoHelper() + + client := &http.Client{Timeout: 60 * time.Second} + resp, err := client.Get(uri) + Expect(err).NotTo(HaveOccurred(), "download %s", uri) + defer resp.Body.Close() + Expect(resp.StatusCode).To(Equal(http.StatusOK), "download %s returned %d", uri, resp.StatusCode) + + f, err := os.CreateTemp("", "java-cfenv-*.jar") + Expect(err).NotTo(HaveOccurred()) + defer f.Close() + + _, err = io.Copy(f, resp.Body) + Expect(err).NotTo(HaveOccurred()) + + return f.Name() +} + +func fileSHA256(path string) string { + GinkgoHelper() + + f, err := os.Open(path) + Expect(err).NotTo(HaveOccurred()) + defer f.Close() + + h := sha256.New() + _, err = io.Copy(h, f) + Expect(err).NotTo(HaveOccurred()) + + return hex.EncodeToString(h.Sum(nil)) +} + +func readZipEntry(jarPath, entry string) (string, bool) { + GinkgoHelper() + + r, err := zip.OpenReader(jarPath) + Expect(err).NotTo(HaveOccurred()) + defer r.Close() + + for _, f := range r.File { + if f.Name == entry { + rc, err := f.Open() + Expect(err).NotTo(HaveOccurred()) + defer rc.Close() + + data, err := io.ReadAll(rc) + Expect(err).NotTo(HaveOccurred()) + return string(data), true + } + } + return "", false +} diff --git a/src/java/frameworks/java_cf_env_test.go b/src/java/frameworks/java_cf_env_test.go index b307be606f..44353ba17a 100644 --- a/src/java/frameworks/java_cf_env_test.go +++ b/src/java/frameworks/java_cf_env_test.go @@ -272,5 +272,29 @@ var _ = Describe("Java CF Env", func() { Expect(string(content)).To(ContainSubstring("java-cfenv-2.5.0.jar")) }) }) + + Context("when the mirrored, underscore-named JAR is installed", func() { + // The CF dependency mirror names the artifact + // java-cfenv___.jar (underscores), not the + // hyphenated Maven name. Finalize must still add it to the classpath. + BeforeEach(func() { + javaCfEnvDir := filepath.Join(depsDir, "0", "java_cf_env") + Expect(os.MkdirAll(javaCfEnvDir, 0755)).To(Succeed()) + Expect(os.WriteFile( + filepath.Join(javaCfEnvDir, "java-cfenv_3.5.1_linux_noarch_any-stack_696225e3.jar"), + []byte("fake jar"), + 0644, + )).To(Succeed()) + }) + + It("adds the mirrored JAR to the profile.d CLASSPATH", func() { + Expect(fw.Finalize()).To(Succeed()) + scriptPath := filepath.Join(depsDir, "0", "profile.d", "java_cf_env.sh") + Expect(scriptPath).To(BeAnExistingFile()) + content, err := os.ReadFile(scriptPath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(ContainSubstring("java-cfenv_3.5.1_linux_noarch_any-stack_696225e3.jar")) + }) + }) }) }) From 0eb20a894be6cb0730b360e21b6900a46c762dff Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Thu, 9 Jul 2026 10:58:10 +0000 Subject: [PATCH 2/3] test(java-cfenv): reset deps slice in BeforeEach to avoid accumulation across re-runs --- src/java/frameworks/java_cf_env_artifact_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/java/frameworks/java_cf_env_artifact_test.go b/src/java/frameworks/java_cf_env_artifact_test.go index 64f8f1f766..06a5886019 100644 --- a/src/java/frameworks/java_cf_env_artifact_test.go +++ b/src/java/frameworks/java_cf_env_artifact_test.go @@ -50,6 +50,8 @@ var _ = Describe("Java CF Env artifact", func() { var deps []manifestDependency BeforeEach(func() { + deps = nil + manifestPath, err := filepath.Abs(filepath.Join("..", "..", "..", "manifest.yml")) Expect(err).NotTo(HaveOccurred()) From ff26527d60c3b51082b7ec5f229a4e0e13d9d0df Mon Sep 17 00:00:00 2001 From: Peter Paul Bakker Date: Thu, 9 Jul 2026 11:01:00 +0000 Subject: [PATCH 3/3] docs(java-cfenv): clarify Finalize glob comment (mirror underscore name is what ships today) --- src/java/frameworks/java_cf_env.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/java/frameworks/java_cf_env.go b/src/java/frameworks/java_cf_env.go index 7bb41fdf36..8c0a0fceb8 100644 --- a/src/java/frameworks/java_cf_env.go +++ b/src/java/frameworks/java_cf_env.go @@ -86,8 +86,9 @@ func (j *JavaCfEnvFramework) Supply() error { func (j *JavaCfEnvFramework) Finalize() error { // Add the JAR to additional libraries (classpath) javaCfEnvDir := filepath.Join(j.context.Stager.DepDir(), "java_cf_env") - // Match both the Maven name (java-cfenv-all-.jar) and the CF mirror name - // (java-cfenv___.jar, underscores) — a hyphen-only glob misses the latter. + // The CF mirror installs the jar as java-cfenv___.jar (underscores). + // Glob java-cfenv*.jar so this — and a hyphenated Maven name, if the uri is ever + // pointed directly at Maven Central — both match. jarPattern := filepath.Join(javaCfEnvDir, "java-cfenv*.jar") matches, err := filepath.Glob(jarPattern)