From 5ed5445bb5743614d187c21dcef08d57c55a40d2 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Fri, 2 Oct 2026 16:02:45 -0400 Subject: [PATCH] fixes #186 remove gradle build --- .project | 6 - Makefile | 7 - README.md | 37 +++++ bootstrap | 2 +- build.gradle | 49 ------- build_graalvm.sh | 21 --- gradle.properties | 8 -- gradle/wrapper/gradle-wrapper.properties | 6 - gradlew | 169 ----------------------- gradlew.bat | 84 ----------- native-image-linux.sh | 80 ++++++----- tests/test_native_image_script.py | 108 +++++++++++++++ 12 files changed, 188 insertions(+), 389 deletions(-) delete mode 100644 Makefile delete mode 100644 build.gradle delete mode 100644 build_graalvm.sh delete mode 100644 gradle.properties delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100755 gradlew delete mode 100644 gradlew.bat create mode 100644 tests/test_native_image_script.py diff --git a/.project b/.project index a201aca..7972f00 100644 --- a/.project +++ b/.project @@ -10,11 +10,6 @@ - - org.eclipse.buildship.core.gradleprojectbuilder - - - org.eclipse.m2e.core.maven2Builder @@ -24,7 +19,6 @@ org.eclipse.jdt.core.javanature org.eclipse.m2e.core.maven2Nature - org.eclipse.buildship.core.gradleprojectnature diff --git a/Makefile b/Makefile deleted file mode 100644 index 8a069e1..0000000 --- a/Makefile +++ /dev/null @@ -1,7 +0,0 @@ - -CUR_DIR := $(abspath $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))) - -build-MarketStoreProductsGetFunction: - cd $(CUR_DIR) && ./gradlew build - cp $(CUR_DIR)/build/graalvm/server $(ARTIFACTS_DIR) - cp $(CUR_DIR)/bootstrap $(ARTIFACTS_DIR) diff --git a/README.md b/README.md index 69a0c00..e61068e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,40 @@ +## Build and package + +Use Maven with JDK 25 (the Java version declared in `pom.xml`): + +```sh +mvn clean verify +``` + +The executable shaded JAR is `target/lambda-native-.jar`, with the +version taken from `pom.xml`. Customers can use this JAR with a compatible +GraalVM Native Image installation to build their own native binary. + +To build the Linux native binary and AWS Lambda custom-runtime ZIP, install +Docker and `zip`, then run: + +```sh +bash native-image-linux.sh +``` + +The script runs `mvn clean install`, reads the JAR name and Java version from +Maven, and uses `ghcr.io/graalvm/native-image-community:`. +Set `GRAALVM_IMAGE` to select a pinned image compatible with that Java version. +Docker builds for its host architecture; use a host matching your Lambda +architecture and verify the resulting binary against your chosen Lambda runtime. +The ZIP is `lambda-native-custom-runtime.zip` and contains executable +`lambda-native` and `bootstrap` files. HTTP and HTTPS protocols are enabled. +Build or packaging failures stop the script with a nonzero exit status. + +Run the native-build script regression tests separately with Python 3 and `zip`: + +```sh +python3 tests/test_native_image_script.py +``` + +These tests stub Maven and Docker to check packaging and failure handling. +They are not run by Maven or the current CI workflow. + ### Test with Main We have provided a Main class in the src folder for testing all the middleware handlers with a sample request. You can run the Main class within your IDE with debug mode to see how each middleware handler works. diff --git a/bootstrap b/bootstrap index 58dbd30..64f143e 100644 --- a/bootstrap +++ b/bootstrap @@ -1,3 +1,3 @@ #!/bin/sh -set -euo pipefail +set -eu ./lambda-native -Dlight-4j-config-dir=/opt -Dlogback.configurationFile=/opt/logback.xml diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 4196ecc..0000000 --- a/build.gradle +++ /dev/null @@ -1,49 +0,0 @@ - -plugins { - id 'java' - id "com.github.johnrengelman.shadow" version "5.2.0" -} - -repositories { - mavenCentral() - mavenLocal() - maven { - url 'https://oss.sonatype.org/content/repositories/snapshots/' - } -} - -dependencies { - implementation "com.networknt:custom-runtime:$light4jVersion" - implementation "org.slf4j:slf4j-api:$slf4jVersion" - implementation "ch.qos.logback:logback-classic:$logbackVersion" - implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" - implementation "com.amazonaws:aws-lambda-java-core:$lambdaCoreVersion" - implementation "com.amazonaws:aws-lambda-java-events:$lambdaEventsVersion" - testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" - testImplementation "org.junit.jupiter:junit-jupiter-engine:$junitVersion" -} - -test { - useJUnitPlatform() -} - -jar { - manifest { - attributes 'Main-Class': 'com.networknt.aws.lambda.Runtime' - } -} - -task buildGraalvmImage { - inputs.files("${project.projectDir}/src/main", configurations.compileClasspath) - outputs.upToDateWhen {file("${buildDir}/graalvm/server").exists()} - outputs.file file("${buildDir}/graalvm/server") - - doLast { - exec { - commandLine "bash", "-c", "chmod +x build_graalvm.sh; chmod +x bootstrap; ./build_graalvm.sh" - } - } -} - -buildGraalvmImage.dependsOn shadowJar, test -build.dependsOn buildGraalvmImage diff --git a/build_graalvm.sh b/build_graalvm.sh deleted file mode 100644 index e426e19..0000000 --- a/build_graalvm.sh +++ /dev/null @@ -1,21 +0,0 @@ - -#!/bin/bash - -docker run --rm --name graal -v $(pwd):/working springci/graalvm-ce:master-java11 \ - /bin/bash -c "native-image \ - --enable-url-protocols=http,https \ - --no-fallback \ - --allow-incomplete-classpath \ - --enable-all-security-services \ - -H:ReflectionConfigurationFiles=/working/reflect.json \ - -H:ResourceConfigurationFiles=/working/resource-config.json \ - -H:+ReportExceptionStackTraces \ - -jar /working/build/libs/MarketStoreProductsGetFunction-all.jar \ - ; \ - cp MarketStoreProductsGetFunction-all /working/build/graalvm/server" - -mkdir -p build/graalvm -if [ ! -f "build/graalvm/server" ]; then - echo "there was an error building graalvm image" - exit 1 -fi diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 4e4cbd5..0000000 --- a/gradle.properties +++ /dev/null @@ -1,8 +0,0 @@ -# Versions of Frequently used Libraries -light4jVersion=2.1.34-SNAPSHOT -lambdaCoreVersion=1.4.0 -lambdaEventsVersion=3.16.1 -jacksonVersion=2.17.0 -slf4jVersion=2.0.20 -logbackVersion=1.6.3 -junitVersion=5.11.4 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ac98e42..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Thu Jan 08 17:23:48 EST 2026 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew deleted file mode 100755 index 9aa616c..0000000 --- a/gradlew +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index f955316..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/native-image-linux.sh b/native-image-linux.sh index 08772f3..99eb3b5 100755 --- a/native-image-linux.sh +++ b/native-image-linux.sh @@ -1,42 +1,46 @@ -PROJECT_NAME=lambda-native -PROJECT_VERSION=2.1.34-SNAPSHOT +#!/usr/bin/env bash +set -euo pipefail -# mvn clean might fail due to permission issue on Linux, so remove target with root. -sudo rm -rf target; -# Generate Jar file -mvn clean install; -echo "Jar file generated successfully"; -echo $(pwd); +cd -- "$(dirname -- "${BASH_SOURCE[0]}")" -# Generate Native Image -docker run --rm --name graal -v $(pwd):/${PROJECT_NAME} springci/graalvm-ce:master-java11 \ - /bin/bash -c "echo 'Building native image'; \ - native-image \ - -H:EnableURLProtocols=http \ - --no-fallback \ - --verbose \ - --trace-class-initialization=ch.qos.logback.classic.Logger \ - --trace-object-instantiation=ch.qos.logback.core.AsyncAppenderBase$Worker \ - --initialize-at-build-time=org.slf4j.LoggerFactory,org.slf4j.MDC,ch.qos.logback \ - --initialize-at-run-time=io.netty \ - --allow-incomplete-classpath \ - --enable-all-security-services \ - -H:ReflectionConfigurationFiles=/${PROJECT_NAME}/reflect.json \ - -H:ResourceConfigurationFiles=/${PROJECT_NAME}/resource-config.json \ - -H:+ReportExceptionStackTraces \ - -jar /${PROJECT_NAME}/target/${PROJECT_NAME}-${PROJECT_VERSION}.jar \ - ; \ - mkdir /${PROJECT_NAME}/target/custom-runtime \ - ; \ - cp ${PROJECT_NAME}-${PROJECT_VERSION} /${PROJECT_NAME}/target/custom-runtime/${PROJECT_NAME}"; +# Build the shaded JAR, then obtain its name and Java level from Maven. +mvn clean install +metadata_dir=$(mktemp -d) +trap 'rm -rf -- "$metadata_dir"' EXIT +mvn -q help:evaluate -Dexpression=project.build.finalName \ + -Doutput="$metadata_dir/final-name" +mvn -q help:evaluate -Dexpression=maven.compiler.target \ + -Doutput="$metadata_dir/java-version" +jar_name=$(cat "$metadata_dir/final-name") +java_version=$(cat "$metadata_dir/java-version") +[[ "$java_version" =~ ^[0-9]+$ ]] || { echo "Invalid Maven compiler target: $java_version" >&2; exit 1; } +test -s "target/$jar_name.jar" -# Copy the bootstrap to the custom-runtime -sudo cp bootstrap target/custom-runtime/bootstrap; +# Override with a pinned GraalVM image compatible with the POM's Java level. +graalvm_image=${GRAALVM_IMAGE:-ghcr.io/graalvm/native-image-community:$java_version} +mkdir -p target/custom-runtime +docker run --rm --user "$(id -u):$(id -g)" \ + -v "$PWD:/workspace" -w /workspace --entrypoint native-image \ + "$graalvm_image" \ + --enable-url-protocols=http,https \ + --no-fallback \ + --verbose \ + --initialize-at-build-time=org.slf4j,ch.qos.logback \ + --initialize-at-run-time=io.netty \ + -H:ReflectionConfigurationFiles=/workspace/reflect.json \ + -H:ResourceConfigurationFiles=/workspace/resource-config.json \ + -H:+ReportExceptionStackTraces \ + -jar "/workspace/target/$jar_name.jar" \ + -o /workspace/target/custom-runtime/lambda-native -# Make bootstrap executable -sudo chmod +x target/custom-runtime/bootstrap; - -# Zip -rm $PROJECT_NAME-custom-runtime.zip -cd target/custom-runtime || exit -zip -X -r ../../$PROJECT_NAME-custom-runtime.zip . +# Never package a bootstrap-only archive after a failed native build. +test -s target/custom-runtime/lambda-native +test -x target/custom-runtime/lambda-native +cp bootstrap target/custom-runtime/bootstrap +chmod +x target/custom-runtime/bootstrap +( + cd target/custom-runtime + zip -X ../lambda-native-custom-runtime.zip lambda-native bootstrap +) +mv target/lambda-native-custom-runtime.zip lambda-native-custom-runtime.zip +echo "Created $PWD/lambda-native-custom-runtime.zip" diff --git a/tests/test_native_image_script.py b/tests/test_native_image_script.py new file mode 100644 index 0000000..f6faad3 --- /dev/null +++ b/tests/test_native_image_script.py @@ -0,0 +1,108 @@ +"""Script boundary tests; Maven and Docker are stubbed, ZIP packaging is real.""" +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest +from zipfile import ZipFile + + +ROOT = Path(__file__).resolve().parents[1] + + +class NativeImageScriptTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.bin = self.root / "bin" + self.bin.mkdir() + for name in ("native-image-linux.sh", "bootstrap"): + shutil.copy(ROOT / name, self.root / name) + self.stub("mvn", '''#!/usr/bin/env bash +set -eu +if [[ "$1" == clean ]]; then + [[ "${FAIL_STAGE:-}" != maven ]] || exit 21 + mkdir -p target + printf jar > target/customer-runtime-9.9.jar +else + for arg in "$@"; do + case "$arg" in + -Dexpression=*) expression=${arg#*=} ;; + -Doutput=*) output=${arg#*=} ;; + esac + done + case "$expression" in + project.build.finalName) printf customer-runtime-9.9 > "$output" ;; + maven.compiler.target) printf 25 > "$output" ;; + *) exit 22 ;; + esac +fi +''') + self.stub("docker", '''#!/usr/bin/env bash +set -eu +printf '%s\\n' "$@" > docker-args +[[ "${FAIL_STAGE:-}" != docker ]] || exit 23 +[[ "${FAIL_STAGE:-}" != missing ]] || exit 0 +printf '#!/bin/sh\\nexit 0\\n' > target/custom-runtime/lambda-native +chmod +x target/custom-runtime/lambda-native +''') + self.env = dict(os.environ, PATH=f"{self.bin}:{os.environ['PATH']}") + self.env.pop("GRAALVM_IMAGE", None) + + def stub(self, name, source): + path = self.bin / name + path.write_text(source) + path.chmod(0o755) + + def run_script(self, failure=""): + return subprocess.run( + ["bash", str(self.root / "native-image-linux.sh")], + cwd="/tmp", env=dict(self.env, FAIL_STAGE=failure), + capture_output=True, text=True, + ) + + def test_packages_pom_named_jar_with_https_and_java_25(self): + result = self.run_script() + self.assertEqual(result.returncode, 0, result.stderr) + args = (self.root / "docker-args").read_text().splitlines() + self.assertIn("ghcr.io/graalvm/native-image-community:25", args) + self.assertIn("/workspace/target/customer-runtime-9.9.jar", args) + self.assertIn("--enable-url-protocols=http,https", args) + with ZipFile(self.root / "lambda-native-custom-runtime.zip") as archive: + self.assertEqual(set(archive.namelist()), {"lambda-native", "bootstrap"}) + for info in archive.infolist(): + self.assertTrue((info.external_attr >> 16) & 0o111) + result = subprocess.run( + ["sh", "bootstrap"], cwd=self.root / "target/custom-runtime", + capture_output=True, text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + + def test_image_override(self): + self.env["GRAALVM_IMAGE"] = "example/pinned-graalvm:25" + self.assertEqual(self.run_script().returncode, 0) + self.assertIn("example/pinned-graalvm:25", (self.root / "docker-args").read_text()) + + def test_failures_never_publish_bootstrap_only_zip(self): + for stage in ("maven", "docker", "missing"): + with self.subTest(stage=stage): + shutil.rmtree(self.root / "target", ignore_errors=True) + result = self.run_script(stage) + self.assertNotEqual(result.returncode, 0) + self.assertFalse((self.root / "lambda-native-custom-runtime.zip").exists()) + self.assertNotIn("Created ", result.stdout) + + def test_zip_failure_preserves_previous_package(self): + self.stub("zip", "#!/bin/sh\nexit 24\n") + previous = self.root / "lambda-native-custom-runtime.zip" + previous.write_bytes(b"previous package") + result = self.run_script() + self.assertNotEqual(result.returncode, 0) + self.assertEqual(previous.read_bytes(), b"previous package") + self.assertNotIn("Created ", result.stdout) + + +if __name__ == "__main__": + unittest.main()