Skip to content

Commit 66041b7

Browse files
committed
bugfix: show AOT stderr on failure and keep AOT generation mandatory
The jlink build was silently swallowing JVM stderr with 2>/dev/null, making AOT failures impossible to diagnose on CI. AOT cache generation was also made optional as a workaround, which masked the real issue. Fix: capture stderr to a temp file. On success, discard it (suppressing harmless JVM pre-load warnings). On failure, print the captured stderr then fail fast with a clear error message. AOT cache generation remains mandatory. Tests added (tests/build-jlink.bats + tests/aot-harness.sh): - JVM stderr from a failing AOT command is visible to the caller - Build exits non-zero when java AOT recording fails - Script-level error message appears on failure - Successful AOT recording does not fail the build
1 parent 0c9256a commit 66041b7

3 files changed

Lines changed: 178 additions & 8 deletions

File tree

client/build-jlink.sh

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -301,22 +301,34 @@ generate_startup_archives() {
301301
# replacing 20 separate JVM launches (~19s -> ~1s).
302302
log "Recording AOT training data..."
303303
# Set environment variables required by MainJvmScope so handlers can initialize.
304-
CLAUDE_PROJECT_DIR="${PROJECT_DIR%/*}" CLAUDE_PLUGIN_ROOT="${PROJECT_DIR%/*}/plugin" \
305-
"$java_bin" \
306-
-XX:AOTMode=record \
307-
-XX:AOTConfiguration="$aot_config" \
308-
-m "$(handler_main AotTraining)" \
309-
2>/dev/null || error "Failed to record AOT training data"
304+
# Capture stderr: suppress expected JVM AOT warnings on success, show them on failure.
305+
local aot_stderr
306+
aot_stderr=$(mktemp)
307+
# shellcheck disable=SC2064
308+
trap "rm -f '$aot_stderr'" RETURN
309+
if ! CLAUDE_PROJECT_DIR="${PROJECT_DIR%/*}" CLAUDE_PLUGIN_ROOT="${PROJECT_DIR%/*}/plugin" \
310+
"$java_bin" \
311+
-XX:AOTMode=record \
312+
-XX:AOTConfiguration="$aot_config" \
313+
-m "$(handler_main AotTraining)" \
314+
2>"$aot_stderr"; then
315+
cat "$aot_stderr" >&2
316+
error "Failed to record AOT training data"
317+
fi
318+
rm -f "$aot_stderr"
319+
trap - RETURN
310320

311321
[[ -f "$aot_config" ]] || error "AOT configuration file not created: $aot_config"
312322

313-
"$java_bin" \
323+
if ! "$java_bin" \
314324
-XX:AOTMode=create \
315325
-XX:AOTConfiguration="$aot_config" \
316326
-XX:AOTCache="$aot_cache" \
317327
-XX:+AOTClassLinking \
318328
-m "$(handler_main PreToolUseHook)" \
319-
2>/dev/null || error "Failed to create AOT cache"
329+
2>&1; then
330+
error "Failed to create AOT cache"
331+
fi
320332

321333
rm -f "$aot_config"
322334
log " AOT cache: $(du -h "$aot_cache" | cut -f1)"

tests/aot-harness.sh

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
# Copyright (c) 2026 Gili Tzabari. All rights reserved.
3+
#
4+
# Licensed under the CAT Commercial License.
5+
# See LICENSE.md in the project root for license terms.
6+
#
7+
# aot-harness.sh - Test harness that isolates generate_startup_archives logic.
8+
#
9+
# This mirrors the AOT recording pattern in build-jlink.sh so tests can validate
10+
# error reporting behavior without running a full jlink build.
11+
#
12+
# Usage:
13+
# aot-harness.sh <OUTPUT_DIR>
14+
#
15+
# Exit code:
16+
# 0 AOT recording and cache creation succeeded
17+
# 1 AOT recording or cache creation failed (with error details on stderr)
18+
19+
set -euo pipefail
20+
21+
OUTPUT_DIR="${1:?Usage: aot-harness.sh <OUTPUT_DIR>}"
22+
java_bin="${OUTPUT_DIR}/bin/java"
23+
aot_config="${OUTPUT_DIR}/lib/server/aot-config.aotconf"
24+
aot_cache="${OUTPUT_DIR}/lib/server/aot-cache.aot"
25+
26+
log() { echo "[aot-harness] $*"; }
27+
error() { echo "[aot-harness] ERROR: $*" >&2; exit 1; }
28+
29+
MODULE_NAME="io.github.cowwoc.cat.hooks"
30+
31+
handler_main() {
32+
echo "${MODULE_NAME}/${MODULE_NAME}.$1"
33+
}
34+
35+
log "Recording AOT training data..."
36+
aot_stderr=$(mktemp)
37+
trap 'rm -f "$aot_stderr"' EXIT
38+
39+
if ! CLAUDE_PROJECT_DIR="${OUTPUT_DIR}" CLAUDE_PLUGIN_ROOT="${OUTPUT_DIR}/plugin" \
40+
"$java_bin" \
41+
-XX:AOTMode=record \
42+
-XX:AOTConfiguration="$aot_config" \
43+
-m "$(handler_main AotTraining)" \
44+
2>"$aot_stderr"; then
45+
cat "$aot_stderr" >&2
46+
error "Failed to record AOT training data"
47+
fi
48+
rm -f "$aot_stderr"
49+
trap - EXIT
50+
51+
[[ -f "$aot_config" ]] || error "AOT configuration file not created: $aot_config"
52+
53+
if ! "$java_bin" \
54+
-XX:AOTMode=create \
55+
-XX:AOTConfiguration="$aot_config" \
56+
-XX:AOTCache="$aot_cache" \
57+
-m "$(handler_main PreToolUseHook)" \
58+
2>&1; then
59+
error "Failed to create AOT cache"
60+
fi
61+
62+
rm -f "$aot_config"
63+
log "AOT cache: $(du -h "$aot_cache" | cut -f1)"
64+
log "Startup archives complete"

tests/build-jlink.bats

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#!/usr/bin/env bats
2+
# Copyright (c) 2026 Gili Tzabari. All rights reserved.
3+
#
4+
# Licensed under the CAT Commercial License.
5+
# See LICENSE.md in the project root for license terms.
6+
#
7+
# Tests for client/build-jlink.sh AOT error reporting behavior.
8+
# These tests use a purpose-built AOT test harness script to exercise the
9+
# generate_startup_archives logic in isolation.
10+
11+
# The AOT harness is a minimal script that mirrors the error-reporting pattern
12+
# used by generate_startup_archives in build-jlink.sh. Testing with the harness
13+
# keeps tests fast and eliminates the need for a real jlink build.
14+
15+
HARNESS="$BATS_TEST_DIRNAME/aot-harness.sh"
16+
17+
setup() {
18+
FAKE_BIN_DIR="$(mktemp -d)"
19+
}
20+
21+
teardown() {
22+
rm -rf "${FAKE_BIN_DIR:-}"
23+
}
24+
25+
# Creates a fake java binary in FAKE_BIN_DIR/bin/java with configurable behavior.
26+
#
27+
# Parameters:
28+
# $1 exit code the fake java should return for -XX:AOTMode=record (default 0)
29+
# $2 stderr message the fake java should emit (default empty)
30+
create_fake_java() {
31+
local record_exit="${1:-0}"
32+
local stderr_msg="${2:-}"
33+
local aot_conf_path="$FAKE_BIN_DIR/lib/server/aot-config.aotconf"
34+
35+
mkdir -p "$FAKE_BIN_DIR/bin" "$FAKE_BIN_DIR/lib/server"
36+
37+
cat > "$FAKE_BIN_DIR/bin/java" <<EOF
38+
#!/bin/sh
39+
for arg in "\$@"; do
40+
case "\$arg" in
41+
-XX:AOTMode=record)
42+
if [ -n "${stderr_msg}" ]; then
43+
echo "${stderr_msg}" >&2
44+
fi
45+
[ "${record_exit}" -eq 0 ] && touch "${aot_conf_path}"
46+
exit ${record_exit}
47+
;;
48+
-XX:AOTMode=create)
49+
exit 0
50+
;;
51+
esac
52+
done
53+
exit 0
54+
EOF
55+
chmod +x "$FAKE_BIN_DIR/bin/java"
56+
}
57+
58+
# ============================================================================
59+
# AOT error output tests
60+
# ============================================================================
61+
62+
@test "aot-harness: stderr from failing java appears in combined output" {
63+
create_fake_java 1 "CLAUDE_SESSION_ID is not set"
64+
65+
run bash "$HARNESS" "$FAKE_BIN_DIR" 2>&1
66+
67+
[[ "$output" == *"CLAUDE_SESSION_ID is not set"* ]] || \
68+
{ echo "Expected JVM stderr in output. Got: $output"; false; }
69+
}
70+
71+
@test "aot-harness: build fails when java AOT recording exits non-zero" {
72+
create_fake_java 1 "fatal JVM error"
73+
74+
run bash "$HARNESS" "$FAKE_BIN_DIR"
75+
76+
[ "$status" -ne 0 ]
77+
}
78+
79+
@test "aot-harness: build reports script-level error when AOT recording fails" {
80+
create_fake_java 1 ""
81+
82+
run bash "$HARNESS" "$FAKE_BIN_DIR" 2>&1
83+
84+
[[ "$output" == *"Failed to record AOT"* ]] || \
85+
{ echo "Expected 'Failed to record AOT' message. Got: $output"; false; }
86+
}
87+
88+
@test "aot-harness: successful AOT recording does not fail the build" {
89+
create_fake_java 0 ""
90+
91+
run bash "$HARNESS" "$FAKE_BIN_DIR"
92+
93+
[ "$status" -eq 0 ]
94+
}

0 commit comments

Comments
 (0)