Tools

NAME

App::Ariza::Tools - the shell-out layer: run, fetch, digest, extract

SYNOPSIS


use App::Ariza::Tools;

# Processes
my ($code, $out, $err) = try-run(<otool -L /bin/ls>);        # never dies
my $text = run-checked(<git rev-parse HEAD>, :what<git>);    # dies on failure
my %env  = child-env(:PERL6LIB(Str), :NOTCURSES_NATIVE_DATA_DIR($dir));

# Network
my $json = http-get('https://rakudo.org/dl/rakudo');
http-download($url, $cache.add('rakudo.tar.gz'));

# Files
ensure-dir($bundle.add('native'));
my $sha = sha256-file($archive);              # lowercase hex, 64 chars
extract-archive($archive, $staging);
my $top = sole-child($staging);               # the one wrapper directory
copy-writable($src, $dest);
rm-rf($staging);

DESCRIPTION

Building a bundle is mostly driving other programs: curl, tar, zef, otool, install_name_tool, codesign. This module is the one place that knows how to do that safely, so no other module in ariza contains a bare run and every failure reads the same way.

There are no new distribution dependencies here on purpose. The Build.rakumod of every native-wrapping distribution these apps depend on already downloads with curl and unpacks with tar; ariza fetches three artefacts from static hosts, and pulling in a TLS stack to do it would be a much larger surface than the feature justifies.

RUNNING PROCESSES

try-run(@cmd, :%env, :$cwd --> List)

(exitcode, stdout, stderr). Never throws. A command that cannot be spawned at all — no such binary — comes back as exit code -1 with the reason on stderr, so callers need one error path, not two.

run-checked(@cmd, :%env, :$cwd, :$what --> Str)

stdout, or a die naming :$what (defaulting to the command), the exit code, and the child's stderr indented beneath. Captured stderr that nobody prints is the reason build tools are hated; this one prints it.

have-command(Str, :&run --> Bool)

Whether a command can be spawned. Used to choose between interchangeable tools — sha256sum or shasum — and to say "install patchelf" before a build gets half way through, never to skip work. :&run replaces the probe, so a test can assert on the "there is no patchelf" branch without uninstalling one.

child-env(*%overrides --> Hash)

%*ENV plus %overrides, with any key whose override is undefined removed.

Removal matters more than it looks. Setting PERL6LIB="" does not unset it: Rakudo sees a set-but-empty variable and prints a v6.e deprecation warning across the entire child run, which is exactly what happens when you try to keep a stray PERL6LIB out of a zef invocation the obvious way. child-env(:PERL6LIB(Str)) deletes the key.

NETWORK

http-get(Str $url --> Str)

The response body as text, via curl --fail or wget. Dies on any HTTP error, which is what --fail buys over a 404 page silently becoming your JSON.

http-download(Str $url, IO() $dest --> IO::Path)

Download to $dest. The transfer lands in a .part sibling and is renamed into place only on success, so an interrupted download cannot be picked up as a cache hit by the next run. A partial file is removed on failure.

FILES

sha256-file(IO(), :&run --> Str)

Lowercase hex digest. sha256sum, shasum -a 256 and certutil -hashfile are tried in that order until one of them returns 64 hex characters.

Every kind of failure falls through to the next tool: absent from PATH, unspawnable, non-zero exit, or output that is not a digest. Only an exhausted list dies, and it names what each attempt did. That is not defensiveness for its own sake — it is what makes Windows work. A GitHub runner has a shasum on PATH that is a Perl script with no interpreter association, so where finds it and CreateProcess refuses it; stopping there took down every digest on the machine while certutil, which Windows has always had, went untried.

certutil is last for the opposite reason: Linux distributions ship an unrelated NSS tool under that name, and being last means it is only reached when nothing else answered, where its refusal is simply one more failed attempt.

There is no "skip verification" fallback. Every digest here is either gating a cache reuse or being written into a manifest a user may check, and both are worse than useless if they can silently be absent.

An empty file never reaches any of the three tools: its digest is a constant, and both certutil (which cannot map a zero-byte file — ERROR_FILE_INVALID, not a transient) and sha256sum (which, given a backslash-separated Windows path, switches to GNU's "escaped output" format and fuses a leading backslash onto the digest) mishandle it in ways worth not asking them about.

extract-archive(IO() $archive, IO() $into --> IO::Path)

Unpack a .tar.gz, .tgz or .zip. Zip prefers unzip and falls back to tar -xf (bsdtar reads zip; GNU tar does not), so both a Linux box with unzip and a Windows box with only tar work.

sole-child(IO() $dir --> IO::Path)

The single entry inside a directory, for archives that wrap everything in one top-level folder. Dies naming what it found otherwise, rather than guessing at the first alphabetically.

ensure-dir(IO() --> IO::Path) / rm-rf(IO()) / copy-writable(IO(), IO() --> IO::Path)

Create a directory tree; remove one; copy a file and make the copy writable.

rm-rf unlinks symlinks rather than descending through them: a bundle contains symlinked libraries, and following one out of the tree during a cleanup is how a delete becomes an incident. A missing path is not an error, so it is safe in LEAVE.

copy-writable exists because Homebrew ships libraries mode 444. A bundler that preserves that mode cannot run install_name_tool over its own copy, and the failure surfaces several steps later as an inexplicable "permission denied".

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.

App::Ariza v0.2.4

bundler and distribution tool for Raku terminal apps

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

Config::TOML:ver<0.1.3+>:auth<zef:raku-community-modules>JSON::Fast:ver<0.19+>:auth<cpan:TIMOTIMO>Template::Jinja2:ver<0.3.0+>:auth<zef:apogee>

Test Dependencies

Provides

  • App::Ariza
  • App::Ariza::Bundle
  • App::Ariza::CI
  • App::Ariza::Config
  • App::Ariza::Installer
  • App::Ariza::Launcher
  • App::Ariza::Licensing
  • App::Ariza::Native
  • App::Ariza::Platform
  • App::Ariza::Rakudo
  • App::Ariza::Resources
  • App::Ariza::Runner
  • App::Ariza::Site
  • App::Ariza::Smoke
  • App::Ariza::Tools
  • App::Ariza::Update
  • App::Ariza::Versions

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.