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
18 changes: 16 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,24 @@ 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.
tasks.register('nativeJperlLauncher', Exec) {
onlyIf { !org.gradle.internal.os.OperatingSystem.current().isWindows() }
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')
}

// Debian package build dependency
tasks.buildDeb {
dependsOn installDist
}

// Copy custom wrapper scripts to installDist bin directory
tasks.register('copyWrapperScripts', Copy) {
dependsOn installDist
dependsOn installDist, nativeJperlLauncher
from(projectDir) {
include 'jperl'
include 'jperl.bat'
Expand All @@ -47,6 +57,7 @@ tasks.register('copyWrapperScripts', Copy) {
include 'jprove'
include 'jprove.bat'
}
from(file("$buildDir/../target/jperl-exec"))
into "${buildDir}/install/perlonjava/bin"
}

Expand Down Expand Up @@ -494,11 +505,14 @@ 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
t.dependsOn shadowJar, 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()) {
t.environment 'PERLONJAVA_SHEBANG_EXECUTABLE', file("$buildDir/../target/jperl-exec").absolutePath
}
}

// Task to generate Perl SBOM
Expand Down
2 changes: 2 additions & 0 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ priorities and future plans.

## Work in progress

- Fix direct execution of scripts generated with a PerlOnJava `$^X` shebang and update the Java-backed `Compress::Raw::{Bzip2,Zlib}` providers to the audited 2.224 compatibility level.

- Restore Perl-compatible integer increment/decrement semantics, imprecision
warnings, numeric overload fallback, and postfix-reference lifetime handling.

Expand Down
8 changes: 8 additions & 0 deletions jperl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ JPERL_PATH="$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$SCRIPT_DIR/j
# Export environment variable for PerlOnJava to use as $^X
export PERLONJAVA_EXECUTABLE="$JPERL_PATH"

# Use the native shim for $^X when it has been built. This allows a generated
# '#!$^X' script to run directly on platforms that reject chained scripts.
if [ -x "$SCRIPT_DIR/jperl-exec" ]; then
export PERLONJAVA_SHEBANG_EXECUTABLE="$SCRIPT_DIR/jperl-exec"
elif [ -x "$SCRIPT_DIR/target/jperl-exec" ]; then
export PERLONJAVA_SHEBANG_EXECUTABLE="$SCRIPT_DIR/target/jperl-exec"
fi

# Java 24 virtual threads are the default Perl ithread carrier. Preserve an
# explicit environment selection; the equivalent JVM property in JPERL_OPTS
# still takes precedence inside PerlThreadExecutionPolicy.
Expand Down
55 changes: 55 additions & 0 deletions native/jperl-exec.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Small POSIX executable shim used as PerlOnJava's $^X.
*
* A script whose shebang names the shell-based jperl launcher cannot be
* executed directly on macOS: the kernel does not reliably chain script
* interpreters. This binary is the first interpreter instead, and in turn
* starts the regular launcher under bash.
*/
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char **argv) {
const char *launcher = getenv("PERLONJAVA_EXECUTABLE");
char fallback[PATH_MAX];

if ((launcher == NULL || launcher[0] == '\0') && strchr(argv[0], '/') != NULL) {
snprintf(fallback, sizeof(fallback), "%s", argv[0]);
char *slash = strrchr(fallback, '/');
*slash = '\0';
size_t directoryLength = strlen(fallback);
snprintf(fallback + directoryLength, sizeof(fallback) - directoryLength, "/jperl");
if (access(fallback, X_OK) != 0) {
// Development builds keep the shim in target/ beside the repository launcher.
slash = strrchr(fallback, '/');
*slash = '\0';
slash = strrchr(fallback, '/');
if (slash != NULL) {
*slash = '\0';
size_t parentLength = strlen(fallback);
snprintf(fallback + parentLength, sizeof(fallback) - parentLength, "/jperl");
}
}
launcher = fallback;
}
if (launcher == NULL || launcher[0] == '\0') {
fputs("jperl-exec: PERLONJAVA_EXECUTABLE is not set\n", stderr);
return 127;
}

char **command = calloc((size_t)argc + 2, sizeof(*command));
if (command == NULL) {
perror("jperl-exec: calloc");
return 127;
}
command[0] = "/bin/bash";
command[1] = (char *) launcher;
for (int i = 1; i < argc; i++) command[i + 1] = argv[i];
execv(command[0], command);
fprintf(stderr, "jperl-exec: cannot start %s: %s\n", launcher, strerror(errno));
return 127;
}
11 changes: 9 additions & 2 deletions src/main/java/org/perlonjava/app/cli/ArgumentParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -445,13 +445,20 @@ private static boolean interpreterScriptUsesPerlOnJava(java.nio.file.Path script
}

/**
* True when {@code interpreterPath} resolves to the same file as {@code PERLONJAVA_EXECUTABLE}.
* True when {@code interpreterPath} resolves to this runtime's regular launcher or its native shebang shim.
*/
private static boolean isPerlOnJavaExecutable(java.nio.file.Path interpreterPath) {
String self = System.getenv("PERLONJAVA_EXECUTABLE");
if (self == null || self.isEmpty()) {
String shebangSelf = System.getenv("PERLONJAVA_SHEBANG_EXECUTABLE");
if ((self == null || self.isEmpty()) && (shebangSelf == null || shebangSelf.isEmpty())) {
return false;
}
return isSameExecutable(interpreterPath, self)
|| isSameExecutable(interpreterPath, shebangSelf);
}

private static boolean isSameExecutable(java.nio.file.Path interpreterPath, String self) {
if (self == null || self.isEmpty()) return false;
try {
java.nio.file.Path a = interpreterPath.toAbsolutePath().normalize().toRealPath();
java.nio.file.Path b = Paths.get(self).toAbsolutePath().normalize().toRealPath();
Expand Down
22 changes: 20 additions & 2 deletions src/main/java/org/perlonjava/runtime/operators/SystemOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -577,8 +577,14 @@ private static List<String> expandJperlShebangForProcessBuilder(List<String> com
}

List<String> expanded = new ArrayList<>();
expanded.add("/bin/bash");
expanded.add(shebangWords.getFirst());
if (isCurrentNativeShebangShim(shebangWords.getFirst())) {
// The shim is already a real executable. Running it through bash
// would make bash attempt to parse its binary bytes as a script.
expanded.add(shebangWords.getFirst());
} else {
expanded.add("/bin/bash");
expanded.add(shebangWords.getFirst());
}
expanded.addAll(shebangWords.subList(1, shebangWords.size()));
expanded.add(script.getAbsolutePath());
expanded.addAll(commandArgs.subList(1, commandArgs.size()));
Expand Down Expand Up @@ -612,6 +618,18 @@ private static boolean isCurrentJperlWrapper(String interpreter) {
return isCurrentJperlWrapper(interpreter, getCurrentJperlPath());
}

private static boolean isCurrentNativeShebangShim(String interpreter) {
String shim = System.getenv("PERLONJAVA_SHEBANG_EXECUTABLE");
if (shim == null || shim.isEmpty()) return false;
try {
return new File(interpreter).getCanonicalFile()
.equals(new File(shim).getCanonicalFile());
} catch (IOException e) {
return new File(interpreter).getAbsolutePath()
.equals(new File(shim).getAbsolutePath());
}
}

private static boolean isCurrentJperlWrapper(
String interpreter, String currentJperlPath) {
if (interpreter == null || interpreter.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/
public class CompressRawBzip2 extends PerlModuleBase {

public static String XS_VERSION = "2.218";
public static String XS_VERSION = "2.224";

private static final int BZ_OK = 0;
private static final int BZ_RUN_OK = 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ public static void initialize() {

// Set $XS_VERSION for version check in CPAN .pm
GlobalVariable.getGlobalVariable("Compress::Raw::Zlib::XS_VERSION")
.set(new RuntimeScalar("2.222"));
.set(new RuntimeScalar("2.224"));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ public static void initializeGlobals(CompilerOptions compilerOptions) {

// Initialize $^X - the name used to execute the current copy of Perl
// PERLONJAVA_EXECUTABLE is set by the `jperl` or `jperl.bat` launcher
String perlExecutable = System.getenv("PERLONJAVA_EXECUTABLE");
String perlExecutable = System.getenv("PERLONJAVA_SHEBANG_EXECUTABLE");
if (perlExecutable == null || perlExecutable.isEmpty()) {
perlExecutable = System.getenv("PERLONJAVA_EXECUTABLE");
}
RuntimeScalar executableVariable =
GlobalVariable.getGlobalVariable("main::" + Character.toString('X' - 'A' + 1));
if (perlExecutable != null && !perlExecutable.isEmpty()) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/perl/lib/Compress/Raw/Bzip2.pm
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use Carp ();

our ($VERSION, $XS_VERSION, @ISA, @EXPORT, $AUTOLOAD);

$VERSION = '2.218';
$VERSION = '2.224';
$XS_VERSION = $VERSION;
$VERSION = eval $VERSION;

Expand Down
2 changes: 1 addition & 1 deletion src/main/perl/lib/Compress/Raw/Zlib.pm
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use warnings ;
use bytes ;
our ($VERSION, $XS_VERSION, @ISA, @EXPORT, %EXPORT_TAGS, @EXPORT_OK, $AUTOLOAD, %DEFLATE_CONSTANTS, @DEFLATE_CONSTANTS);

$VERSION = '2.222';
$VERSION = '2.224';
$XS_VERSION = $VERSION;
$VERSION = eval $VERSION;

Expand Down
8 changes: 7 additions & 1 deletion src/test/resources/unit/compress_raw_bzip2.t
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ eval {
Compress::Raw::Bzip2->import;
1;
} or plan skip_all => 'Compress::Raw::Bzip2 required';
plan tests => 14;
plan tests => 16;

SKIP: {
skip 'PerlOnJava provider version contract', 2 unless $^X =~ /jperl/;
is($Compress::Raw::Bzip2::VERSION, '2.224', 'advertises the audited 2.224 Perl API');
is($Compress::Raw::Bzip2::XS_VERSION, '2.224', 'Java XS provider satisfies the 2.224 prerequisite');
}

is(Compress::Raw::Bzip2::BZ_OK(), 0, 'BZ_OK exported');
is(Compress::Raw::Bzip2::BZ_RUN_OK(), 1, 'BZ_RUN_OK exported');
Expand Down
6 changes: 6 additions & 0 deletions src/test/resources/unit/compress_raw_zlib_gzip_wrapper.t
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ use warnings;
use Test::More;
use Compress::Raw::Zlib qw(WANT_GZIP Z_STREAM_END);

SKIP: {
skip 'PerlOnJava provider version contract', 2 unless $^X =~ /jperl/;
is($Compress::Raw::Zlib::VERSION, '2.224', 'advertises the audited 2.224 Perl API');
is($Compress::Raw::Zlib::XS_VERSION, '2.224', 'Java XS provider satisfies the 2.224 prerequisite');
}

my $payload = "gzip wrapper payload\n";
my $deflater = Compress::Raw::Zlib::Deflate->new(
-WindowBits => WANT_GZIP(),
Expand Down
24 changes: 24 additions & 0 deletions src/test/resources/unit/current_perl_shebang_exec.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More tests => 3;
use File::Spec;
use File::Temp qw(tempdir);

SKIP: {
skip 'direct executable shebang test is Unix-specific', 3
if $^O eq 'MSWin32';

my $dir = tempdir(CLEANUP => 1);
my $script = File::Spec->catfile($dir, 'current-perl-script');
open my $fh, '>', $script or die "open $script: $!";
print {$fh} "#!$^X\n";
print {$fh} 'print "current-perl-ok @ARGV\\n";', "\n";
close $fh or die "close $script: $!";
chmod 0755, $script or die "chmod $script: $!";

ok(-x $script, 'generated $^X shebang script is executable');
my $output = `$script alpha beta 2>&1`;
is($?, 0, 'generated $^X shebang script runs directly');
is($output, "current-perl-ok alpha beta\n", 'generated script receives argv');
}
Loading