Skip to content

feat(moose): Moose-as-Moo shim + cross-platform ./jcpan -t Moose harness#565

Merged
fglock merged 4 commits intomasterfrom
feature/moose-shim
Apr 26, 2026
Merged

feat(moose): Moose-as-Moo shim + cross-platform ./jcpan -t Moose harness#565
fglock merged 4 commits intomasterfrom
feature/moose-shim

Conversation

@fglock
Copy link
Copy Markdown
Owner

@fglock fglock commented Apr 26, 2026

Summary

Implements the Quick path from dev/modules/moose_support.md: a thin pure-Perl Moose compatibility layer that delegates to Moo, so simple CPAN modules using use Moose; has ...; extends; with; can install and run on PerlOnJava without the real Class::MOP / mop.c.

This unblocks the long tail of CPAN modules that use Moose only for attribute declarations, inheritance, roles, and method modifiers — including the originally-reported ANSI::Unicode. It also wires up ./jcpan -t Moose to run upstream Moose's full test suite against the shim, in a cross-platform way (Unix + Windows).

Commits in this PR

  1. feat(moose): add Moose-as-Moo compatibility shim (Phase 1, Quick path) — the Moose.pm / Moose/Role.pm / Moose/Object.pm / Moose/Util/TypeConstraints.pm shim files.
  2. feat(moose): wire up ./jcpan -t Moose via CPAN distroprefs — bundled Moose.yml distropref so ./jcpan -t Moose actually runs the upstream test suite against the shim.
  3. feat(moose): auto-install Moo when running ./jcpan -t Moose — first iteration of Moo bootstrapping (POSIX-only).
  4. feat(moose): make ./jcpan -t Moose distropref cross-platform — replaces the POSIX-only shell with a small Perl helper so Windows works too.

Motivation

./jcpan -t ANSI::Unicode previously FAILED:

ETHER/Moose-2.4000.tar.gz
This distribution requires a working compiler at Makefile.PL line 12.
  /Users/fglock/projects/PerlOnJava2/jperl Makefile.PL -- NOT OK
...
Can't locate Moose.pm in @INC ...
t/basic.t ... Dubious, test returned 2

Moose 2.4000 ships 13 .xs files plus mop.c and dies in Makefile.PL if no C compiler is available. PerlOnJava can't satisfy that, and Moose isn't bundled.

What's bundled

File Purpose
src/main/perl/lib/Moose.pm Sets up the target as a Moo class; wraps has to translate string isa => 'Int' | 'Str' | ... into Moo-compatible coderef checks; drops Moose-only attribute options Moo doesn't know; expands lazy_build; installs a stub meta() and a Moose::Object ancestor marker.
src/main/perl/lib/Moose/Role.pm Analogous shim over Moo::Role.
src/main/perl/lib/Moose/Object.pm Minimal base class with new / BUILDARGS / does / DOES / meta.
src/main/perl/lib/Moose/Util/TypeConstraints.pm Best-effort stub for type / subtype / enum / class_type / role_type / duck_type / coerce DSL.
src/main/perl/lib/PerlOnJava/Distroprefs/Moose.pm Cross-platform helpers (bootstrap_pl_phase, noop) used by the bundled distropref's commandlines.
Bundled Moose.yml in src/main/perl/lib/CPAN/Config.pm Auto-bootstrapped to ~/.perlonjava/cpan/prefs/Moose.yml so ./jcpan -t Moose runs the upstream suite against the shim.

Type translation

The shim recognises Moose's standard scalar type names: Any, Item, Defined, Undef, Bool, Value, Ref, Str, Num, Int, ScalarRef, ArrayRef, HashRef, CodeRef, RegexpRef, GlobRef, FileHandle, Object, ClassName, plus Maybe[X] and ArrayRef[X] / HashRef[X] (parameterization is dropped, base is checked). Unknown names are treated as class names and verified via UNIVERSAL::isa.

Key implementation note: avoiding Moo's MOP bridge

Moo ships Moo::sification, which auto-bridges to real Moose's MOP whenever it sees $INC{"Moose.pm"} on Moo load. Since we are Moose.pm, this would unconditionally fire and require Class::MOP. Moose.pm pre-sets $Moo::sification::setup_done / $disabled in a BEGIN block before use Moo () so the sification bridge is a no-op.

./jcpan -t Moose against the shim

The bundled Moose.yml distropref makes ./jcpan -t Moose work end-to-end:

match:
  distribution: "^ETHER/Moose-"
pl:      jperl -MPerlOnJava::Distroprefs::Moose -e 'PerlOnJava::Distroprefs::Moose::bootstrap_pl_phase()'
make:    jperl -MPerlOnJava::Distroprefs::Moose -e 'PerlOnJava::Distroprefs::Moose::noop()'
test:    prove --exec jperl -r t/
install: jperl -MPerlOnJava::Distroprefs::Moose -e 'PerlOnJava::Distroprefs::Moose::noop()'

What each phase does:

  • pl — runs bootstrap_pl_phase(): require Moo and recursively invoke system $ENV{JCPAN_BIN}, 'Moo' if missing; then create a stub Makefile (so CPAN.pm's "no Makefile created" fallback path doesn't kick in).
  • make / installnoop() returns 0; cross-platform replacement for POSIX true / cmd /c exit 0.
  • test — runs prove --exec jperl -r t/ against the unpacked tarball. Because prove --exec invokes jperl per file without adding lib/ or blib/lib/ to @INC, the bundled shim from the jar wins over the unpacked upstream lib/Moose.pm.

To make this portable, jcpan / jcpan.bat now prepend the project directory to PATH (so jperl is findable as a plain command in any shell) and export JCPAN_BIN (so the helper can recursively call jcpan with an absolute path).

Why not a CPAN depends: block?

I tried it. Adding depends: requires: Moo: 0 to Moose.yml makes CPAN merge it with Moose's full upstream META.yml prereq tree (Package::Stash::XS, MooseX::NonMoose, …), and CPAN starts trying to install all of it — most is XS and unsatisfiable. Lots of noise, several unwanted side-effects. The pl-helper approach is narrower: it installs only the one thing the shim genuinely needs.

Cross-platform

The Moose.yml commandlines avoid POSIX-only constructs (||, ;, touch, /dev/null, $VAR) so they parse identically in bash, sh, cmd.exe, and PowerShell. All the actual logic is in the PerlOnJava::Distroprefs::Moose Perl module.

Out of scope (deferred to follow-up phases)

  • Real Class::MOP introspection ($meta->get_all_attributes etc.) — see Phase C/D in moose_support.md.
  • MooseX::Types, native traits (traits => ['Array']), Moose::Exporter deep MOP APIs.
  • Bundling Moo itself into the jar (still loaded from ~/.perlonjava/lib; auto-installed by the distropref on first jcpan run).
  • DESTROY / weaken semantics — handled on a separate branch (dev/architecture/weaken-destroy.md).

Plan updates

dev/modules/moose_support.md:

  • Marked Phase 1 (B-module sub names) as complete with the verification one-liner.
  • Corrected status of every dependency in the Class::MOP tree; removed stale "needs PP flag" / "needs investigation" notes after empirical re-checking on master.
  • Noted that Moose 2.4000 has 13 .xs files: bypassing the compiler check alone is necessary but not sufficient.
  • Added an Out of scope callout for DESTROY/weaken and JVM bytecode libraries (Byte Buddy / Javassist).
  • Added a Lock in progress as bundled-module tests section: when (and only when) we bundle a CPAN distribution from the Moose ecosystem, vendor its upstream t/ under src/test/resources/module/{Distribution}/. Tests for non-bundled downstream consumers (e.g. ANSI::Unicode) stay as ./jcpan -t smoke checks, not as module/ snapshots.
  • Added a Running upstream Moose's test suite against the shim section explaining the cross-platform distropref design and why we don't use depends:.
  • Added a Quick-path baseline table recording the 478 / 616 / ~29 / 370 / 246 numbers as the metric Phases C/D will improve.

Regression net

src/test/resources/module/ is reserved for unmodified upstream test files of CPAN distributions we actually bundle. Since this PR only ships a shim — no real Moose distribution is bundled yet — nothing is snapshotted under module/. The regression net is make plus the ./jcpan -t ANSI::Unicode end-to-end check. Upstream Moose-2.4000/t/ will be vendored once Phase D bundles a pure-Perl Moose.

Test plan

  • make — all unit tests pass.
  • make test-bundled-modules — all module tests pass (no new snapshots in this PR).
  • ./jcpan -t ANSI::Unicode — Result: PASS (t/basic.t ... ok, All tests successful).
  • ./jcpan -t Moose — full upstream suite runs end-to-end via the new distroprefs entry: 478 files / 616 assertions executed, ~29 files fully pass, 370 assertions ok, 246 fail (expected — most files require Class::MOP/Moose::Meta::* not provided by the shim). Recorded as Quick-path baseline in the plan.
  • Smoke-test of attribute creation, type validation (isa => 'Int'), required => 1, accessor read/write, isa('Moose::Object') — all expected behaviour.
  • JVM and interpreter backends both load the shim cleanly.
  • Missing-Moo path: hiding ~/.perlonjava/lib/Moo.pm and invoking PerlOnJava::Distroprefs::Moose::bootstrap_pl_phase directly triggers the system $ENV{JCPAN_BIN}, 'Moo' fallback.
  • Windows: distroprefs commandlines designed to be cmd.exe / PowerShell-portable but not yet exercised on a Windows runner; can be confirmed via the existing Windows CI workflow on demand.

Generated with Devin

@fglock fglock force-pushed the feature/moose-shim branch from 76f47af to 55bb5d0 Compare April 26, 2026 16:26
@fglock fglock changed the title feat(moose): add Moose-as-Moo compatibility shim (Quick path, unblocks ANSI::Unicode) feat(moose): Moose-as-Moo shim + cross-platform ./jcpan -t Moose harness Apr 26, 2026
fglock and others added 4 commits April 26, 2026 20:39
Implements the "Quick path" from dev/modules/moose_support.md: a thin
pure-Perl Moose compatibility layer that delegates to Moo so simple
CPAN modules using `use Moose; has ...; extends; with;` can install
and run on PerlOnJava without the real Class::MOP / mop.c.

What this enables:
  - `jcpan -t ANSI::Unicode` now passes (previously FAIL — Moose's
    Makefile.PL died with "This distribution requires a working
    compiler" because Moose 2.4000 ships 13 .xs files plus mop.c).
  - The long tail of CPAN modules that use Moose only for attribute
    declarations, inheritance, roles, and method modifiers.

What's bundled:
  - src/main/perl/lib/Moose.pm — sets up the target as a Moo class,
    wraps `has` to translate string `isa => 'Int' | 'Str' | ...` into
    Moo-compatible coderef checks, drops Moose-only attribute options
    Moo doesn't understand, expands `lazy_build`, installs a stub
    `meta()` and a `Moose::Object` ancestor marker.
  - src/main/perl/lib/Moose/Role.pm — analogous shim over Moo::Role.
  - src/main/perl/lib/Moose/Object.pm — minimal base class with
    new/BUILDARGS/does/DOES/meta.
  - src/main/perl/lib/Moose/Util/TypeConstraints.pm — best-effort stub
    for type/subtype/enum/class_type/role_type/duck_type DSL.

Key implementation note:
  Moo ships Moo::sification, which auto-bridges to real Moose's MOP
  whenever it sees $INC{"Moose.pm"} on Moo load. Since *we* are
  Moose.pm, this would unconditionally fire and require Class::MOP.
  Moose.pm pre-sets $Moo::sification::setup_done / $disabled in a
  BEGIN block before `use Moo ()` so the sification bridge is a no-op.

Out of scope (deferred to follow-up phases — see moose_support.md):
  - Real Class::MOP introspection ($meta->get_all_attributes etc.).
  - MooseX::Types, native traits, Moose::Exporter deep MOP APIs.
  - Bundling Moo itself into the jar (still loaded from
    ~/.perlonjava/lib via jcpan).
  - DESTROY/weaken semantics — handled on a separate branch
    (dev/architecture/weaken-destroy.md).

Plan updates (dev/modules/moose_support.md):
  - Marked Phase 1 (B-module sub names) as complete with verification.
  - Corrected status of every dependency in the Class::MOP tree;
    removed stale "needs PP flag" / "needs investigation" notes.
  - Noted that Moose 2.4000 has 13 .xs files: bypassing the compiler
    check alone is necessary but not sufficient.
  - Added "Out of scope" callout for DESTROY/weaken and JVM bytecode
    libraries (Byte Buddy / Javassist).
  - Added "Lock in progress as bundled-module tests" guidance:
    snapshot upstream tests under src/test/resources/module/
    whenever they start passing.

Regression net:
  - src/test/resources/module/ANSI-Unicode/t/basic.t (upstream copy)
    is now wired into `make test-bundled-modules` so this PR's gain
    can't silently regress.

Verification:
  - `make`                              → all unit tests pass
  - `make test-bundled-modules`         → all module tests pass
  - `./jcpan -t ANSI::Unicode`          → Result: PASS
  - JVM and interpreter backends both load the shim cleanly.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Adds a CPAN distroprefs entry so `./jcpan -t Moose` actually downloads
and runs Moose's full upstream test suite against PerlOnJava's
bundled Moose-as-Moo shim, without patching upstream sources.

How it works
------------
- `src/main/perl/lib/CPAN/Config.pm` already auto-bootstraps a set of
  bundled distroprefs YAMLs (Moo, Params-Validate). This commit adds
  `Moose.yml` to that list, written to
  `~/.perlonjava/cpan/prefs/Moose.yml` on first jcpan run.
- The Moose distropref matches `^ETHER/Moose-` and overrides each
  build phase:
    pl:      touch Makefile          (placates CPAN's "no Makefile
                                      created" fallback path)
    make:    true                    (nothing to build — XS skipped)
    test:    prove --exec "$JPERL_BIN" -r t/
    install: true                    (shim is already on @inc)
- `jcpan` / `jcpan.bat` now export `JPERL_BIN` pointing at the project's
  `jperl` launcher, so the distroprefs `prove --exec` line finds it
  regardless of the user's PATH.

Why this works
--------------
`prove --exec jperl` runs each .t file as `jperl <file>` without adding
`lib/` or `blib/lib/` to @inc. So the bundled shim from the jar
(`src/main/perl/lib/Moose.pm`) wins over the unpacked
`lib/Moose.pm` in the build dir. The full upstream suite runs without
needing a working compiler and without modifying any upstream source.

Result on this commit
---------------------
Full Moose-2.4000 suite, executed end-to-end:
  Files=478  Tests=616  ~29 files fully pass  370 assertions ok
  Result: FAIL  (expected — most files require Class::MOP /
                 Moose::Meta::* which the shim doesn't provide)

Plan updates (dev/modules/moose_support.md)
-------------------------------------------
- New section explaining the `./jcpan -t Moose` distropref recipe and
  its applicability to other "test against shim, don't install"
  scenarios.
- New "Quick-path baseline" subsection recording the 478/616/29/370/246
  numbers as the metric to beat in Phases C/D.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The Moose-as-Moo shim delegates to Moo at runtime. On a fresh checkout
where Moo isn't yet under ~/.perlonjava/lib, `./jcpan -t Moose` would
fail at shim load time. Two changes:

1. `jcpan` / `jcpan.bat` now also export `JCPAN_BIN` (in addition to
   `JPERL_BIN`), pointing at the active jcpan launcher. Distroprefs
   commandlines can use it to bootstrap missing helper modules.

2. The bundled `Moose.yml` distropref's `pl:` phase now runs:
       "$JPERL_BIN" -e "require Moo; 1" >/dev/null 2>&1 \
           || "$JCPAN_BIN" Moo; touch Makefile

   - If Moo is already loadable, the require returns immediately and
     touch creates the stub Makefile (effectively zero-cost).
   - If Moo is missing, we recursively invoke jcpan to install it,
     then create the stub Makefile.

Why not a CPAN `depends: requires: Moo:` block?
  Because CPAN merges that with Moose's upstream META prereqs and
  starts trying to resolve the entire XS-heavy tree (Package::Stash::XS,
  MooseX::NonMoose, ...), most of which is unsatisfiable on PerlOnJava.
  The pl-shell conditional is narrower: it installs only the one
  thing the shim genuinely needs.

Plan updated to document the new `pl:` step and explain the rationale
for not using `depends:`.

Verified: with Moo present, `./jcpan -t Moose` still produces the same
baseline (Files=478, Tests=616, Result: FAIL — expected, ~29 files
fully pass). The conditional shell logic is verified independently:
  bash -c '"$JPERL_BIN" -e "require Moo; 1" >/dev/null 2>&1 \
           || "$JCPAN_BIN" Moo; touch /tmp/M' → exit 0, M created.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Previous revision used POSIX-only shell in the Moose distropref:

  pl: '"$JPERL_BIN" -e "require Moo; 1" || "$JCPAN_BIN" Moo; touch Makefile'

That fails on Windows cmd.exe (`$VAR`, `||`/`;` semantics, `touch`,
`/dev/null` all wrong). This commit replaces it with a Perl-helper
approach that works the same in bash, sh, cmd.exe, and PowerShell.

Changes
-------
1. New `src/main/perl/lib/PerlOnJava/Distroprefs/Moose.pm` provides
   two helpers used by the distropref's commandlines:

     bootstrap_pl_phase()   ensure Moo is loadable (recursively
                            install via $ENV{JCPAN_BIN} if not),
                            then create a stub Makefile.
     noop()                 cross-platform replacement for POSIX
                            `true` / `cmd /c exit 0`.

2. `jcpan` / `jcpan.bat` prepend the project directory to PATH so
   shell-spawned subprocesses (distroprefs commandlines, prove's
   child processes) find `jperl` on both Unix and Windows without
   needing $JPERL_BIN tokens that don't expand in cmd.exe. They
   still export `JCPAN_BIN` so the helper can recursively invoke
   jcpan with an absolute path.

3. `Moose.yml` now uses portable invocations only:

     pl:      jperl -MPerlOnJava::Distroprefs::Moose -e '...bootstrap_pl_phase()'
     make:    jperl -MPerlOnJava::Distroprefs::Moose -e '...noop()'
     test:    prove --exec jperl -r t/
     install: jperl -MPerlOnJava::Distroprefs::Moose -e '...noop()'

   Each line is a single command with no shell-only constructs, so
   it parses identically in bash, sh, cmd.exe, and PowerShell.

Verification
------------
- `make` -- all unit tests pass.
- `./jcpan -t Moose` -- new pl-phase command reports OK; full Moose
  suite still runs (`Files=478, Tests=616, Result: FAIL` -- expected
  baseline, ~29 files fully pass under the shim).
- Missing-Moo path verified by hiding ~/.perlonjava/lib/Moo.pm and
  invoking the helper directly: `require Moo` fails as expected, and
  the fallback would invoke `$ENV{JCPAN_BIN} Moo`.

Plan updated to describe the cross-platform design and explicitly
call out the shell-construct pitfalls in the previous revision.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@fglock fglock force-pushed the feature/moose-shim branch from 47a34fe to 83ddbdc Compare April 26, 2026 18:40
@fglock fglock merged commit cdaa1b7 into master Apr 26, 2026
2 checks passed
@fglock fglock deleted the feature/moose-shim branch April 26, 2026 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant