From 05add87ecf2c5aafe6f4295a3a4338c2b9565b3a Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 15 May 2026 20:51:44 +0200 Subject: [PATCH] fix: make jcpan -t Inline pass - Treat jar:PERL5LIB paths as stable absolutes in Internals::abs_path so Inline::derive_minus_I never emits bare -I flags. - Implement perlrun-style alternate shebang delegation for wrappers such as inc/bin/testml-cpan (word-boundary perl/indir detection, skip self-exec, spawn via PERLONJAVA_EXECUTABLE when the wrapper targets jperl). - Preserve argv spelling when delegating so TestML path rewrites stay relative. - In diagnostics.pm death_trap, detect eval context from "(eval N) line" when caller lacks an (eval) frame under $SIG{__DIE__}. Verified: make; ./jcpan -t Inline Generated with [Cursor](https://cursor.com/docs) Co-Authored-By: Cursor --- .../perlonjava/app/cli/ArgumentParser.java | 169 +++++++++++++++--- .../runtime/perlmodule/Internals.java | 10 ++ src/main/perl/lib/diagnostics.pm | 23 ++- 3 files changed, 172 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java index f9b30b9ff..9c26910f3 100644 --- a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java +++ b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java @@ -13,6 +13,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.perlonjava.core.Configuration.getPerlVersionBundle; @@ -317,31 +319,152 @@ private static void processNonSwitchArgument(String[] args, CompilerOptions pars */ private static void processShebangLine(String[] args, CompilerOptions parsedArgs, String fileContent, int index) { String[] lines = fileContent.split("\n", 2); - if (lines.length > 0 && lines[0].startsWith("#!")) { - // Extract the shebang line and process it - String shebangLine = lines[0].substring(2).trim(); - int perlIndex = shebangLine.indexOf("perl"); - if (perlIndex != -1) { - String relevantPart = shebangLine.substring(perlIndex + 4).trim(); - // Strip emacs mode line marker (e.g. "-*- mode: cperl -*-") which real - // perl tolerates in #! lines but not on the command line. - int emacsStart = relevantPart.indexOf("-*-"); - if (emacsStart != -1) { - int emacsEnd = relevantPart.indexOf("-*-", emacsStart + 3); - if (emacsEnd != -1) { - relevantPart = relevantPart.substring(0, emacsStart) - + relevantPart.substring(emacsEnd + 3); - } else { - relevantPart = relevantPart.substring(0, emacsStart); - } + if (lines.length == 0 || !lines[0].startsWith("#!")) { + return; + } + String shebangLine = lines[0].substring(2).trim(); + if (shebangLine.isEmpty()) { + return; + } + + // perlrun: parsing of #! switches starts at a *word* "perl" or "indir". + // Substrings like "jperl" must NOT match (matches stock perl behavior). + Matcher perlWord = Pattern.compile("\\b(?:perl|indir)\\b", Pattern.CASE_INSENSITIVE).matcher(shebangLine); + if (perlWord.find()) { + String relevantPart = shebangLine.substring(perlWord.end()).trim(); + // Strip emacs mode line marker (e.g. "-*- mode: cperl -*-") which real + // perl tolerates in #! lines but not on the command line. + int emacsStart = relevantPart.indexOf("-*-"); + if (emacsStart != -1) { + int emacsEnd = relevantPart.indexOf("-*-", emacsStart + 3); + if (emacsEnd != -1) { + relevantPart = relevantPart.substring(0, emacsStart) + + relevantPart.substring(emacsEnd + 3); + } else { + relevantPart = relevantPart.substring(0, emacsStart); } - String[] shebangArgs = relevantPart.trim().split("\\s+"); - // Filter out empty args from shebang processing - String[] nonEmptyArgs = Arrays.stream(shebangArgs) - .filter(arg -> !arg.isEmpty()) - .toArray(String[]::new); - processArgs(nonEmptyArgs, parsedArgs); } + String[] shebangArgs = relevantPart.trim().split("\\s+"); + String[] nonEmptyArgs = Arrays.stream(shebangArgs) + .filter(arg -> !arg.isEmpty()) + .toArray(String[]::new); + processArgs(nonEmptyArgs, parsedArgs); + return; + } + + // Alternate interpreter (perlrun): if there is no word "perl"/"indir", exec the named program. + // Example: Inline's TestML tests start with "#!inc/bin/testml-cpan". + String[] tokens = shebangLine.split("\\s+"); + if (tokens.length == 0) { + return; + } + if (isPerlOnJavaExecutable(Paths.get(tokens[0]))) { + // Same binary as this runtime (e.g. "#!/path/to/jperl"): compile here; do not re-exec. + return; + } + List cmd = buildShebangCommand(tokens); + delegateToShebangInterpreter(args, cmd, index); + } + + /** + * Build argv for an alternate #! interpreter. + * When {@code PERLONJAVA_EXECUTABLE} points at our launcher, prefer {@code jperl /abs/script} + * over executing {@code script} directly so ENOEXEC/shell-fallback (bash parsing Perl) + * cannot occur — CPAN's Inline bundles "#!inc/bin/testml-cpan" wrappers whose kernel + * exec path is fragile under JVM-spawned children on some platforms. + */ + private static List buildShebangCommand(String[] shebangTokens) { + java.nio.file.Path interpScript = + Paths.get(shebangTokens[0]).toAbsolutePath().normalize(); + String perlExe = System.getenv("PERLONJAVA_EXECUTABLE"); + List out = new ArrayList<>(); + if (perlExe != null && !perlExe.isEmpty() && interpreterScriptUsesPerlOnJava(interpScript)) { + out.add(perlExe); + out.add(interpScript.toString()); + for (int i = 1; i < shebangTokens.length; i++) { + out.add(shebangTokens[i]); + } + return out; + } + out.add(interpScript.toString()); + for (int i = 1; i < shebangTokens.length; i++) { + out.add(shebangTokens[i]); + } + return out; + } + + /** True when {@code script}'s own shebang names this PerlOnJava launcher (absolute path). */ + private static boolean interpreterScriptUsesPerlOnJava(java.nio.file.Path script) { + try { + List lines = java.nio.file.Files.readAllLines(script, java.nio.charset.StandardCharsets.UTF_8); + if (lines.isEmpty()) { + return false; + } + String line = lines.getFirst().trim(); + if (!line.startsWith("#!")) { + return false; + } + String body = line.substring(2).trim(); + if (body.isEmpty()) { + return false; + } + String[] parts = body.split("\\s+"); + return isPerlOnJavaExecutable(Paths.get(parts[0])); + } catch (IOException e) { + return false; + } + } + + /** + * True when {@code interpreterPath} resolves to the same file as {@code PERLONJAVA_EXECUTABLE}. + */ + private static boolean isPerlOnJavaExecutable(java.nio.file.Path interpreterPath) { + String self = System.getenv("PERLONJAVA_EXECUTABLE"); + 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(); + return a.equals(b); + } catch (IOException e) { + try { + java.io.File fa = interpreterPath.toAbsolutePath().normalize().toFile(); + java.io.File fb = Paths.get(self).toAbsolutePath().normalize().toFile(); + return fa.getCanonicalPath().equals(fb.getCanonicalPath()); + } catch (IOException e2) { + return false; + } + } + } + + /** + * Spawn the alternate #! interpreter with this script and trailing argv, then exit the JVM + * with its status (matches perl's exec semantics closely enough for harness-driven tests). + */ + private static void delegateToShebangInterpreter(String[] args, List interpreterArgv0, int scriptArgIndex) { + // Preserve the argv spelling (usually relative, e.g. t/foo.t). Some wrappers + // (Inline's inc/bin/testml-cpan) regex-rewrite paths assuming a distribution-relative name; + // canonical absolute paths break their compiled-.tml lookup. + List cmd = new ArrayList<>(interpreterArgv0); + cmd.add(args[scriptArgIndex]); + for (int i = scriptArgIndex + 1; i < args.length; i++) { + cmd.add(args[i]); + } + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.inheritIO(); + try { + Process p = pb.start(); + int exit = p.waitFor(); + System.exit(exit); + } catch (IOException e) { + System.err.println("Error: unable to run shebang interpreter \"" + + interpreterArgv0.get(0) + "\": " + e.getMessage()); + System.exit(255); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + System.err.println("Error: interrupted while running shebang interpreter"); + System.exit(255); } } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java b/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java index d6aa79810..8c532a957 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java @@ -579,6 +579,16 @@ public static RuntimeList getcwd(RuntimeArray args, int ctx) { */ public static RuntimeList abs_path(RuntimeArray args, int ctx) { String path = args.size() > 0 ? args.get(0).toString() : "."; + // jar:PERL5LIB & jar:PERL5LIB/… paths live in the embedded Perl library. + // File.exists/canonicalPath cannot see them, but FileTestOperator and @INC do. + // Inline::derive_minus_I maps abs_path over @INC entries; returning undef here + // produced bare "-I" flags and broke Inline's config subprocess. + if (path.startsWith("jar:")) { + if (Jar.isJarDirectory(path) || Jar.exists(path)) { + return new RuntimeScalar(path).getList(); + } + return new RuntimeScalar().getList(); + } try { java.io.File file = new java.io.File(path); if (!file.isAbsolute()) { diff --git a/src/main/perl/lib/diagnostics.pm b/src/main/perl/lib/diagnostics.pm index 8ba7c1b07..f41ea5d5d 100644 --- a/src/main/perl/lib/diagnostics.pm +++ b/src/main/perl/lib/diagnostics.pm @@ -573,13 +573,22 @@ sub death_trap { # See if we are coming from anywhere within an eval. If so we don't # want to explain the exception because it's going to get caught. - my $in_eval = 0; - my $i = 0; - while (my $caller = (caller($i++))[3]) { - if ($caller eq '(eval)') { - $in_eval = 1; - last; - } + # + # PerlOnJava note: caller()[3] inside $SIG{__DIE__} may not include the + # synthetic "(eval)" frame yet (stack differs from perl's XS caller). + # Inline's tests hit this via `eval "require Missing::Mod"` under + # diagnostics; missing the eval frame makes splain+die recurse badly. + # When the exception already carries an "(eval N) line" location, treat + # it as eval-bound — matching perl's behavior for $@ caught by eval. + my $in_eval = ($exception =~ /\bat \(eval \d+\) line\b/); + unless ($in_eval) { + my $i = 0; + while (my $caller = (caller($i++))[3]) { + if ($caller eq '(eval)') { + $in_eval = 1; + last; + } + } } splainthis($exception) unless $in_eval;