Skip to content

Commit de38d37

Browse files
committed
fix(finalize): build javaexec in bin/finalize for source/git buildpack usage
bin/finalize only built src/java/finalize/cli into a temp dir; packaged buildpacks got bin/javaexec from scripts/build.sh, but source/git usage (e.g. WithBuildpacks("https://github.com/cloudfoundry/java-buildpack.git")) had no bin/javaexec and finalize aborted before release completed. - bin/finalize now also builds src/java/javaexec/cli into the same temp dir and passes the path via JAVAEXEC_BINARY_PATH. - InstallJavaexecLauncher() (renamed from private) prefers JAVAEXEC_BINARY_PATH and falls back to <buildpackDir>/bin/javaexec for packaged buildpacks. - Three unit tests cover packaged path, source/git env-var override, and the missing-binary error case.
1 parent 6a3bce4 commit de38d37

7 files changed

Lines changed: 162 additions & 9 deletions

File tree

bin/finalize

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ output_dir=$(mktemp -d -t finalizeXXX)
1414
pushd $BUILDPACK_DIR > /dev/null
1515
echo "-----> Running go build finalize"
1616
GOROOT=$GoInstallDir $GoInstallDir/bin/go build -mod=vendor -o $output_dir/finalize ./src/java/finalize/cli
17+
echo "-----> Running go build javaexec"
18+
GOROOT=$GoInstallDir $GoInstallDir/bin/go build -mod=vendor -o $output_dir/javaexec ./src/java/javaexec/cli
1719
popd > /dev/null
1820

19-
$output_dir/finalize "$BUILD_DIR" "$CACHE_DIR" "$DEPS_DIR" "$DEPS_IDX" "$PROFILE_DIR"
21+
JAVAEXEC_BINARY_PATH=$output_dir/javaexec $output_dir/finalize "$BUILD_DIR" "$CACHE_DIR" "$DEPS_DIR" "$DEPS_IDX" "$PROFILE_DIR"

docs/DEVELOPING.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ Build the buildpack binaries:
9797
This creates executables in the `bin/` directory:
9898
- `bin/supply` - Staging phase binary (downloads and installs dependencies)
9999
- `bin/finalize` - Finalization phase binary (configures runtime)
100+
- `bin/javaexec` - Shell-free JVM launcher (tokenizes `JAVA_OPTS` without `eval`)
100101

101102
## Project Structure
102103

@@ -118,6 +119,7 @@ java-buildpack/
118119
│ ├── jres/ # JRE implementations (7 providers)
119120
│ ├── supply/cli/ # Supply phase entrypoint
120121
│ ├── finalize/cli/ # Finalize phase entrypoint
122+
│ ├── javaexec/cli/ # Shell-free JVM launcher entrypoint
121123
│ ├── resources/ # Resource configuration files
122124
│ └── integration/ # Integration tests
123125
├── scripts/ # Build and test scripts
@@ -156,6 +158,7 @@ Build for the default platform (Linux):
156158
```
157159
-----> Building supply for linux
158160
-----> Building finalize for linux
161+
-----> Building javaexec for linux
159162
-----> Build complete
160163
```
161164

@@ -193,6 +196,25 @@ go build -mod vendor -o bin/supply src/java/supply/cli/main.go
193196

194197
# Build finalize
195198
go build -mod vendor -o bin/finalize src/java/finalize/cli/main.go
199+
200+
# Build javaexec (shell-free JVM launcher, required at runtime)
201+
go build -mod vendor -o bin/javaexec src/java/javaexec/cli/main.go
202+
```
203+
204+
### Source/Git Buildpack Usage
205+
206+
When deploying with a git URL (`cf push -b https://github.com/.../java-buildpack.git`),
207+
Cloud Foundry runs `bin/finalize` directly from the cloned source. In that mode
208+
`bin/javaexec` does not exist (only packaged buildpacks have it). `bin/finalize`
209+
therefore builds `javaexec` into a temp directory alongside the finalize binary
210+
and passes the path via the `JAVAEXEC_BINARY_PATH` environment variable.
211+
`InstallJavaexecLauncher()` prefers this override and falls back to
212+
`bin/javaexec` for packaged buildpacks.
213+
214+
To verify this path locally (no CF required):
215+
216+
```bash
217+
bash scripts/test-javaexec-source-path.sh
196218
```
197219

198220
## Running Tests

docs/design.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,12 @@ During the finalize phase (`bin/finalize`), the buildpack:
186186
1. **Finalizes JRE**: Configures JVM options, memory calculator
187187
2. **Finalizes Frameworks**: Adds agent paths, system properties
188188
3. **Finalizes Container**: Generates launch command
189+
4. **Installs `javaexec`**: Copies the shell-free JVM launcher into
190+
`$DEPS_DIR/<idx>/bin/javaexec`. The generated start command invokes it
191+
instead of `eval "exec java $JAVA_OPTS ..."` so that `JAVA_OPTS` is
192+
tokenized without shell interpretation. For source/git buildpack usage
193+
`bin/finalize` builds `javaexec` on the fly and passes its path via
194+
`JAVAEXEC_BINARY_PATH` (see [DEVELOPING.md](DEVELOPING.md#sourcegit-buildpack-usage)).
189195

190196
Components can:
191197
- Read installed dependencies from `$DEPS_DIR/<idx>/`
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/bin/bash
2+
# Manual smoke test for the bin/finalize source/git buildpack path.
3+
# Simulates what bin/finalize does: build javaexec into a temp dir and pass
4+
# the path via JAVAEXEC_BINARY_PATH. Verifies the binary builds, that
5+
# InstallJavaexecLauncher picks up the override, and that javaexec tokenizes
6+
# JAVA_OPTS correctly when actually invoked.
7+
set -euo pipefail
8+
9+
BUILDPACK_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
10+
cd "$BUILDPACK_DIR"
11+
12+
tmpdir=$(mktemp -d)
13+
trap 'rm -rf "$tmpdir"' EXIT
14+
15+
echo "==> [1/4] Build javaexec from source (as bin/finalize now does)"
16+
go build -mod=vendor -o "$tmpdir/javaexec" ./src/java/javaexec/cli
17+
echo " OK: $tmpdir/javaexec ($(wc -c < "$tmpdir/javaexec") bytes)"
18+
19+
echo ""
20+
echo "==> [2/4] Build finalize from source"
21+
go build -mod=vendor -o "$tmpdir/finalize" ./src/java/finalize/cli
22+
echo " OK: $tmpdir/finalize"
23+
24+
echo ""
25+
echo "==> [3/4] Unit tests: InstallJavaexecLauncher with JAVAEXEC_BINARY_PATH override"
26+
go test ./src/java/finalize/ -count=1 -v -run "javaexec launcher" 2>&1 | grep -E "PASS|FAIL|RUN|---"
27+
28+
echo ""
29+
echo "==> [4/4] Tokenization smoke test: run javaexec with a fake java binary"
30+
31+
# Fake java: prints each received argument on its own line.
32+
cat > "$tmpdir/fake-java" << 'EOF'
33+
#!/bin/bash
34+
printf '%s\n' "$@"
35+
EOF
36+
chmod +x "$tmpdir/fake-java"
37+
38+
# Quoted value with spaces → one token; cron expr with * → literal; $(...) → not executed.
39+
JAVA_OPTS='-Dfoo="bar baz" -DcronSched="0 */7 * * * *" -Dwhere=$(hostname)' \
40+
"$tmpdir/javaexec" "$tmpdir/fake-java" -jar app.jar 2>/dev/null > "$tmpdir/actual.txt"
41+
42+
expected="-Dfoo=bar baz
43+
-DcronSched=0 */7 * * * *
44+
-Dwhere=\$(hostname)
45+
-jar
46+
app.jar"
47+
48+
actual=$(cat "$tmpdir/actual.txt")
49+
50+
if [ "$actual" = "$expected" ]; then
51+
echo " OK: all tokens correct"
52+
else
53+
echo " FAIL: unexpected output"
54+
echo " expected:"
55+
printf '%s\n' "$expected" | sed 's/^/ /'
56+
echo " got:"
57+
printf '%s\n' "$actual" | sed 's/^/ /'
58+
exit 1
59+
fi
60+
61+
echo ""
62+
echo "PASS: source/git buildpack path works."
63+
echo " bin/finalize builds javaexec and passes it via JAVAEXEC_BINARY_PATH."
64+
echo " javaexec tokenizes JAVA_OPTS correctly without shell execution."

src/integration/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ property that must hold in a real container launch:
151151
- **The `javaexec` launcher actually starts the JVM** for the affected containers,
152152
including `Play` (previously `eval exec java $JAVA_OPTS ...`).
153153

154+
**Source/git buildpack path** (`bin/finalize` building `javaexec` on the fly) is
155+
**not covered** by integration tests here: they use a packaged zip where `bin/javaexec`
156+
is pre-built by `scripts/build.sh`. The source/git path is covered by unit tests in
157+
[`../java/finalize/finalize_test.go`](../java/finalize/finalize_test.go) and can be
158+
verified locally with `bash scripts/test-javaexec-source-path.sh`.
159+
154160
## Writing New Tests
155161

156162
To add a new test:

src/java/finalize/finalize.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func Run(f *Finalizer) error {
115115
}
116116

117117
// Install the javaexec launcher binary used by the start command.
118-
if err := f.installJavaexecLauncher(); err != nil {
118+
if err := f.InstallJavaexecLauncher(); err != nil {
119119
f.Log.Error("Failed to install javaexec launcher: %s", err.Error())
120120
return err
121121
}
@@ -198,13 +198,20 @@ func (f *Finalizer) finalizeFrameworks(ctx *common.Context) error {
198198
return nil
199199
}
200200

201-
// installJavaexecLauncher copies the javaexec launcher binary from the
202-
// buildpack's bin/ directory into the dependency directory so it is present in
203-
// the droplet at runtime. The start command (see containers.JavaExecCommand)
204-
// invokes it as $DEPS_DIR/<idx>/bin/javaexec to tokenize JAVA_OPTS without a
205-
// shell. The binary is required: if it is missing the generated start command
206-
// would fail at launch, so this returns an error rather than continuing.
207-
func (f *Finalizer) installJavaexecLauncher() error {
201+
// InstallJavaexecLauncher copies the javaexec launcher binary into the
202+
// dependency directory so it is present in the droplet at runtime. The start
203+
// command (see containers.JavaExecCommand) invokes it as
204+
// $DEPS_DIR/<idx>/bin/javaexec to tokenize JAVA_OPTS without a shell.
205+
//
206+
// Source path resolution (first match wins):
207+
// 1. JAVAEXEC_BINARY_PATH env var — set by bin/finalize for source/git usage
208+
// where javaexec is built into a temp dir alongside the finalize binary.
209+
// 2. <buildpackDir>/bin/javaexec — present in packaged buildpacks built by
210+
// scripts/build.sh.
211+
//
212+
// The binary is required: missing it means the generated start command would
213+
// fail at launch, so this returns an error rather than continuing.
214+
func (f *Finalizer) InstallJavaexecLauncher() error {
208215
buildpackDir := f.BuildpackDir
209216
if buildpackDir == "" {
210217
var err error
@@ -215,6 +222,13 @@ func (f *Finalizer) installJavaexecLauncher() error {
215222
}
216223

217224
src := filepath.Join(buildpackDir, "bin", "javaexec")
225+
// For source/git buildpack usage the bin/finalize wrapper builds javaexec
226+
// into a temp directory and passes the path here so the source checkout is
227+
// not mutated. Prefer the override; fall back to <buildpackDir>/bin/javaexec
228+
// for packaged buildpacks where scripts/build.sh provides the binary.
229+
if override := os.Getenv("JAVAEXEC_BINARY_PATH"); override != "" {
230+
src = override
231+
}
218232
data, err := os.ReadFile(src)
219233
if err != nil {
220234
return fmt.Errorf("javaexec launcher binary not found at %s (required by the start command): %w", src, err)

src/java/finalize/finalize_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,43 @@ dependencies: []
242242
Expect(depDir).To(ContainSubstring(depsDir))
243243
})
244244
})
245+
246+
Describe("javaexec launcher installation", func() {
247+
It("installs launcher from buildpack bin/ for packaged buildpack usage", func() {
248+
// Default path: <buildpackDir>/bin/javaexec already written in BeforeEach.
249+
Expect(finalizer.InstallJavaexecLauncher()).To(Succeed())
250+
installed := filepath.Join(stager.DepDir(), "bin", "javaexec")
251+
Expect(installed).To(BeARegularFile())
252+
})
253+
254+
It("installs launcher from JAVAEXEC_BINARY_PATH for source/git buildpack usage", func() {
255+
// Simulate bin/finalize wrapper: javaexec built into a temp dir, path passed via env var.
256+
// <buildpackDir>/bin/javaexec is still present from BeforeEach, but the env var must take precedence.
257+
altBinary, err := os.CreateTemp("", "javaexec-alt-*")
258+
Expect(err).NotTo(HaveOccurred())
259+
defer os.Remove(altBinary.Name())
260+
_, err = altBinary.WriteString("#!/bin/sh\n# alt\n")
261+
Expect(err).NotTo(HaveOccurred())
262+
Expect(altBinary.Close()).To(Succeed())
263+
264+
os.Setenv("JAVAEXEC_BINARY_PATH", altBinary.Name())
265+
defer os.Unsetenv("JAVAEXEC_BINARY_PATH")
266+
267+
Expect(finalizer.InstallJavaexecLauncher()).To(Succeed())
268+
installed := filepath.Join(stager.DepDir(), "bin", "javaexec")
269+
Expect(installed).To(BeARegularFile())
270+
content, err := os.ReadFile(installed)
271+
Expect(err).NotTo(HaveOccurred())
272+
Expect(string(content)).To(ContainSubstring("# alt"))
273+
})
274+
275+
It("fails when launcher is missing and JAVAEXEC_BINARY_PATH is not set", func() {
276+
// Point BuildpackDir at an empty dir so bin/javaexec doesn't exist.
277+
emptyDir, err := os.MkdirTemp("", "finalize-no-javaexec")
278+
Expect(err).NotTo(HaveOccurred())
279+
defer os.RemoveAll(emptyDir)
280+
finalizer.BuildpackDir = emptyDir
281+
Expect(finalizer.InstallJavaexecLauncher()).NotTo(Succeed())
282+
})
283+
})
245284
})

0 commit comments

Comments
 (0)