From 15c0542d70a9749ca228e1cbfe4f85cb1599a5b2 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 28 Apr 2026 16:36:27 +0200 Subject: [PATCH] fix: enable jcpan -t HTTP::Client::Parallel Two unrelated issues blocked HTTP::Client::Parallel: 1. POSIX.pm did not export the termios constants nor the _SC_* sysconf() name constants used by POE::Wheel::Run (loaded transitively from HTTP::Client::Parallel via POE). The Java side already implemented _const_ECHO, _const_TCSANOW, etc., and POSIX.pm wired them into POSIX::ECHO, POSIX::TCSANOW, ... but they were missing from @EXPORT_OK so `use POSIX qw(ECHO ...)` died with "ECHO is not exported by the POSIX module". Add the termios and _SC_* names to @EXPORT_OK and provide a small sysconf() stub that returns sensible defaults (e.g. _SC_OPEN_MAX => 1024). 2. The 1-argument form `open FILEHANDLE` (used by Module::Install::DSL, which LWP::Online ships) crashed with "Modification of a read-only value" when the filehandle was given as a numeric literal such as `open 0`. The runtime tried to call set() on the readonly arg and was also picking the filename from $_ instead of from the global scalar of the same name as the filehandle. Rewrite the 1-arg open path to derive the filehandle name from the value (covering glob refs, bareword strings, and numeric literals like 0), look up the filename in $main::, and only assign back to args[0] when it is a writable scalar. With both fixes, `jcpan -t HTTP::Client::Parallel` and its LWP::Online dependency build and pass their unit tests. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 4 +- .../runtime/operators/IOOperator.java | 58 ++++++++++++----- src/main/perl/lib/POSIX.pm | 62 +++++++++++++++++++ 3 files changed, 107 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index e58842ed8..270bb2f8c 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "dfa87a6c4"; + public static final String gitCommitId = "32360b133"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 28 2026 16:14:51"; + public static final String buildTimestamp = "Apr 28 2026 16:35:16"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java index bbc446d9d..079705531 100644 --- a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java @@ -558,28 +558,56 @@ public static RuntimeScalar open(int ctx, RuntimeBase... args) { RuntimeScalar fileHandle = (RuntimeScalar) args[0]; if (args.length < 2) { // 1-argument open: open FILEHANDLE - // Uses $_ as the filename (with embedded mode prefix parsed from it) - String fileName = getGlobalVariable("main::_").toString(); + // Per Perl semantics, the global scalar variable of the same name as the + // filehandle holds the filename (which may include a leading mode prefix). + // For example: `open MYFH` reads filename from $main::MYFH, `open 0` reads + // from $main::0 (which is the script name $0). + String filehandleName = null; + RuntimeGlob existingGlob = null; + if ((fileHandle.type == RuntimeScalarType.GLOB || fileHandle.type == RuntimeScalarType.GLOBREFERENCE) && fileHandle.value instanceof RuntimeGlob glob) { + existingGlob = glob; + filehandleName = glob.globName; + } else { + // Otherwise, derive the name from the scalar value. This covers both + // bareword-as-string ("MYFH") and constants like `open 0` where the + // filehandle is named "0". + String name = fileHandle.toString(); + if (name != null && !name.isEmpty()) { + filehandleName = name.contains("::") ? name : ("main::" + name); + } + } + + // Resolve the filename from the global scalar of the same name. + String fileName = filehandleName != null + ? getGlobalVariable(filehandleName).toString() + : ""; RuntimeIO oneFh = RuntimeIO.open(fileName); if (oneFh == null) { return scalarUndef; } - // Assign the IO handle to the filehandle glob (reuse the existing assignment logic below) - RuntimeGlob targetGlob = null; - if ((fileHandle.type == RuntimeScalarType.GLOB || fileHandle.type == RuntimeScalarType.GLOBREFERENCE) && fileHandle.value instanceof RuntimeGlob glob) { - targetGlob = glob; - } else if ((fileHandle.type == RuntimeScalarType.STRING || fileHandle.type == RuntimeScalarType.BYTE_STRING) && fileHandle.value instanceof String name) { - if (!name.isEmpty() && name.matches("^[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)*$")) { - String fullName = name.contains("::") ? name : ("main::" + name); - targetGlob = GlobalVariable.getGlobalIO(fullName); - RuntimeScalar newGlob = new RuntimeScalar(); - newGlob.type = RuntimeScalarType.GLOBREFERENCE; - newGlob.value = targetGlob; - fileHandle.set(newGlob); - } + + RuntimeGlob targetGlob = existingGlob; + if (targetGlob == null && filehandleName != null) { + targetGlob = GlobalVariable.getGlobalIO(filehandleName); } if (targetGlob != null) { targetGlob.setIO(oneFh); + // If args[0] is a writable scalar (not readonly), update it to point + // at the glob. We must NOT call set() on a readonly scalar (e.g. when + // args[0] is a numeric literal like in `open 0`). + if (!(fileHandle instanceof RuntimeScalarReadOnly) + && fileHandle.type != RuntimeScalarType.GLOB + && fileHandle.type != RuntimeScalarType.GLOBREFERENCE) { + try { + RuntimeScalar newGlob = new RuntimeScalar(); + newGlob.type = RuntimeScalarType.GLOBREFERENCE; + newGlob.value = targetGlob; + fileHandle.set(newGlob); + } catch (RuntimeException ignored) { + // Read-only / unsettable scalar - the IO has already been + // registered on the global glob, so callers can find it by name. + } + } } else { RuntimeScalar newGlob = new RuntimeScalar(); newGlob.type = RuntimeScalarType.GLOBREFERENCE; diff --git a/src/main/perl/lib/POSIX.pm b/src/main/perl/lib/POSIX.pm index 7f5ab758b..8e3b3da57 100644 --- a/src/main/perl/lib/POSIX.pm +++ b/src/main/perl/lib/POSIX.pm @@ -142,6 +142,21 @@ our @EXPORT_OK = qw( # Constants - access (for access() function) F_OK R_OK W_OK X_OK + + # Constants - termios (termios_h) + BRKINT + CS5 CS6 CS7 CS8 CSIZE CSTOPB CREAD PARENB PARODD HUPCL CLOCAL + ECHO ECHOE ECHOK ECHONL + ICANON IEXTEN ISIG + ICRNL INPCK ISTRIP IXON IXOFF IGNBRK IGNCR IGNPAR INLCR IXANY PARMRK + OPOST + TCSADRAIN TCSAFLUSH TCSANOW + VEOF VEOL VERASE VINTR VKILL VMIN VQUIT VSTART VSTOP VSUSP VTIME + + # Constants - sysconf (subset, used by POE etc.) + _SC_ARG_MAX _SC_CHILD_MAX _SC_CLK_TCK _SC_NGROUPS_MAX _SC_OPEN_MAX + _SC_JOB_CONTROL _SC_SAVED_IDS _SC_VERSION _SC_PAGESIZE _SC_PAGE_SIZE + _SC_NPROCESSORS_CONF _SC_NPROCESSORS_ONLN ); our %EXPORT_TAGS = ( @@ -575,6 +590,53 @@ for my $const (qw( *{$const} = eval "sub () { POSIX::_const_$const() }"; } +# sysconf() variable name constants and stub implementation. +# Real POSIX sysconf() returns system-dependent runtime limits. PerlOnJava +# does not implement true sysconf(), but many CPAN modules (POE, Proc::Daemon, +# etc.) call sysconf(_SC_OPEN_MAX) etc. for sensible defaults. Provide the +# common _SC_* names as constants and have sysconf() return reasonable values. +BEGIN { + my %sc = ( + _SC_ARG_MAX => 0, + _SC_CHILD_MAX => 1, + _SC_CLK_TCK => 2, + _SC_NGROUPS_MAX => 3, + _SC_OPEN_MAX => 4, + _SC_JOB_CONTROL => 5, + _SC_SAVED_IDS => 6, + _SC_VERSION => 7, + _SC_PAGESIZE => 8, + _SC_PAGE_SIZE => 8, # alias of _SC_PAGESIZE + _SC_NPROCESSORS_CONF => 9, + _SC_NPROCESSORS_ONLN => 10, + ); + no strict 'refs'; + for my $name (keys %sc) { + my $value = $sc{$name}; + *{"POSIX::$name"} = sub () { $value }; + } +} + +sub sysconf { + my $name = shift; + return undef unless defined $name; + if ($name == 0) { return 4096 * 1024; } # _SC_ARG_MAX + elsif ($name == 1) { return 1024; } # _SC_CHILD_MAX + elsif ($name == 2) { return 100; } # _SC_CLK_TCK + elsif ($name == 3) { return 16; } # _SC_NGROUPS_MAX + elsif ($name == 4) { return 1024; } # _SC_OPEN_MAX + elsif ($name == 5) { return 1; } # _SC_JOB_CONTROL + elsif ($name == 6) { return 1; } # _SC_SAVED_IDS + elsif ($name == 7) { return 200809; } # _SC_VERSION + elsif ($name == 8) { return 4096; } # _SC_PAGESIZE + elsif ($name == 9 || $name == 10) { # _SC_NPROCESSORS_* + my $n = eval { 0 + (`getconf _NPROCESSORS_ONLN 2>/dev/null` || 1) }; + $n = 1 if !$n || $n < 1; + return $n; + } + return undef; +} + # Locale category constants - defined directly since XS _const_ may not exist BEGIN { my %lc = (