Licensing
NAME
App::Ariza::Licensing - what a bundle redistributes, and under what terms
SYNOPSIS
use App::Ariza::Licensing;
my %l = App::Ariza::Licensing.write(
:bundle-dir($work),
:config($cfg),
:app-dir($app),
:app-version<0.2.0>,
:app-display<Moneymoor>,
:platform<macos-arm64>,
:placeholders(%( 'rakudo-version' => '2026.07',
'rakudo-tag' => '2026.07-01' )),
:conditions(<sqlcipher>),
);
say %l<summary><rows>; # 41
say %l<summary><spdx-ids>; # (Apache-2.0 Artistic-2.0 BSD-2-Clause ā¦)
.note for %l<warnings>;
# Just the identifiers in an expression:
say spdx-ids('GPL-3.0-or-later WITH GCC-exception-3.1');
# (GPL-3.0 GCC-exception-3.1)
DESCRIPTION
A bundle is a binary redistribution of other people's software: a
vendored Rakudo, the C libraries inside its MoarVM, every Raku
distribution in the closure, a native pack or two, SQLCipher where an app
asks for it, and ā on Windows ā a compiled launcher. This module
collects all of that into one THIRD-PARTY.md at the bundle root and
one LICENSES/ directory beside it, and refuses to produce a bundle
whose contents it cannot account for.
Data, never code
ariza bundles anybody's Raku application, so it cannot hold a table of who wrote what. Every fact in the merged document comes from one of four places, and none of them is a Raku source file:
A native pack's own licensing kit ā
third-party.jsonwhere the pack ships one, its generatedTHIRD-PARTY.mdwhere it does not. The pack knows what is in the pack.resources/runtime-third-party.jsonā ariza's own maintained record of the vendored runtime, MoarVM's vendored C libraries, SQLCipher and the Windows runner. Those components have nowhere else to speak from: they arrive as compiled bytes inside an archive with no manifest.Each installed distribution's
META6.jsonā thelicensefield, read out of the bundle's own site repository and the one inside the vendored runtime, which is wherezeflives.The app's
ariza.tomlā its own row, and rows for anything it ships that ariza cannot see: a font, a dataset, an asset with a licence of its own.
The 0.2.0 "recipe" work adds a fifth contributor without changing any of this: a recipe describes a native dependency, and a native dependency's licensing is rows in exactly the shape above.
What fails, and what warns
The rule is that silence is never an option, and that the difference between a warning and a failure is whether ariza has anything true to say instead.
A native pack with no licensing kit at all is a row saying exactly that, and a warning on the build. It is not dropped, because a redistributed binary nobody attributed is the thing this document exists to make visible.
licensing.strict = truein the app'sariza.tomlturns it into a failed build.A pack whose
THIRD-PARTY.mdis not in the generated shape falls back to the same unattributed row rather than to rows assembled out of the wrong columns.A distribution with no
licensefield, or with one ariza has no text for, fails the build ā naming every offender in the closure at once, not the first one, because an app whose closure has three of them should learn that in one build rather than three. There is no unknown row for a Raku module: the field exists, filling it in is a one-line change, and an app that has hit a distribution which has not can say so once with[[licensing.dists]].A cited licence text that is nowhere to be found fails the build naming the identifier, what ariza ships, and how to supply one.
NOASSERTIONis a declaration, not a gap, and only an app can make it ā in a[[licensing.dists]]or[[licensing.third-party]]row, after looking and failing to find an answer. No licence text is looked up for it (there is none), the row carries a generated sentence saying so and pointing at the component's own repository, and the manifest counts those rows separately from the identifier set, so a gate never mistakes one for a permissive licence. A distribution whose own metadata saysNOASSERTIONfails like any other missing licence: nobody has looked yet. An application that declares it about itself fails outright ā there is nobody to look on its behalf. Andlicensing.strictrefuses the lot, because strict means every component in the bundle names a licence and "we could not find one" is not one.Two sources offering the same licence text with different bytes warn, naming both, and keep the higher-priority copy ā the app's first, then a native pack's, then ariza's own template last ā so the document is the same on every machine and a real notice is never replaced by a generic one.
The row
Every collector produces the same shape, which is what lets four sources that know nothing about each other end up in one table:
%(
id => 'notcurses/ffmpeg', name => 'FFmpeg', version => '8.1.2',
kind => 'native', # application runtime native module other
spdx => 'LGPL-2.1-or-later', conveyed => '',
copyright => 'Copyright (c) 2000-2026 the FFmpeg developers',
url => 'https://ffmpeg.org/', source => 'https://ā¦/n8.1.2.tar.gz',
notes => '', license-files => ('LGPL-2.1.txt',), files => ('libavcodec.*',),
provenance => 'pack manifest (native/ā¦/lib/third-party.json)',
)
provenance is ariza's own addition to the pack format, and it is the
field a reader checks first: it says which of the four sources this row's
facts came from, so "who claims this?" has an answer that is not "the
tool".
Ordering
Rows are sorted by kind, then by name folded, then by id ā never by the order a directory happened to be read in. Two builds of the same inputs produce byte-identical documents, which is what makes the output diffable and the golden test meaningful.
METHODS
write(:$bundle-dir!, :$config!, :$app-dir!, :$app-version!, :$app-display!, :$platform!, :%placeholders, :@conditions, :%files, :$data-path --> Hash)
The whole job: collect, resolve every cited licence text, write
LICENSES/ and THIRD-PARTY.md, and return
{ rows, warnings, summary, document, dir }.
:@conditions are the facts the runtime data file's condition keys
are tested against (sqlcipher, runner); :%placeholders the
values substituted into its {...} tokens; :%files the file names a
component turned out to cover, keyed by component id.
runtime-rows(:%placeholders, :@conditions, :%files, :$path --> List) / runtime-components(:$path --> List) / runtime-claims(:$path, :@conditions --> List)
The rows from resources/runtime-third-party.json, the validated
components behind them, and the native/ subdirectory names those
components account for.
pack-rows(:$bundle-dir!, :$family!, :$native-dir, :@claimed, :$strict, :@warnings --> Hash)
{ rows, texts } for every native pack staged into the bundle:
rows from each pack's own kit, and the LICENSES/ directories those
kits carry.
site-rows(:$bundle-dir!, :$app-name, :@overrides, :%texts! --> List)
One row per installed distribution, from both of the bundle's
repositories. Fails closed on a missing or unusable license field.
app-rows(:$config!, :$app-dir!, :$app-version!, :%texts!, :$license-file --> List)
The application's own row and its [[licensing.third-party]] rows.
md-components(Str $md --> List) / manifest-rows($manifest, :$family!, :$pack!, :$provenance! --> List)
The two readers for a native pack's kit: the JSON manifest, and the
generated document as a fallback. md-components returns the empty
list for anything that is not in the exact generated shape.
find-kits(IO::Path $root, :$depth --> List) / kit-dir(IO() $dir --> Bool)
Where the licensing kits under a staged pack are, and what counts as one.
text-pool(:@extra-dirs, :@extra-files, :@warnings --> Hash)
< filename => IO::Path > for every licence text this build can cite,
in priority order: the app's, then the packs', then ariza's own
templates as the fallback.
render(:@rows!, :$app-display!, :$app-version!, :$platform! --> Str)
The merged document as text, with nothing in it that varies between two builds of the same inputs.
merge(@rows --> List)
Sort into the document's order, and refuse two rows that claim the same id.
spdx-ids(Str $expr --> List) / spdx-text-name(Str $id --> Str) / spdx-not-asserted(Str $expr --> Bool)
The identifiers in an SPDX expression, normalised; the file name one is
looked up as; and whether an expression is exactly NOASSERTION. The
last is deliberately a whole-value test ā "MIT OR NOASSERTION" is not
a licence anyone can act on, so it falls through to the ordinary path
and fails for having no text.
SEE ALSO
App::Ariza::Bundle, which calls this once per build and records its
summary in ariza-manifest.json; App::Ariza::Config for the
[licensing] table an app writes.
AUTHOR
Matt Doughty
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.