If you've ever installed a CPAN distribution on two different machines and been surprised when the C extension links against different versions of libz, you've hit the classic "shared store" problem. Every CPAN module on the system shares one global xrepo store (~/.xmake/packages/, or %LOCALAPPDATA%\.xmake\packages\ on Windows); one install, upgrade, or accidental xrepo clean can silently change what a downstream Alien sees.
Alien::Xrepo solves this with installdir isolation: a project-local package store that never touches the global one. This article shows how to pin your native dependencies to a directory, cache them in CI, and transfer them offline.
The default: the global store
By default, every call to $repo->install(...) writes to the shared per-user store:
~/.xmake/packages/l/libpng/v1.6.58/<hash>/bin/png.dll
That's convenient because multiple Perl modules share one libpng. But it means a background xrepo clean or an unrelated install with different flags can change what your code sees.
The fix: project-local isolation
Pass root => to the constructor, or installdir => to any store-touching method, and the operation is confined to that directory:
use v5.40;
use Alien::Xrepo;
use Path::Tiny;
my $store = Path::Tiny->tempdir(CLEANUP => 1);
my $repo = Alien::Xrepo->new(root => $store);
# Both go into $store, not the global store
$repo->install('zlib');
$repo->install('libpng');
You can also isolate per-call, which is useful when the rest of your code uses the default store:
my $repo = Alien::Xrepo->new;
$repo->install('pcre2', undef, installdir => $store);
# This stays in the global store
$repo->install('sqlite3');
The installdir option applies to install, fetch, scan, uninstall, download, import_pkg, export, and every other method that touches the package store.
The isolation guard
Isolation is enforced, not just requested. xmake likes to satisfy requirements from the system (Homebrew, apt, ...) instead of building into a store so a package can end up living outside your pinned directory. Alien::Xrepo watches for that: if a resolution's install root isn't inside the requested store, it dies rather than silently handing you a path that breaks the "project-local" promise. If you hit that error, either drop the root/installdir requirement or ask for a package the system doesn't already provide.
What about the cache?
Downloading and building from source can be slow. A project-local installdir isolates where the installed files go, but the downloaded sources still live in the global cache by default.
To isolate the cache too, pass cachedir =>:
$repo->install('libpng', undef, installdir => $store, cachedir => $store);
This is especially handy in CI: the first run builds from source and caches; subsequent runs find everything already cached and return instantly.
What lives in the store?
The scan method lists every package under a given store (namespace it with a package name to filter):
say for $repo->scan('zlib', installdir => $store);
This is also useful for verification. In a CI pipeline, after running install, you can scan the store and assert the expected package is present:
my @found = $repo->scan('libpng', installdir => $store);
die 'libpng not found in store' unless @found;
Offline transfer with download / export / import_pkg
On machines without internet access (air-gapped CI runners, corporate build servers), you need a way to move the store around. Alien::Xrepo provides three methods for this:
1. Download source archives
Fetch the source tarballs/7z archives to a local directory without building:
$repo->download('libpng', undef, outputdir => $dl_dir);
2. Export a built package
After building on a connected machine, export the package as a single archive:
$repo->export('libpng', undef, packagedir => $export_dir);
3. Import on the target machine
On the air-gapped machine, import from the archive:
$repo->import_pkg('libpng', undef, installdir => $store);
The three steps form a complete offline workflow: build once on a connected machine, export, transfer the archive, import on the target. No internet, no compilers, no retries.
CI integration
A concrete CI pattern to build and cache a project-local store:
# .github/workflows/ci.yml (pseudocode)
- name: Install native deps
run: |
perl -MAlien::Xrepo -e '
my $repo = Alien::Xrepo->new;
$repo->install("libpng", undef, installdir => ".cache/native");
$repo->install("zlib", undef, installdir => ".cache/native");
'
- name: Cache native store
uses: actions/cache@v4
with:
path: .cache/native
key: native-${{ runner.os }}-${{ hashFiles('cpanfile') }}
On cache hit, the install calls resolve instantly (everything is already in the directory). On cache miss, they build from source and populate the cache.
The dist builder integration
The same isolation is how Alien::Xrepo::Build keeps a CPAN distribution self-contained. When a dist is built, the pipeline installs every package in its recipe into an isolated store rooted at the distribution's share directory (blib/lib/auto/share/dist/<Alien-Dist>), and its export stage writes the resolved paths into a snapshot file in that same share directory:
use v5.40;
use FindBin qw[$Bin];
use Path::Tiny;
use Alien::Xrepo::Build;
my $snapshot = path($Bin)->child( 'blib', 'lib', 'auto', 'share', 'dist', 'Alien-Zstandard', 'xrepo-snapshot.json' );
Alien::Xrepo::Build->new( recipe => $Bin, snapshot => $snapshot )->run;
Downstream, Alien::Xrepo::Runtime auto-detects that snapshot and serves every accessor from it, hermetic, offline, with no xrepo subprocess and no shared store involved at all. It's the same root/installdir principle, applied at distribution-build time rather than at user-install time: isolate the store, and nothing leaks in or out.
Summary
| Scope | Mechanism | Use case |
|---|---|---|
| Global (default) | No options | Shared across all CPAN modules |
| Per-instance | root => $store |
One Alien module, own store |
| Per-call | installdir => $store |
Mix local and global stores |
| CI cache | cachedir => $store |
Skip source builds on cache hit |
| Offline | download / export / import_pkg |
Air-gapped build servers |
Alien::Xrepo's store isolation turns a shared, mutable resource into a pinned, reproducible one; exactly what you need when a CI run or a CPAN release depends on a specific version of a native library.
Comments