Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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 \
Expand Down Expand Up @@ -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`
Expand Down
44 changes: 36 additions & 8 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
}

Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions native/README.md
Original file line number Diff line number Diff line change
@@ -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/`.
Loading