Alien-Xrepo

 view release on metacpan or  search on metacpan

Changes.md  view on Meta::CPAN

- `Alien::Xrepo::Runtime` snapshot introspection: `has_snapshot`, `snapshot_path`, and `snapshot_errors`, so a consumer's tests can tell a cold checkout (no snapshot) from a snapshot that was written but is incomplete (a failed build).

### Changed

- `Alien::Xrepo::MB` and `Alien::Xrepo::MM` now call `assert_installed` after the build, turning a partial or failed install into a build failure.

## [v1.0.4] - 2026-10-07

### Fixed

- ExtUtils::Install installs files as readonly (mode 0444) so Xmake's templates (used by `xmake create`, etc.) would copy readonly files into a project and then try to modify them which obviously does not work. I just copy them to the global xmake di...
- In situations like prebuilt libs bundled in a single archive, `libpath` used to resolve to whichever lib the filesystem happened to sort first. The runtime library now uses the package's `links`; falling back to the first lib when none match.

## [v1.0.3] - 2026-10-06

### Added

- `system` recipe option. With `system => 1` a package is allowed to resolve from the system (Homebrew, apt, vcpkg, ...), which makes `add_extsources()` in a package recipe usable again. Without it an install is a private build into its own store, as...

### Fixed

README.md  view on Meta::CPAN

Exports installed package files for offline use. `packagedir` selects the destination directory.

## `env( [ ..., [ ... ] ] )`

```perl
$repo->env( 'bash', bind => 'zlib' );   # run a program inside the package env
$repo->env( undef, show => 1 );         # only print the environment
```

Sets up the package environment and either prints it (`show`) or executes `$program` (default `shell`) inside it.
`bind` selects which environment config or package to bind, `list` lists global configs, and `add`/`remove` manage
global environment config files.

## `list_repo()`

```perl
my @repos = $repo->list_repo();
```

Lists all configured remote repositories (as output lines).

## `uninstall( ..., [ ... ] )`

README.md  view on Meta::CPAN

- **What is stored**

    The raw `fetch --json` output plus the resolved install directory, keyed by a SHA-1 fingerprint of the package spec
    and every option that can move the installed layout (`kind`, `plat`, `arch`, `mode`, `configs`). Config values run
    through the same boolean stringifier as the CLI, so `configs => { shared => true }` and `configs =>
    { shared => 'true' }` share a key.

- **Where it lives**

    A `cache.json` under `<store>/.alien-xmake/` when an instance or per-call store is set (`root` / `installdir`),
    otherwise under `<xmake global dir>/.alien-xmake/` (normally `~/.xmake`), honoring `XMAKE_GLOBALDIR`.

- **Validation**

    A hit is only trusted while its recorded install directory still exists on disk. An entry whose package was
    uninstalled, expired, or otherwise removed disqualifies itself and is pruned on the next save, after which the package
    is reinstalled normally.

- **Bounded**

    Entries are kept Least-Recently-Used with a fixed cap (64), so the file stays a few hundred KB no matter how many

README.md  view on Meta::CPAN

The installed results (`libpath`, `includedirs`, `links`, ...) are decoded into an [Alien::Xrepo::PackageInfo](https://metacpan.org/pod/Alien%3A%3AXrepo%3A%3APackageInfo)
exactly like any `xmake-repo` package, so your wrapper code does not care where the library came from.

See the `xmake-repo` [integration notes](https://github.com/xmake-io/xrepo-docs/blob/master/getting_started.md) for
the corresponding `add_requires` syntax inside an `xmake` project.

## How third-party namespaces are installed

A package in a third-party namespace (`vcpkg::zlib`) is installed by the _external_ manager, and `xmake`'s
integration does not bootstrap that manager for you: e.g., the `vcpkg` integration raises **`vcpkg not found!`** when
it cannot locate the tool (`find_vcpkgdir` checks the `xmake g --vcpkg` global config, `$VCPKG_ROOT`,
`$VCPKG_INSTALLATION_ROOT`, Homebrew, and the Windows `vcpkg.path.txt` lookup in that order). A recipe that installs
from a third-party namespace therefore lists the **bare tool package** that owns the namespace as well:

```perl
packages => [ { name => 'vcpkg::zlib' }, { name => 'vcpkg' } ]
```

These two names mean different things, and their order in the recipe is not their install order:

- **Recipe order**: (`vcpkg::zlib` then `vcpkg`) is the _consumer_ order. [Alien::Xrepo::Runtime](https://metacpan.org/pod/Alien%3A%3AXrepo%3A%3ARuntime) resolves `libpath`, `find_header`, `cflags`, and `libs` against the _first_ package, so the libr...

README.md  view on Meta::CPAN


Two implementation details make that work and are easy to get wrong (see [Alien::Xrepo::Build](https://metacpan.org/pod/Alien%3A%3AXrepo%3A%3ABuild)):

- `_pkg_installdir` refuses _namespaced_ (`::`) packages. The external manager owns the on-disk layout, so a per-package store path such as `<share>/vcpkg::zlib` is meaningless and the store guard would reject a perfectly valid install. Leaving `inst...
- Environment propagation uses a hash-slice `local`:

    ```
    local @ENV{ keys %env } = values %env if %env;
    ```

    The otherwise-obvious form `local $ENV{$_} = $env{$_} for keys %env` silently binds the _global_ `$_` (not the
    loop variable) in some builds of Perl, so the values never reach the child process.

Also note that the decoded `fetch --json` output for third-party packages is not always a tidy list: `libfiles`,
`bindirs`, `includedirs`, and `linkdirs` may each arrive as a plain string when there is exactly one file or
directory. The engine normalizes those scalars into one-element lists before use.

# SEE ALSO

[https://xrepo.xmake.io](https://xrepo.xmake.io), [https://packages.xmake.io/](https://packages.xmake.io/)

eg/examples/Exotic-Lsquic/t/02_in_use.t  view on Meta::CPAN


# The package must actually be installed (a build-served snapshot or a store
# hit); otherwise there is nothing at all to exercise.
SKIP: {
    my $info = $lsquic->package_info;
    skip 'lsquic not installed; run the build first' => 1 unless $info;
    ok defined $lsquic->version, 'resolved package version is recorded';
}

# lsquic has no exported version getter, so load the installed library and
# confirm it answers a real call (lsquic_global_init(0) returns 0). First FFI
# backend that is installed (Affix, then FFI::Platypus); neither installed, or
# a static archive: skip.
SKIP: {
    my $info = $lsquic->package_info;
    skip 'package not installed, or resolved as a static archive with nothing to load' => 1 unless $info && $info->shared && $lsquic->ffi_lib;
    my $lib = $lsquic->ffi_lib;
    if ( eval { require Affix; Affix->import; 1 } ) {
        Affix::affix( $lib, 'lsquic_global_init', [ Affix::Int() ], Affix::Int() );
        my $rc = lsquic_global_init(0);
        diag "Affix loaded $lib; lsquic_global_init(0) rc=$rc";
        is $rc, 0, 'lsquic initializes (real library responds)';
    }
    elsif ( eval { require FFI::Platypus; 1 } ) {
        my $ffi = FFI::Platypus->new( api => 2, lib => [$lib] );
        $ffi->attach( 'lsquic_global_init' => ['int'] => 'int' );
        my $rc = lsquic_global_init(0);
        diag "FFI::Platypus loaded $lib; lsquic_global_init(0) rc=$rc";
        is $rc, 0, 'lsquic initializes (real library responds)';
    }
    else {
        skip 'neither Affix nor FFI::Platypus is installed' => 1;
    }
}
#
done_testing;

lib/Alien/Xrepo.pm  view on Meta::CPAN

    # Default package kind (-k) for every action; a per-call `kind` wins. undef omits -k so installs match a bare
    # `xrepo` command, matching cd52e9b behavior.
    field $kind : param //= ();

    # Warm-start resolution cache. install() memorizes a successful fetch result under this instance's store so a
    # repeat launch skips xrepo entirely. Entries are LRU-bounded and a hit is only trusted while its recorded install
    # dir still exists. Pass cache => 0 to disable (pure fetch-first behavior, one spawn per launch).
    field $cache : param //= 1;

    # Resolve which package store a call should operate on. A per-call `installdir` wins, then the `root` handed to the
    # constructor, then whatever xrepo/the environment chooses (global store).
    method _store_dir (%opts) { return $opts{installdir} // $root }
    method blah       ($msg)  { return unless $verbose; say $msg; }

    # Echo every command we are about to run
    method _debug_cmd (@cmd) {
        my $str = join( ' ', map { /\s/ ? qq{"$_"} : $_ } @cmd );
        my @env;
        push @env, "XMAKE_THEME=$ENV{XMAKE_THEME}"                   if defined $ENV{XMAKE_THEME};
        push @env, "XMAKE_PKG_INSTALLDIR=$ENV{XMAKE_PKG_INSTALLDIR}" if defined $ENV{XMAKE_PKG_INSTALLDIR};
        push @env, "XMAKE_PKG_CACHEDIR=$ENV{XMAKE_PKG_CACHEDIR}"     if defined $ENV{XMAKE_PKG_CACHEDIR};

lib/Alien/Xrepo.pm  view on Meta::CPAN

            $self->_cache_put( $key, $meta, $self->_cache_entry($fresh) );
            $self->_cache_save( $meta, %opts );
        }
        $self->_finalize( $fresh, %opts );
    }

    # LRU cap on cached resolutions. Entries are just the fetch JSON plus its install dir, so the bound keeps the file
    # a few hundred KB even after a long debugging session.
    sub _CACHE_MAX () {64}

    # Where the resolution cache lives: alongside the caller's/instance's store when one is set, else the xmake global
    # dir (~/.xmake) that xrepo itself uses for the default per-user store.
    method _cache_dir (%opts) {
        my $store = $self->_store_dir(%opts);
        return path($store)->child('.alien-xmake') if defined $store && length $store;
        my $home = $ENV{XMAKE_GLOBALDIR};
        $home //= $ENV{HOME};
        if ( !defined $home || !length $home ) {
            $home = $^O eq 'MSWin32' ? "$ENV{HOMEDRIVE}$ENV{HOMEPATH}" : ();
        }
        return () if !defined $home || !length $home;

lib/Alien/Xrepo.pm  view on Meta::CPAN

    method _ensure_templates_available() {
        return unless $^O eq 'MSWin32';
        my $exe = eval { $xmake->exe };
        return unless defined $exe && length $exe && -e $exe;
        my $src = path($exe)->parent->child('templates');
        return unless $src->is_dir;

        # ExtUtils::Install ships these as 0444, and on Windows that attribute survives xmake's
        # {writeable=true} copy, so `xmake create` dies "cannot open file ... Permission denied" while
        # rewriting the copied xmake.lua. The installed tree under perl/lib should stay read-only, so mirror
        # the built-in templates into the user's writable xmake global dir instead -- <globaldir>/templates,
        # xmake's own per-user override, which it resolves before <programdir>/templates. Seeded once per
        # installed xmake version; an upgrade recopies the mirror wholesale.
        #
        # The global dir comes from xmake itself: its `~` mapping (LOCALAPPDATA on Windows) and the
        # %APPDATA% legacy fallback are not portable to replicate in Perl, but `lua -c` prints the very
        # directory create will consult. Single-quoted Lua strings only: win32's naive argv quoting mangles
        # embedded double quotes (which surfaces as a bogus "Can't spawn" warning and exit 255).
        my $script  = q{import('core.base.global'); print('XREPO_GLOBALDIR=' .. global.directory())};
        my $lines   = capture { system $exe, 'lua', '-c', $script };
        my ($gldir) = $lines =~ /^XREPO_GLOBALDIR=(.+)$/m;
        return unless defined $gldir;
        $gldir =~ s/\s+$//;
        my $dst = path($gldir)->child('templates');
        $dst->mkpath;
        my $marker = $dst->child('.alien-xrepo-synced');
        my $ver    = eval { $xmake->_getver } // '';
        my $synced = $marker->exists ? $marker->slurp : '';
        return if length $synced && ( !length $ver || $synced eq "$ver\n" );
        $src->visit(
            sub {
                my $file = shift;
                return if $file->is_dir;
                my $to = $dst->child( $file->relative($src) );
                $to->parent->mkpath;

                # Windows copies carry the source's read-only attribute with them, so force the per-user mirror
                # writable (it lives in <globaldir>/templates, xmake's own mutable per-user template override).
                $file->copy($to);
                $to->chmod( $to->stat->mode | 0200 );
            },
            { recurse => 1 }
        );
        $marker->spew("$ver\n");
    }

    method _argv ( $action, $flags, @spec ) {
        $self->_ensure_working_project;

lib/Alien/Xrepo.pm  view on Meta::CPAN

    # Default OS locations whose libraries the loader already finds without help.
    method _system_lib_dirs () {
        if ( $^O eq 'MSWin32' ) {
            my $root = $ENV{SystemRoot} // 'C:\\Windows';
            return grep {-d} map { path($root)->child($_)->stringify } qw[System32 SysWOW64];
        }
        if ( $^O eq 'darwin' ) {
            return grep {-d} qw[/usr/lib /usr/lib/system /System/Library/Frameworks /System/Library/PrivateFrameworks /Library/Frameworks];
        }
        my @dirs = qw[/lib /lib64 /usr/lib /usr/lib64 /usr/local/lib /usr/local/lib64];
        push @dirs, grep {-d} glob('/lib/*-linux-gnu'), glob('/usr/lib/*-linux-gnu');
        return grep {-d} @dirs;
    }

    # A bare loader name the OS itself provides: the Windows API sets/CRT, Mach-O system install names, and the
    # usual ELF runtime sonames. Anything else that stayed unresolved is worth reporting.
    method _looks_system_lib ($name) {
        if ( $^O eq 'MSWin32' ) {
            return 1 if $name =~ /\A(?:api-ms-win-|ext-ms-)/i;
            my $sys
                = qr/\A(?:ucrtbase|vcruntime\d*|msvcp\d*|msvcr\d*|kernel32|kernelbase|user32|gdi32|gdi32full|advapi32|shell32|ole32|oleaut32|ws2_32|ntdll|combase|bcrypt|bcryptprimitives|cryptbase|userenv|imm32|setupapi|version|winmm|shlwapi|rpcrt4|se...

lib/Alien/Xrepo.pod  view on Meta::CPAN

    $repo->export( 'zlib', undef, packagedir => './packages', shallow => 1 ); # Export the latest version

Exports installed package files for offline use. C<packagedir> selects the destination directory.

=head2 C<env( [ ..., [ ... ] ] )>

    $repo->env( 'bash', bind => 'zlib' );   # run a program inside the package env
    $repo->env( undef, show => 1 );         # only print the environment

Sets up the package environment and either prints it (C<show>) or executes C<$program> (default C<shell>) inside it.
C<bind> selects which environment config or package to bind, C<list> lists global configs, and C<add>/C<remove> manage
global environment config files.

=head2 C<list_repo()>

    my @repos = $repo->list_repo();

Lists all configured remote repositories (as output lines).

=head2 C<uninstall( ..., [ ... ] )>

    $repo->uninstall( 'zlib' );

lib/Alien/Xrepo.pod  view on Meta::CPAN

=item * B<What is stored>

The raw C<fetch --json> output plus the resolved install directory, keyed by a SHA-1 fingerprint of the package spec
and every option that can move the installed layout (C<kind>, C<plat>, C<arch>, C<mode>, C<configs>). Config values run
through the same boolean stringifier as the CLI, so C<< configs =E<gt> { shared =E<gt> true } >> and C<< configs =E<gt>
{ shared =E<gt> 'true' } >> share a key.

=item * B<Where it lives>

A C<cache.json> under F<< <store>/.alien-xmake/ >> when an instance or per-call store is set (C<root> / C<installdir>),
otherwise under F<< <xmake global dir>/.alien-xmake/ >> (normally F<~/.xmake>), honoring C<XMAKE_GLOBALDIR>.

=item * B<Validation>

A hit is only trusted while its recorded install directory still exists on disk. An entry whose package was
uninstalled, expired, or otherwise removed disqualifies itself and is pruned on the next save, after which the package
is reinstalled normally.

=item * B<Bounded>

Entries are kept Least-Recently-Used with a fixed cap (64), so the file stays a few hundred KB no matter how many

lib/Alien/Xrepo.pod  view on Meta::CPAN

The installed results (C<libpath>, C<includedirs>, C<links>, ...) are decoded into an L<Alien::Xrepo::PackageInfo>
exactly like any C<xmake-repo> package, so your wrapper code does not care where the library came from.

See the C<xmake-repo> L<integration notes|https://github.com/xmake-io/xrepo-docs/blob/master/getting_started.md> for
the corresponding C<add_requires> syntax inside an C<xmake> project.

=head2 How third-party namespaces are installed

A package in a third-party namespace (C<vcpkg::zlib>) is installed by the I<external> manager, and C<xmake>'s
integration does not bootstrap that manager for you: e.g., the C<vcpkg> integration raises B<C<vcpkg not found!>> when
it cannot locate the tool (C<find_vcpkgdir> checks the C<xmake g --vcpkg> global config, C<$VCPKG_ROOT>,
C<$VCPKG_INSTALLATION_ROOT>, Homebrew, and the Windows C<vcpkg.path.txt> lookup in that order). A recipe that installs
from a third-party namespace therefore lists the B<bare tool package> that owns the namespace as well:

    packages => [ { name => 'vcpkg::zlib' }, { name => 'vcpkg' } ]

These two names mean different things, and their order in the recipe is not their install order:

=over

=item * B<Recipe order>: (C<vcpkg::zlib> then C<vcpkg>) is the I<consumer> order. L<Alien::Xrepo::Runtime> resolves C<libpath>, C<find_header>, C<cflags>, and C<libs> against the I<first> package, so the library comes first and the bare tool package ...

lib/Alien/Xrepo.pod  view on Meta::CPAN

Two implementation details make that work and are easy to get wrong (see L<Alien::Xrepo::Build>):

=over

=item * C<_pkg_installdir> refuses I<namespaced> (C<::>) packages. The external manager owns the on-disk layout, so a per-package store path such as C<< <share>/vcpkg::zlib >> is meaningless and the store guard would reject a perfectly valid install....

=item * Environment propagation uses a hash-slice C<local>:

    local @ENV{ keys %env } = values %env if %env;

The otherwise-obvious form C<< local $ENV{$_} = $env{$_} for keys %env >> silently binds the I<global> C<$_> (not the
loop variable) in some builds of Perl, so the values never reach the child process.

=back

Also note that the decoded C<fetch --json> output for third-party packages is not always a tidy list: C<libfiles>,
C<bindirs>, C<includedirs>, and C<linkdirs> may each arrive as a plain string when there is exactly one file or
directory. The engine normalizes those scalars into one-element lists before use.

=head1 SEE ALSO



( run in 1.015 second using v1.01-cache-2.11-cpan-1236ad508bc )