From 8f69d931fc99aa4a10416412dc8c4043a86ea40b Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 15 Sep 2026 13:18:36 +0200 Subject: [PATCH] docs: document macOS-only native launcher shim Limit the optional jperl-exec launcher to macOS, where chained script interpreters are unreliable, and document the compiler-free Linux path. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- Makefile | 5 +++-- build.gradle | 44 ++++++++++++++++++++++++++++++------- native/README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 native/README.md diff --git a/Makefile b/Makefile index 5149073284..74b7e8fdb5 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ .PHONY: all clean test test-unit test-interpreter check-thread-test-sources check-thread-core-test-sources check-thread-ecosystem-test-sources check-thread-regex-test-sources test-thread-tooling test-threads test-threads-core test-threads-core-platform test-threads-core-mode test-threads-windows test-threads-regex test-threads-release test-threads-ecosystem test-bundled-modules test-cpan-distroprefs test-cpan-release-acceptance test-exiftool test-all test-gradle test-gradle-unit test-gradle-all test-gradle-parallel test-maven-parallel build run wrapper check-java-gradle dev ci sbom sbom-java sbom-perl sbom-clean check-links perl5-update perl5-sync perl5-sync-check PERL ?= perl +GRADLE_ARGS ?= THREAD_TOOLING_TESTS := \ dev/tools/tests/check_thread_core_parity.t \ @@ -147,9 +148,9 @@ wrapper: check-java-gradle # Standard build - incremental compilation with parallel tests (5 JVMs; last shard isolates heavy tests) build: check-java-gradle ifeq ($(OS),Windows_NT) - gradlew.bat classes testUnitParallel --parallel shadowJar + gradlew.bat $(GRADLE_ARGS) classes testUnitParallel --parallel shadowJar else - ./gradlew classes testUnitParallel --parallel shadowJar + ./gradlew $(GRADLE_ARGS) classes testUnitParallel --parallel shadowJar endif # Focused vendored-Joni unit gate for parser/matcher iteration. A full `make` diff --git a/build.gradle b/build.gradle index e9f514c677..3e52467c36 100644 --- a/build.gradle +++ b/build.gradle @@ -29,14 +29,32 @@ application { mainClass = 'org.perlonjava.app.cli.Main' } -// $^X must be a native executable when generated Perl scripts are invoked -// directly. macOS does not reliably chain a #! script whose interpreter is -// itself the shell-based jperl launcher. +// macOS does not reliably chain a #! script whose interpreter is itself the +// shell-based jperl launcher. The optional native $^X shim is therefore a +// macOS-only compatibility layer; PerlOnJava otherwise runs through its shell +// launcher and Java. +def nativeLauncherPlatform = org.gradle.internal.os.OperatingSystem.current().isMacOsX() +def nativeLauncherCompiler = System.getenv('CC') ?: 'cc' +def nativeLauncherDisabled = providers.gradleProperty('skipNativeJperlLauncher') + .map { it.toBoolean() } + .getOrElse(false) +def nativeLauncherCompilerAvailable = { + File compiler = new File(nativeLauncherCompiler) + if (compiler.parent != null) { + return compiler.isFile() && compiler.canExecute() + } + return (System.getenv('PATH') ?: '').split(java.util.regex.Pattern.quote(File.pathSeparator)) + .any { new File(it, nativeLauncherCompiler).canExecute() } +} +def nativeLauncherEnabled = nativeLauncherPlatform + && !nativeLauncherDisabled && nativeLauncherCompilerAvailable() + tasks.register('nativeJperlLauncher', Exec) { - onlyIf { !org.gradle.internal.os.OperatingSystem.current().isWindows() } + description = 'Builds the optional native launcher used by direct $^X shebang scripts' + onlyIf { nativeLauncherEnabled } inputs.file('native/jperl-exec.c') outputs.file("$buildDir/../target/jperl-exec") - commandLine 'cc', '-O2', '-o', file("$buildDir/../target/jperl-exec"), file('native/jperl-exec.c') + commandLine nativeLauncherCompiler, '-O2', '-o', file("$buildDir/../target/jperl-exec"), file('native/jperl-exec.c') } // Debian package build dependency @@ -57,7 +75,9 @@ tasks.register('copyWrapperScripts', Copy) { include 'jprove' include 'jprove.bat' } - from(file("$buildDir/../target/jperl-exec")) + if (nativeLauncherEnabled) { + from(file("$buildDir/../target/jperl-exec")) + } into "${buildDir}/install/perlonjava/bin" } @@ -505,12 +525,15 @@ tasks.register('verifyJoniPackaging', Exec) { // children go through the repository launcher, so the target jar must be built // before tests run; otherwise a stale target/perlonjava-*.jar can be executed. tasks.withType(Test).configureEach { t -> - t.dependsOn shadowJar, nativeJperlLauncher + t.dependsOn shadowJar + if (nativeLauncherEnabled) { + t.dependsOn nativeJperlLauncher + } def jperlLauncher = org.gradle.internal.os.OperatingSystem.current().isWindows() ? "jperl.bat" : "jperl" t.environment 'PERLONJAVA_EXECUTABLE', file(jperlLauncher).absolutePath - if (!org.gradle.internal.os.OperatingSystem.current().isWindows()) { + if (nativeLauncherEnabled) { t.environment 'PERLONJAVA_SHEBANG_EXECUTABLE', file("$buildDir/../target/jperl-exec").absolutePath } } @@ -657,6 +680,11 @@ tasks.named('processTestResources', Copy) { // ordinary resource corpus prevents a default invocation from pretending // to exercise a command-line contract it does not supply. exclude 'unit/custom_warning_command_line_W.t' + // macOS direct #!$^X execution needs the optional C shim. Linux supports + // nested script interpreters and continues to run this capability test. + if (nativeLauncherPlatform && !nativeLauncherEnabled) { + exclude 'unit/current_perl_shebang_exec.t' + } } // Parallel test execution tasks diff --git a/native/README.md b/native/README.md new file mode 100644 index 0000000000..615b5c6c0b --- /dev/null +++ b/native/README.md @@ -0,0 +1,56 @@ +# Native launcher shim + +PerlOnJava normally needs no C compiler and does not execute native Perl or +XS code. This directory contains one small, optional macOS C program: +`jperl-exec`. + +## Why it exists + +Perl programs sometimes generate another executable script with this shebang: + +```text +#!$^X +``` + +For PerlOnJava, `$^X` is ordinarily the `jperl` Bash launcher. macOS does not +reliably execute a script whose shebang points to another script. Directly +running the generated script can therefore fail, even though +`jperl generated-script` works. + +`jperl-exec` is a real executable, so the kernel can use it as the first +shebang interpreter. It locates the adjacent `jperl` launcher (or honors +`PERLONJAVA_EXECUTABLE`) and `exec`s `/bin/bash jperl` with the original +arguments. The usual launcher then starts the Java runtime. It contains no +Perl implementation, JNI, or native library binding. + +## Optional build behavior + +Only on macOS, the Gradle build compiles `jperl-exec` with `$CC`, or `cc` when +`$CC` is unset, if that compiler is available. It is installed beside `jperl` +and PerlOnJava exposes it as `$^X`; direct generated shebang scripts then +work. + +If no compiler is found, the macOS build continues without the shim. +PerlOnJava, `jperl`, `jcpan`, and scripts invoked as `jperl script.pl` +continue to work; only direct execution of generated `#!$^X` scripts is +unavailable. The corresponding macOS capability test is omitted in that +configuration. + +Linux has supported a script interpreter that is itself a script since kernel +2.6.28, so it needs no shim or C compiler for this behavior. Windows does not +build the macOS shim. + +To deliberately omit it even when a compiler is installed: + +```bash +make build GRADLE_ARGS=-PskipNativeJperlLauncher +``` + +To select a compiler explicitly: + +```bash +CC=clang make build +``` + +The compiled development artifact is `target/jperl-exec`. Packaged macOS +installations place it next to `jperl` in `bin/`.