App-Ariza.git | t/ | 13-bundle.rakutest
use v6.d;
use Test;
use JSON::Fast;
use App::Ariza::Bundle;
use App::Ariza::Tools;
plan 8;
sub tmp-dir(--> IO::Path) {
my $dir = $*TMPDIR.add("ariza-bundle-{$*PID}-{(^1_000_000).pick}");
$dir.mkdir;
$dir;
}
#| An app checkout: a META6.json, an ariza.toml, and nothing else. Enough
#| for everything `build` decides before it touches the network.
sub fake-app(IO::Path $dir, :$version = '9.9.9', :$meta = True,
:@platforms = <macos-arm64 linux-x86_64-glibc> --> IO::Path) {
$dir.add('META6.json').spurt(to-json({
name => 'App::ExampleApp', version => $version, license => 'Artistic-2.0',
})) if $meta;
$dir.add('ariza.toml').spurt(
"[app]\nname = \"App::ExampleApp\"\nexec = \"exampleapp\"\n"
~ "display = \"Example App\"\n\n[bundle]\n"
~ "platforms = [{@platforms.map({ "\"$_\"" }).join(', ')}]\n");
$dir;
}
my constant MANIFEST = {
'ariza-manifest' => 1,
ariza => '0.1.0',
built-at => '2026-08-11T18:43:01Z',
platform => 'macos-arm64',
app => { name => 'App::ExampleApp', exec => 'exampleapp',
display => 'Example App', version => '9.9.9' },
components => {
rakudo => { tag => '2026.07-01', version => '2026.07', revision => '01' },
sqlcipher => { version => '4.14.0', path => 'rakudo/lib/libsqlcipher.0.dylib' },
notcurses => { tag => 'binaries-notcurses-3.0.17-r11',
path => 'native/Notcurses-Native/binaries-notcurses-3.0.17-r11/lib' },
runner => { tag => 'runner-v1', artifact => 'ariza-runner-windows-x86_64.exe',
sha256 => 'f' x 64, url => 'https://example.invalid/r.exe',
path => 'bin/exampleapp.exe' },
},
};
subtest 'the bundle is named after the app, its version and its platform', {
plan 2;
is App::Ariza::Bundle.bundle-name(:exec<exampleapp>, :version<9.9.9>,
:platform<macos-arm64>),
'exampleapp-9.9.9-macos-arm64',
'which is the workdir name and the archive stem';
is App::Ariza::Bundle.bundle-name(:exec<exampleapp>, :version<9.9.9>,
:platform<windows-x86_64>),
'exampleapp-9.9.9-windows-x86_64', 'per platform, so releases coexist';
};
subtest 'the version comes from the checkout, and a bad one is named', {
plan 4;
my $dir = tmp-dir;
LEAVE { rm-rf($dir) }
is App::Ariza::Bundle.app-version(fake-app($dir)), '9.9.9',
'read from the checkout\'s META6.json';
throws-like { App::Ariza::Bundle.app-version($dir.add('elsewhere')) },
Exception, message => /'no META6.json in'/, 'a directory that is not a dist';
$dir.add('META6.json').spurt('{ not json');
throws-like { App::Ariza::Bundle.app-version($dir) }, Exception,
message => /'malformed META6.json'/, 'a broken META6';
$dir.add('META6.json').spurt(to-json({ name => 'App::ExampleApp' }));
throws-like { App::Ariza::Bundle.app-version($dir) }, Exception,
message => /'has no version'/,
'and one with no version, rather than naming the bundle "exampleapp--macos-arm64"';
};
subtest 'a build refuses the wrong platform before it downloads anything', {
plan 3;
my $dir = tmp-dir;
LEAVE { rm-rf($dir) }
fake-app($dir);
throws-like {
App::Ariza::Bundle.build(:app-dir($dir), :platform<macos-aarch64>,
:out-dir($dir.add('out')), :!verbose)
}, Exception, message => /'not a platform ariza knows'/,
'a misspelled slug — macos-aarch64 for macos-arm64 — is caught';
# An app that lists its platforms has said which ones it is tested
# on; producing an artefact named after one it never claimed is a
# promise ariza has no business making for it.
throws-like {
App::Ariza::Bundle.build(:app-dir($dir), :platform<windows-x86_64>,
:out-dir($dir.add('out')), :!verbose)
}, Exception, message => /'does not declare' .* 'windows-x86_64'/,
'and a platform the app does not declare';
throws-like {
App::Ariza::Bundle.build(:app-dir($dir), :platform<windows-x86_64>,
:out-dir($dir.add('out')), :!verbose)
}, Exception, message => /'macos-arm64, linux-x86_64-glibc'/,
'naming the ones it does';
};
subtest 'an app with no declared platforms constrains nothing', {
plan 1;
my $dir = tmp-dir;
LEAVE { rm-rf($dir) }
fake-app($dir, :platforms(()));
# The build will fail later, at the network — the point is that it
# gets past the platform gate rather than being refused on principle.
throws-like {
App::Ariza::Bundle.build(:app-dir($dir), :platform<windows-arm64>,
:out-dir($dir.add('out')), :!verbose)
}, Exception, message => /'publishes no binary build'/,
'an unconstrained app reaches the runtime step, and fails there instead';
};
subtest 'VERSION is one screen a human can quote in a bug report', {
plan 8;
my $text = App::Ariza::Bundle.version-file(MANIFEST);
my @lines = $text.lines;
is @lines[0], 'Example App 9.9.9 (macos-arm64)',
'the headline names the product, the version and the platform';
ok @lines.first(/^ 'app' \s+ 'App::ExampleApp 9.9.9'/), 'the distribution';
ok @lines.first(/^ 'rakudo' \s+ '2026.07-01'/), 'the embedded runtime';
ok @lines.first(/^ 'sqlcipher' \s+ '4.14.0'/), 'SQLCipher';
ok @lines.first(/^ 'notcurses' \s+ 'binaries-notcurses-3.0.17-r11'/),
'the notcurses build, by the tag it was actually staged under';
ok @lines.first(/^ 'runner' \s+ 'runner-v1'/),
'the compiled Windows launcher, by the release it was pinned from';
ok @lines.first(/^ 'built' \s+ '2026-08-11T18:43:01Z'/), 'and when';
ok $text.contains('Run bin/exampleapp'),
'ending with the one thing the reader has to do';
};
subtest 'the manifest keeps Site\'s exact notcurses library directory', {
plan 3;
my %component = App::Ariza::Bundle.notcurses-component(%(
notcurses-tag => 'binaries-notcurses-3.0.17-r11',
notcurses-lib-rel =>
'native/Notcurses-Native/binaries-notcurses-3.0.17-r11/lib',
));
is %component<tag>, 'binaries-notcurses-3.0.17-r11',
'the staged binary tag is recorded';
is %component<path>,
'native/Notcurses-Native/binaries-notcurses-3.0.17-r11/lib',
'with the exact tag-selected lib directory passed out by Site';
my %legacy = App::Ariza::Bundle.notcurses-component(%(
notcurses-tag => 'binaries-notcurses-3.0.17-r10',
));
is %legacy<path>,
'native/Notcurses-Native/binaries-notcurses-3.0.17-r10/lib',
'and an older Site result still has one canonical derivation';
};
subtest 'the manifest records the SQLCipher that was staged, not the pin', {
plan 6;
# The pin is advisory: the build machine's package manager decides
# what is actually bundled, so a manifest quoting the pin would be
# quoting a number nobody verified.
my %m = App::Ariza::Bundle.sqlcipher-component(%(
version => '4.17.0', pinned => '4.14.0',
rel => 'rakudo/lib/libsqlcipher.0.dylib', sha256 => 'abc123',
origin => 'homebrew keg: /opt/homebrew/opt/sqlcipher/lib/libsqlcipher.0.dylib',
));
is %m<version>, '4.17.0', 'the version staged';
is %m<pinned>, '4.14.0', 'with the version expected beside it, so a'
~ ' reader can see the two disagree';
is %m<path>, 'rakudo/lib/libsqlcipher.0.dylib', 'where it went';
is %m<source>,
'homebrew keg: /opt/homebrew/opt/sqlcipher/lib/libsqlcipher.0.dylib',
'and where it came from';
# A library whose bytes did not name a version: still not the pin.
my %unknown = App::Ariza::Bundle.sqlcipher-component(%(
version => Str, pinned => '4.14.0', rel => 'native/sqlcipher/sqlcipher.dll',
sha256 => 'def456', origin => 'SQLCIPHER_LIB_DIR: C:\\vcpkg\\bin\\sqlcipher.dll',
));
is %unknown<version>, 'unknown', 'said plainly rather than filled in';
is %unknown<pinned>, '4.14.0', 'the pin is still recorded as the expectation';
};
subtest 'the manifest can account for the runner it downloaded', {
plan 6;
my $dir = tmp-dir;
LEAVE { rm-rf($dir) }
my $exe = ensure-dir($dir.add('bin')).add('exampleapp.exe');
$exe.spurt('MZ');
# The one binary in a bundle that is not somebody else's software is
# still a downloaded one, and everything else downloaded is recorded
# with the URL it came from and the digest it was checked against.
# Without this entry the file a Windows user actually runs is the
# only thing in the bundle a reader cannot trace back to a release.
my %c = App::Ariza::Bundle.runner-component(%(
path => $exe,
artifact => 'ariza-runner-windows-x86_64.exe',
tag => 'runner-v1',
url => 'https://example.invalid/download/runner-v1/ariza-runner-windows-x86_64.exe',
sha256 => 'f' x 64,
), $dir);
is %c<artifact>, 'ariza-runner-windows-x86_64.exe', 'the published artefact';
is %c<tag>, 'runner-v1', 'the release it came from';
is %c<sha256>, 'f' x 64, 'the digest it was verified against';
ok %c<url>.ends-with('ariza-runner-windows-x86_64.exe'),
'the URL a reader can fetch it from themselves';
is %c<path>, 'bin/exampleapp.exe',
'and where it went, bundle-relative and forward-slashed';
# A platform with no runner, or a Windows build made before the
# first release was pinned: `stage` returns an empty hash, and the
# manifest simply has no runner entry rather than an empty one.
is App::Ariza::Bundle.runner-component(%(), $dir)<path>, '',
'nothing staged reports nothing rather than a half-filled row';
};