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/); 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 — 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, and every other method that touches the package store.

What about the cache?

Downloading and building from source is 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:

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 / import_pkg / export

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, shallow => 1);

2. Export a built package

After building on a connected machine, export the package as a single archive:

$repo->export('libpng', undef, outputdir => $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 — 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 dist builder (used internally to build this distribution's own share/ directory) uses the same root mechanism to install all of xmake's own dependencies into the distribution's share/ directory at build time. When a user runs ./Build, Alien::Xrepo::Base automatically creates an isolated store under blib/share so that the built distribution is fully self-contained.

This is the same root / installdir mechanism described above, just applied at distribution-build time rather than at user-install time. The principle is the same: 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.