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
57 changes: 47 additions & 10 deletions dev/modules/cpanplus.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,22 @@

## Current Status

`CPANPLUS::Config` now loads under PerlOnJava after fixing loop-control parsing for bare labels that share a name with an imported constant. This unblocks the upstream `Makefile.PL` path that declares CPANPLUS' dynamic prerequisites, including `Log::Message`.
`CPANPLUS::Config` loads under PerlOnJava after fixing loop-control parsing for bare labels that share a name with an imported constant. This unblocks the upstream `Makefile.PL` path that declares CPANPLUS' dynamic prerequisites.

`./jcpan -t CPANPLUS` now passes with CPANPLUS 0.9916:

```text
Files=20, Tests=1751, Result: PASS
EXIT: 0
```

The run also verifies the dependency chain that previously blocked CPANPLUS: `Archive::Extract`, `Object::Accessor`, `File::Fetch`, `Log::Message`, `Module::Loaded`, `Package::Constants`, `Log::Message::Simple`, and `Term::UI`.

The only observed remaining issue is a non-fatal warning during CPANPLUS' own suite:

```text
Use of uninitialized value in addition (+) at jar:PERL5LIB/File/Copy.pm line 303.
```

## Symptom

Expand All @@ -25,7 +40,7 @@ WriteMakefile(

That fallback had no `PREREQ_PM`, so `Log::Message` was never scheduled.

## Root Cause Fixed
## Root Causes Fixed

`CPANPLUS::Config` imports constants from `CPANPLUS::Internals::Constants`, including:

Expand All @@ -45,33 +60,55 @@ Perl treats bare `last BIN` as a literal loop label, even when a constant sub na

The fix keeps a standalone bare identifier immediately after `last`, `next`, or `redo` as a literal loop label. Parenthesized and expression labels still go through the dynamic-label path.

`Archive::Extract` then failed because PerlOnJava's bundled `Archive::Zip` member objects exposed `_name` but CPAN `Archive::Extract` expects the compatibility hash key `fileName`, and it passes member objects back into `extractMember`. The bundled module now exposes both names and accepts member objects.

`Object::Accessor` then exposed tied lexical cleanup differences. A tied lexical scalar whose tie object should be destroyed at scope exit was being cleaned through the generic scalar path, so `DESTROY` did not fire at the Perl-compatible time. Tied scalar cleanup is now explicit and idempotent, including the closure-capture case.

CPANPLUS' tests also use strict bareword coderef calls in forms such as `BUILD_PL->(...)`, `-e BUILD_PL->(...)`, and `stat MAKEFILE->(...)`. PerlOnJava now treats a bareword to the left of `->(...)` as a sub call returning a coderef, and reassociates filetest and `stat`/`lstat` unary operators so those expressions match Perl's parse.

Version checks then exposed decimal vs dotted-version numification differences. `version->parse("v1.5")->numify` now returns `1.005000`, while decimal versions such as `1.5` and `1.2345` retain decimal-padding semantics.

The final CPANPLUS blocker was in the generated Makefile for a dummy `Foo-Bar` distribution. When `Makefile.PL` was rerun after `blib/lib` already existed, PerlOnJava's MakeMaker treated staged `blib/lib/*.pm` files as source files. Its generated `pm_to_blib` target could delete `blib/lib/Foo/Bar.pm` and then try to copy that same path back to itself. MakeMaker now prefers real `lib/` sources over stale `blib/` entries and does not stage already-staged files back into `blib`.

## Completed Work

- Fixed loop-control parsing in [`OperatorParser.java`](../../src/main/java/org/perlonjava/frontend/parser/OperatorParser.java).
- Added regression coverage in [`loop_label_bareword_constant.t`](../../src/test/resources/unit/loop_label_bareword_constant.t).
- Verified CPANPLUS' upstream `Makefile.PL` now completes and emits `PREREQ_PM` / `MYMETA.yml` entries for `Log::Message`.
- Fixed the `Archive::Extract` dependency failure by making PerlOnJava's bundled `Archive::Zip` expose the CPAN-compatible member hash field `fileName` and accept member objects in `extractMember`.
- Added regression coverage in [`archive_zip_members_matching_qr.t`](../../src/test/resources/unit/archive_zip_members_matching_qr.t) for direct member hash access and object extraction.
- Verified `Archive::Extract` 0.88 upstream suite passes: `Files=1, Tests=1795, Result: PASS`.
- Fixed tied scalar scope cleanup so `Object::Accessor` local attribute restore passes.
- Added regression coverage in [`tie_scalar.t`](../../src/test/resources/unit/tie_scalar.t) for tied lexical `DESTROY` at scope exit and deferred destruction while a tie object is still referenced.
- Fixed strict bareword coderef arrow parsing and filetest/stat reassociation for CPANPLUS' `BUILD_PL->(...)` and `MAKEFILE->(...)` patterns.
- Added regression coverage in [`subroutine.t`](../../src/test/resources/unit/subroutine.t) for strict bareword coderef calls and unary-operator reassociation.
- Fixed `version` numification/normalization for dotted `v` versions and decimal versions.
- Added regression coverage in [`version_numify.t`](../../src/test/resources/unit/version_numify.t).
- Fixed PerlOnJava MakeMaker reruns after `blib/lib` exists so stale staged files are not copied onto themselves.
- Added regression coverage in [`makemaker_stale_blib_source.t`](../../src/test/resources/unit/makemaker_stale_blib_source.t).
- Verified `Object::Accessor` upstream suite passes: `Files=7, Tests=155, Result: PASS`.
- Verified `./jcpan -t CPANPLUS` passes: `Files=20, Tests=1751, Result: PASS`.
- Verified `make` passes.

## Acceptance

```bash
timeout 60 ./jperl src/test/resources/unit/loop_label_bareword_constant.t
timeout 60 ./jperl -I$CPANPLUS_DIR/inc/bundle -I$CPANPLUS_DIR/lib -e 'require CPANPLUS::Config; print "ok\n"'
timeout 600 ./jcpan -t CPANPLUS
timeout 1200 ./jcpan -t CPANPLUS
make
```

Before running the full `jcpan -t CPANPLUS` acceptance, make sure no local CPANPLUS distropref is masking the dependency path. A previous investigation generated `/Users/fglock/.perlonjava/cpan/prefs/CPANPLUS.yml`; move it aside or use an isolated CPAN home before judging dependency discovery.

## Next Steps

1. Re-run `timeout 600 ./jcpan -t CPANPLUS` without the temporary local CPANPLUS distropref and confirm CPAN installs or schedules `Log::Message` from the upstream Makefile.PL metadata.
2. Inspect the generated `Makefile`, `MYMETA.yml`, and CPAN log to verify `PREREQ_PM` includes the CPANPLUS runtime dependency set from `CPANPLUS::Selfupdate`.
3. Continue from the next observed failures after dependency discovery is correct. The earlier workaround run reached `Archive::Extract` and `Module::Loaded`; treat those as separate module/runtime issues, not dependency-discovery fixes.
4. Add any new minimal runtime/parser regression tests before patching CPAN distroprefs. Distroprefs should only be used for unavoidable CPAN packaging/test harness differences, not to hide missing interpreter semantics.
5. When CPANPLUS tests are passing or have documented non-runtime blockers, update this document with the final test count and remaining skips/failures.
1. Reduce the non-fatal `File::Copy.pm line 303` warning from CPANPLUS' own suite. It appears to come from numeric conversion of `$!` or `$^E` after a failed move fallback, but it does not currently fail CPANPLUS.
2. Re-run `timeout 1200 ./jcpan -t CPANPLUS` from a fresh or isolated CPAN home before merging if cache independence is required.
3. Audit whether MakeMaker still needs to discover installable files from `blib/lib`; if it does, keep the new no-self-staging behavior as the regression guard.
4. Keep CPANPLUS as a regression target when touching `Archive::Extract`, `Object::Accessor`, `ExtUtils::MakeMaker`, parser precedence, or `version`.

## Open Questions

- Does CPAN consume CPANPLUS' dynamic prereqs from `MYMETA.yml` reliably after the upstream Makefile.PL succeeds, or does PerlOnJava's MakeMaker shim need a targeted metadata handoff fix?
- Are the later `Archive::Extract` / `Module::Loaded` failures pure module gaps, network/cache issues, or consequences of CPANPLUS test setup?
- Does the `File::Copy` warning reveal a generic `$!` / `$^E` numeric conversion difference when the error variables are unset?
- Are there CPAN distributions that intentionally rely on MakeMaker installing files that only exist under `blib/lib` after configure/build, and should that path be modeled more explicitly?
28 changes: 28 additions & 0 deletions src/main/java/org/perlonjava/frontend/parser/ParseInfix.java
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,15 @@ public static Node parseInfixOperation(Parser parser, Node left, int precedence)
case "(":
TokenUtils.consume(parser);
right = new ListNode(ListParser.parseList(parser, ")", 0), parser.tokenIndex);
if (left instanceof OperatorNode op && isArrowReassociatingUnaryOperator(op.operator)) {
Node arrowLeft = coderefArrowLeft(op.operand);
BinaryOperatorNode arrowCall = new BinaryOperatorNode(token.text,
arrowLeft,
right,
parser.tokenIndex);
return new OperatorNode(op.operator, arrowCall, op.getIndex());
}
left = coderefArrowLeft(left);
return new BinaryOperatorNode(token.text, left, right, parser.tokenIndex);
case "**":
// Postfix GLOB dereference: $ref->**
Expand Down Expand Up @@ -630,4 +639,23 @@ private static void checkMyInFalseConditional(String operator, Node left, Node r
}
}
}

private static Node coderefArrowLeft(Node left) {
if (left instanceof IdentifierNode) {
OperatorNode subRef = new OperatorNode("&", left, left.getIndex());
return new BinaryOperatorNode("(",
subRef,
new ListNode(left.getIndex()),
left.getIndex());
}
return left;
}

private static boolean isArrowReassociatingUnaryOperator(String operator) {
if (operator == null) return false;
if (operator.equals("stat") || operator.equals("lstat")) return true;
return operator.length() == 2
&& operator.charAt(0) == '-'
&& "rwxoRWXOezsfdlpSbctugkTBMAC".indexOf(operator.charAt(1)) >= 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,9 @@ public static String normalizeVersion(RuntimeScalar wantVersion) {
if (minor.length() > 3) {
minor = minor.substring(0, 3);
}
while (patch.length() < 3) {
patch = patch + "0";
}
if (patch.length() > 3) {
patch = patch.substring(0, 3);
}
Expand Down
61 changes: 45 additions & 16 deletions src/main/java/org/perlonjava/runtime/perlmodule/ArchiveZip.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public class ArchiveZip extends PerlModuleBase {
private static final String MEMBERS_KEY = "_members";
private static final String FILENAME_KEY = "_filename";
private static final String COMMENT_KEY = "_zipfileComment";
private static final String MEMBER_NAME_KEY = "_name";
private static final String MEMBER_COMPAT_FILENAME_KEY = "fileName";

/**
* Resolve a path string against Perl's notion of the current working
Expand Down Expand Up @@ -329,7 +331,7 @@ public static RuntimeList readFromFileHandle(RuntimeArray args, int ctx) {

// Create member object
RuntimeHash member = new RuntimeHash();
member.put("_name", new RuntimeScalar(entry.getName()));
putMemberName(member, entry.getName());
member.put("_externalFileName", new RuntimeScalar(""));
member.put("_isDirectory", entry.isDirectory() ? scalarTrue : scalarFalse);
member.put("_uncompressedSize", new RuntimeScalar(entry.getSize() >= 0 ? entry.getSize() : entryBaos.size()));
Expand Down Expand Up @@ -511,7 +513,8 @@ public static RuntimeList memberNames(RuntimeArray args, int ctx) {
RuntimeList result = new RuntimeList();
for (int i = 0; i < members.size(); i++) {
RuntimeHash member = members.get(i).hashDeref();
result.add(member.get("_name"));
RuntimeScalar memberName = getMemberNameScalar(member);
result.add(memberName != null ? memberName : scalarUndef);
}
return result;
}
Expand Down Expand Up @@ -545,7 +548,7 @@ public static RuntimeList memberNamed(RuntimeArray args, int ctx) {

for (int i = 0; i < members.size(); i++) {
RuntimeHash member = members.get(i).hashDeref();
RuntimeScalar memberName = member.get("_name");
RuntimeScalar memberName = getMemberNameScalar(member);
if (memberName != null && memberName.toString().equals(name)) {
return members.get(i).getList();
}
Expand Down Expand Up @@ -577,7 +580,7 @@ public static RuntimeList membersMatching(RuntimeArray args, int ctx) {
try {
for (int i = 0; i < members.size(); i++) {
RuntimeHash member = members.get(i).hashDeref();
RuntimeScalar memberName = member.get("_name");
RuntimeScalar memberName = getMemberNameScalar(member);
if (memberName != null && RuntimeRegex.matchRegex(
regex, memberName, RuntimeContextType.SCALAR).scalar().getBoolean()) {
result.add(members.get(i));
Expand Down Expand Up @@ -616,7 +619,7 @@ public static RuntimeList addFile(RuntimeArray args, int ctx) {
long lastModified = Files.getLastModifiedTime(path).toMillis();

RuntimeHash member = new RuntimeHash();
member.put("_name", new RuntimeScalar(memberName));
putMemberName(member, memberName);
member.put("_externalFileName", new RuntimeScalar(filename));
member.put("_contents", new RuntimeScalar(new String(content, StandardCharsets.ISO_8859_1)));
member.put("_isDirectory", scalarFalse);
Expand Down Expand Up @@ -655,7 +658,7 @@ public static RuntimeList addString(RuntimeArray args, int ctx) {
byte[] contentBytes = content.getBytes(StandardCharsets.ISO_8859_1);

RuntimeHash member = new RuntimeHash();
member.put("_name", new RuntimeScalar(memberName));
putMemberName(member, memberName);
member.put("_externalFileName", new RuntimeScalar(""));
member.put("_contents", new RuntimeScalar(content));
member.put("_isDirectory", scalarFalse);
Expand Down Expand Up @@ -692,7 +695,7 @@ public static RuntimeList addDirectory(RuntimeArray args, int ctx) {
}

RuntimeHash member = new RuntimeHash();
member.put("_name", new RuntimeScalar(dirName));
putMemberName(member, dirName);
member.put("_externalFileName", new RuntimeScalar(""));
member.put("_contents", new RuntimeScalar(""));
member.put("_isDirectory", scalarTrue);
Expand Down Expand Up @@ -721,15 +724,25 @@ public static RuntimeList extractMember(RuntimeArray args, int ctx) {
}

RuntimeHash self = args.get(0).hashDeref();
String memberName = args.get(1).toString();
RuntimeScalar memberArg = args.get(1);
String memberName;
if (RuntimeScalarType.isReference(memberArg)) {
RuntimeScalar name = getMemberNameScalar(memberArg.hashDeref());
if (name == null || name.type == RuntimeScalarType.UNDEF) {
return new RuntimeScalar(AZ_ERROR).getList();
}
memberName = name.toString();
} else {
memberName = memberArg.toString();
}
String destName = args.size() > 2 ? args.get(2).toString() : memberName;

try {
RuntimeArray members = getMembers(self);

for (int i = 0; i < members.size(); i++) {
RuntimeHash member = members.get(i).hashDeref();
RuntimeScalar name = member.get("_name");
RuntimeScalar name = getMemberNameScalar(member);
if (name != null && name.toString().equals(memberName)) {
RuntimeScalar isDir = member.get("_isDirectory");
if (isDir != null && isDir.getBoolean()) {
Expand Down Expand Up @@ -789,7 +802,7 @@ public static RuntimeList extractMemberWithoutPaths(RuntimeArray args, int ctx)
member = found.scalar().hashDeref();
}

RuntimeScalar name = member.get("_name");
RuntimeScalar name = getMemberNameScalar(member);
if (name == null) {
return new RuntimeScalar(AZ_ERROR).getList();
}
Expand Down Expand Up @@ -876,7 +889,7 @@ public static RuntimeList extractTree(RuntimeArray args, int ctx) {

for (int i = 0; i < members.size(); i++) {
RuntimeHash member = members.get(i).hashDeref();
RuntimeScalar name = member.get("_name");
RuntimeScalar name = getMemberNameScalar(member);
if (name == null) continue;

String memberName = name.toString();
Expand Down Expand Up @@ -933,15 +946,15 @@ public static RuntimeList removeMember(RuntimeArray args, int ctx) {
String targetName;
if (RuntimeScalarType.isReference(memberArg)) {
RuntimeHash member = memberArg.hashDeref();
RuntimeScalar name = member.get("_name");
RuntimeScalar name = getMemberNameScalar(member);
targetName = name != null ? name.toString() : "";
} else {
targetName = memberArg.toString();
}

for (int i = 0; i < members.size(); i++) {
RuntimeHash member = members.get(i).hashDeref();
RuntimeScalar name = member.get("_name");
RuntimeScalar name = getMemberNameScalar(member);
if (name != null && name.toString().equals(targetName)) {
RuntimeScalar removed = members.get(i);
// Remove from array
Expand Down Expand Up @@ -969,7 +982,7 @@ public static RuntimeList fileName(RuntimeArray args, int ctx) {
return scalarUndef.getList();
}
RuntimeHash member = args.get(0).hashDeref();
RuntimeScalar name = member.get("_name");
RuntimeScalar name = getMemberNameScalar(member);
return name != null ? name.getList() : scalarUndef.getList();
}

Expand Down Expand Up @@ -1179,9 +1192,25 @@ private static RuntimeArray getMembers(RuntimeHash self) {
return membersRef.arrayDeref();
}

private static void putMemberName(RuntimeHash member, String name) {
RuntimeScalar nameScalar = new RuntimeScalar(name);
member.put(MEMBER_NAME_KEY, nameScalar);
// CPAN Archive::Zip exposes this hash key and callers such as
// Archive::Extract read it directly instead of calling fileName().
member.put(MEMBER_COMPAT_FILENAME_KEY, nameScalar);
}

private static RuntimeScalar getMemberNameScalar(RuntimeHash member) {
RuntimeScalar name = member.get(MEMBER_NAME_KEY);
if (name == null || name.type == RuntimeScalarType.UNDEF) {
name = member.get(MEMBER_COMPAT_FILENAME_KEY);
}
return name;
}

private static RuntimeHash createMemberFromEntry(ZipFile zipFile, ZipEntry entry, Long rawDosTimestamp) throws IOException {
RuntimeHash member = new RuntimeHash();
member.put("_name", new RuntimeScalar(entry.getName()));
putMemberName(member, entry.getName());
member.put("_externalFileName", new RuntimeScalar(""));
member.put("_isDirectory", entry.isDirectory() ? scalarTrue : scalarFalse);
member.put("_uncompressedSize", new RuntimeScalar(entry.getSize()));
Expand Down Expand Up @@ -1234,7 +1263,7 @@ private static RuntimeHash createMemberFromEntry(ZipFile zipFile, ZipEntry entry
}

private static void writeMemberToZip(ZipOutputStream zos, RuntimeHash member) throws IOException {
RuntimeScalar name = member.get("_name");
RuntimeScalar name = getMemberNameScalar(member);
if (name == null) return;

ZipEntry entry = new ZipEntry(name.toString());
Expand Down
Loading
Loading