App-Ariza.git | lib/App/Ariza/ | Platform.rakumod
unit module App::Ariza::Platform;
# Deliberately identical to Notcurses-Native's %PLATFORM-SLUGS (see that
# distribution's Build.rakumod). A bundle carries Notcurses-Native's
# prebuilt shared libraries, so the slug ariza builds under has to be the
# exact string that distribution's binary fetcher will look for later —
# any divergence here produces a bundle whose native libs silently fail to
# resolve on the target machine.
#
# Both `win32-` and `mswin32-` keys are present because `$*KERNEL.name`
# has historically reported either on Windows depending on the build.
#
# Typed `Str` so a missing-key lookup yields the `Str` type object rather
# than `Any`, which keeps `detect-slug`'s `--> Str` return constraint
# satisfiable and lets callers write `without $slug { ... }`.
my Str %PLATFORM-SLUGS =
'darwin-arm64' => 'macos-arm64',
'darwin-x86_64' => 'macos-x86_64',
'linux-x86_64-glibc' => 'linux-x86_64-glibc',
'linux-x86_64-musl' => 'linux-x86_64-musl',
'linux-aarch64-glibc' => 'linux-aarch64-glibc',
'linux-aarch64-musl' => 'linux-aarch64-musl',
'win32-x86_64' => 'windows-x86_64',
'win32-aarch64' => 'windows-arm64',
'mswin32-x86_64' => 'windows-x86_64',
'mswin32-aarch64' => 'windows-arm64',
;
#| The name of the environment variable that overrides platform
#| detection. Exported as a constant so tests and callers never spell it
#| as a bare string literal.
our constant PLATFORM-ENV = 'ARIZA_PLATFORM';
#| Every platform slug ariza knows how to name, sorted and deduplicated.
#| Two keys map to `windows-x86_64` and two to `windows-arm64`, so this is
#| shorter than the key map.
our sub known-slugs(--> List) is export {
%PLATFORM-SLUGS.values.unique.sort.List
}
#| Every C<os-hardware[-libc]> key the slug map recognises, sorted. Useful
#| in diagnostics: it says which probe results ariza can name, which is a
#| different question from which slugs exist.
our sub known-platform-keys(--> List) is export {
%PLATFORM-SLUGS.keys.sort.List
}
#| True if C<$slug> is one of C<known-slugs>.
our sub known-slug(Str $slug --> Bool) is export {
$slug.defined && so %PLATFORM-SLUGS.values.first($slug)
}
#| The system glibc version from `ldd --version`, or an undefined
#| `Version` when `ldd` is absent, exits non-zero (musl's `ldd` does), or
#| prints something unparseable. Only meaningful on Linux.
our sub detect-glibc-version(--> Version) is export {
my $proc = try { run 'ldd', '--version', :out, :err };
return Version without $proc;
my $out = $proc.out.slurp(:close);
$proc.err.slurp(:close);
return Version unless $proc.exitcode == 0;
my $first = $out.lines.head // '';
$first ~~ /(\d+ '.' \d+ [ '.' \d+ ]?) \s* $/
?? Version.new(~$0)
!! Version
}
#| Detect the system C library. Probe order:
#|
#| 1. musl loader presence — a `ld-musl-*.so.1` file under `/lib` or
#| `/usr/lib` is unambiguous.
#| 2. glibc, if `ldd --version` runs and reports a version.
#|
#| Returns the `Str` type object off Linux (there is no libc axis to
#| report), and also on a Linux system with neither loader visible —
#| uclibc, a statically-linked busybox image — which produces a platform
#| key the slug map has no entry for, and so an honest "unknown platform"
#| rather than a wrong guess.
#|
#| `:lib-dirs` and `:glibc-probe` exist so tests can drive every branch
#| without a container: the first replaces the directories searched for a
#| musl loader, the second replaces the `ldd` call with any callable
#| returning a `Version` or an undefined value.
our sub detect-libc(
Str :$os = $*KERNEL.name.lc,
:@lib-dirs = </lib /usr/lib>,
:&glibc-probe = &detect-glibc-version,
--> Str
) is export {
return Str unless $os eq 'linux';
for @lib-dirs -> $d {
next unless $d.IO.d;
return 'musl' if try {
$d.IO.dir.first(*.basename.starts-with('ld-musl-'))
};
}
return 'glibc' if glibc-probe().defined;
Str
}
#| The C<os-hardware[-libc]> key that detection looks up. Linux keys carry
#| a libc suffix because glibc and musl need different binaries; every
#| other OS has no libc axis to disambiguate, so its key is just
#| C<os-hardware>.
our sub platform-key(
Str :$os = $*KERNEL.name.lc,
Str :$hardware = $*KERNEL.hardware.lc,
Str :$libc = detect-libc(:$os),
--> Str
) is export {
$libc ?? "$os-$hardware-$libc" !! "$os-$hardware"
}
#| The platform slug for this machine, or the `Str` type object when the
#| probes produce a combination ariza has no slug for. Never dies — this
#| is the "ask politely" form, for callers that want to report an unknown
#| platform in their own words.
#|
#| The `ARIZA_PLATFORM` override still applies, and is still validated: an
#| override is the one input that is definitely a human mistake if it is
#| wrong, so a bad one dies even here.
our sub detect-slug(
Str :$os = $*KERNEL.name.lc,
Str :$hardware = $*KERNEL.hardware.lc,
Str :$libc = detect-libc(:$os),
Str :$override = %*ENV{PLATFORM-ENV} // Str,
--> Str
) is export {
with $override {
my $trimmed = .trim;
if $trimmed {
die "ariza: {PLATFORM-ENV}='$trimmed' is not a platform ariza knows"
~ " (expected one of: {known-slugs.join(', ')})"
unless known-slug($trimmed);
return $trimmed;
}
}
%PLATFORM-SLUGS{platform-key(:$os, :$hardware, :$libc)} // Str
}
#| The platform slug for this machine, dying with a diagnostic naming the
#| probe result and the supported set if there isn't one. This is the form
#| commands should use: a bundler that cannot name its own platform has
#| nothing useful left to do.
our sub current-slug(
Str :$os = $*KERNEL.name.lc,
Str :$hardware = $*KERNEL.hardware.lc,
Str :$libc = detect-libc(:$os),
Str :$override = %*ENV{PLATFORM-ENV} // Str,
--> Str
) is export {
my $slug = detect-slug(:$os, :$hardware, :$libc, :$override);
return $slug if $slug.defined;
my $key = platform-key(:$os, :$hardware, :$libc);
die "ariza: unsupported platform '$key'"
~ " (ariza knows: {known-platform-keys.join(', ')});"
~ " set {PLATFORM-ENV} to force one of: {known-slugs.join(', ')}";
}
=begin pod
=head1 NAME
App::Ariza::Platform - name the machine ariza is building for
=head1 SYNOPSIS
=begin code :lang<raku>
use App::Ariza::Platform;
say current-slug; # macos-arm64
say known-slugs; # (linux-aarch64-glibc linux-aarch64-musl ...)
# Non-fatal form: Str type object instead of a die
without detect-slug() -> $ {
say "no bundle for this box";
}
# Force a slug (cross-builds, CI matrices, tests):
%*ENV<ARIZA_PLATFORM> = 'linux-x86_64-musl';
say current-slug; # linux-x86_64-musl
# Inject probe results instead of touching the real system:
say detect-slug(:os<linux>, :hardware<aarch64>, :libc<musl>, :override(Str));
# linux-aarch64-musl
=end code
=head1 DESCRIPTION
A self-contained bundle is only self-contained for I<one> platform, so
every artefact ariza produces is named after one. This module produces
that name and nothing else: it has no state, touches no configuration,
and its only side effects are reading C<$*KERNEL>, C<%*ENV>, C</lib>,
and (on Linux) running C<ldd --version>.
=head2 The slugs
=item1 C<macos-arm64>, C<macos-x86_64>
=item1 C<linux-x86_64-glibc>, C<linux-aarch64-glibc>
=item1 C<linux-x86_64-musl>, C<linux-aarch64-musl>
=item1 C<windows-x86_64>, C<windows-arm64>
These strings are B<not> ariza's to choose. They are exactly
Notcurses-Native's platform slugs, because a bundle ships that
distribution's prebuilt notcurses libraries and has to agree with it
about what platform it is on. If Notcurses-Native ever gains a slug,
this map gains it in the same shape.
=head2 Detection
The probe builds a key from C<$*KERNEL.name.lc> and
C<$*KERNEL.hardware.lc>, plus — on Linux only — a libc suffix, then
looks that key up:
=begin code :lang<console>
darwin-arm64 -> macos-arm64
linux-x86_64-glibc -> linux-x86_64-glibc
linux-aarch64-musl -> linux-aarch64-musl
mswin32-x86_64 -> windows-x86_64
=end code
Windows reports its kernel name as either C<win32> or C<mswin32>
depending on the build, so both keys are mapped.
The libc axis exists because a glibc binary does not run on Alpine and
a musl binary does not run on Debian. C<detect-libc> looks for a
C<ld-musl-*.so.1> loader under C</lib> or C</usr/lib> first (its
presence is conclusive), then falls back to parsing C<ldd --version>
for a glibc version. A Linux system with neither — uclibc, a static
busybox image — yields no libc, hence no matching key, hence an honest
"unsupported platform" rather than a bundle that will not run.
=head2 The ARIZA_PLATFORM override
Setting C<ARIZA_PLATFORM> to a known slug short-circuits all detection.
This is what makes cross-building and CI matrices possible, and what
lets this distribution's own tests exercise Linux and Windows paths on
a Mac.
An override that is I<not> a known slug is a hard error, in both
C<current-slug> and C<detect-slug>. Every other input to this module is
a system fact that might legitimately be something ariza cannot name;
an override is a human typing a string, and silently ignoring a
misspelled one would produce a bundle for the wrong platform. An empty
or whitespace-only value is treated as unset, so C<ARIZA_PLATFORM=>
in a shell profile does not wedge the tool.
=head1 SUBROUTINES
=head2 current-slug(:$os, :$hardware, :$libc, :$override --> Str)
The slug for this machine. Dies if there isn't one, with a message
naming the probed key, every key ariza recognises, and the override
that would force the issue.
=head2 detect-slug(:$os, :$hardware, :$libc, :$override --> Str)
Same, but returns the undefined C<Str> type object instead of dying on
an unknown platform. A bad C<ARIZA_PLATFORM> still dies.
=head2 detect-libc(:$os, :@lib-dirs, :&glibc-probe --> Str)
C<'musl'>, C<'glibc'>, or the undefined C<Str>. Off Linux, always the
undefined C<Str>. C<:@lib-dirs> and C<:&glibc-probe> replace the two
system probes, so every branch is reachable from a test on any host.
=head2 detect-glibc-version(--> Version)
The version C<ldd --version> reports, or an undefined C<Version>.
=head2 platform-key(:$os, :$hardware, :$libc --> Str)
The C<os-hardware[-libc]> string detection looks up. Exposed for
diagnostics.
=head2 known-slugs(--> List) / known-platform-keys(--> List) / known-slug(Str --> Bool)
The supported slugs, the recognised probe keys, and a membership test.
=head1 AUTHOR
Matt Doughty
=head1 COPYRIGHT AND LICENSE
Copyright 2026 Matt Doughty
This library is free software; you can redistribute it and/or modify it
under the Artistic License 2.0.
=end pod