#!/usr/bin/env perl use v5.34; use warnings; use feature 'signatures'; no warnings ('experimental::signatures'); use Scalar::Util qw(looks_like_number); use Getopt::Std (); use JSON (); use File::Basename (); use File::Temp (); use File::Fetch (); use Digest::MD5 (); use Digest::SHA (); use MIME::Base64 (); sub usage($fh) { print $fh <<~'EOF' Usage: paku [-f FILE] ACTION paku [-f FILE] -C CONFIG_PATH paku -h EOF } sub help($fh) { print $fh <<~'EOF' Options: -f FILE use data from FILE (default: paku.lock) -C CONFIG get the final value of CONFIG_PATH -h, --help show this message ACTION what to emit, one of: - guix - debian - alpine - nix - html - guix-channel-key Generate package definitions for different package managers. Examples: Emit Guix package definitions using all defaults: $ paku guix > packages.scm $ guix build -f packages.scm Emit Debian build recipes from "debian.json", and execute them as is: $ paku -f debian.json debian | make -f- Find out what is the final value of "datadir": $ paku -C 'datadir' EOF } for (@ARGV) { last if $_ eq '--'; if ($_ eq '--help') { usage *STDOUT; help *STDOUT; exit; } } my %opts; if (!Getopt::Std::getopts('f:C:h', \%opts)) { usage *STDERR; exit 2; } if ($opts{h}) { usage *STDOUT; help *STDOUT; exit; } my $fname = $opts{f} || 'paku.lock'; sub load_json() { my $json_str = do { open my $fh, $fname or die "Failed opening \"$fname\""; local $/; <$fh>; }; return JSON::decode_json($json_str); } sub get_in($j, @l) { my ($head, @tail) = @l; if (!defined $head) { return ref $j ? JSON::encode_json $j : $j; } elsif (looks_like_number($head)) { return get_in($j->[$head], @tail); } else { return get_in($j->{$head}, @tail); } } if ($opts{C}) { say get_in(load_json(), split(/\./, $opts{C})); exit; } my $licenses = { guix => { 'AGPL-3.0-or-later' => 'agpl3+', }, nix => { 'AGPL-3.0-or-later' => 'agpl3Plus', }, }; sub license_for($target, $mappings, $id) { return $mappings->{$id} || $licenses->{$target}->{$id} || $id; } sub inputs_for($mappings, @inputs) { my @out = (); for (@inputs) { if (!$mappings->{$_}) { push @out, $_; } else { push @out, @{$mappings->{$_}}; } } return @out; } sub emit_packages() { for (@ARGV) { my $fh; my $size = (stat($_))[7]; open ($fh, '<', $_) or die "Can't open \"$_\": $!"; my $md5 = Digest::MD5->new->addfile($fh)->hexdigest; close $fh; open ($fh, '<', $_) or die "Can't open \"$_\": $!"; my $sha1 = Digest::SHA->new(1)->addfile($fh)->hexdigest; close $fh; open ($fh, '<', $_) or die "Can't open \"$_\": $!"; my $sha256 = Digest::SHA->new(256)->addfile($fh)->hexdigest; close $fh; open ($fh, '<', $_) or die "Can't open \"$_\": $!"; my $sha512 = Digest::SHA->new(512)->addfile($fh)->hexdigest; close $fh; print `ar -p $_ control.tar.xz | tar -xJO ./control`; say "Filename: ./$_"; say "Size: $size"; say "MD5sum: $md5"; say "SHA1: $sha1"; say "SHA256: $sha256"; print "\n"; } } sub emit_release() { my $f = $ARGV[0] or die 'Missing "Packages" file'; my $name = File::Basename::basename $f; my $size = (stat($f))[7]; my $now = `env LANG=POSIX.UTF-8 date -u '+%a, %d %b %Y %H:%M:%S +0000'`; chomp $now; my $fh; open ($fh, '<', $f) or die "Can't open \"$f\": $!"; my $md5 = Digest::MD5->new->addfile($fh)->hexdigest; close $fh; open ($fh, '<', $f) or die "Can't open \"$f\": $!"; my $sha1 = Digest::SHA->new(1)->addfile($fh)->hexdigest; close $fh; open ($fh, '<', $f) or die "Can't open \"$f\": $!"; my $sha256 = Digest::SHA->new(256)->addfile($fh)->hexdigest; close $fh; open ($fh, '<', $f) or die "Can't open \"$f\": $!"; my $sha512 = Digest::SHA->new(512)->addfile($fh)->hexdigest; close $fh; print <<~EOF; Date: $now MD5Sum: $md5 $size $name SHA1: $sha1 $size $name SHA256: $sha256 $size $name SHA512: $sha512 $size $name EOF exit; } sub emit_build_nix() { my $json = load_json(); my $ns = $json->{namespace}; $ns =~ s/\./-/g; print <<~EOF; { pkgs ? import { overlays = [ (import ./default.nix { inherit pkgs; }) ]; } }: map (name: pkgs.$ns."\${name}") (builtins.attrNames pkgs.$ns) EOF } sub emit_nix() { my $json = load_json(); my $ns = $json->{namespace}; $ns =~ s/\./-/g; print <<~EOF; { pkgs }: self: super: { $ns = rec { EOF my $license_mapping = $json->{mappings}{nix}{licenses}; for my $pkg (@{$json->{pkgs}}) { my $long = $pkg->{'long-description'}; $long =~ s/^(.)/ $1/gm; my $license = license_for 'nix', $license_mapping, $pkg->{license}; print <<~EOF; $pkg->{'full-name'} = pkgs.stdenv.mkDerivation rec { name = "$pkg->{name}"; version = "$pkg->{vlabel}"; src = fetchTarball { url = "$pkg->{url}"; sha256 = "$pkg->{sha256nix}"; }; nativeBuildInputs = with pkgs; [ EOF for my $input (@{$pkg->{'native-inputs'}}) { print <<~EOF; $input EOF } print <<~EOF; ]; buildInputs = with pkgs; [ EOF for my $input (@{$pkg->{inputs}}) { print <<~EOF; $input EOF } print <<~EOF; ]; makeFlags = [ "PREFIX=\$(out)" ]; doCheck = true; enableParallelBuilding = true; meta = with pkgs.lib; { description = "$pkg->{description}"; longDescription = '' $long ''; homepage = "$pkg->{homepage}"; changelog = "$pkg->{changelog}"; downloadPage = "$pkg->{'downloads-page'}"; license = licenses.$license; platforms = platforms.unix; }; }; EOF } print <<~EOF; }; } EOF } sub emit_guix() { my $json = load_json(); my $ns = $json->{namespace}; $ns =~ s/\./ /g; print <<~EOF; (define-module ($ns packages) EOF for my $module (@{$json->{guix}{'module-use'}}) { print <<~EOF; #:use-module (gnu packages $module) EOF } print <<~EOF; #:use-module ((guix licenses) #:prefix licenses:) #:use-module (guix gexp) #:use-module (guix packages) #:use-module (guix download) #:use-module (guix build-system gnu)) EOF my $pkg_mapping = $json->{mappings}{guix}{packages}; my $license_mapping = $json->{mappings}{guix}{licenses}; my @pkgs = (); for my $pkg (@{$json->{pkgs}}) { my $long = $pkg->{'long-description'}; $long =~ s/^(.)/ $1/gm; my $license = license_for 'guix', $license_mapping, $pkg->{license}; push @pkgs, $pkg->{'full-name'}; print <<~EOF; (define-public $pkg->{'full-name'} (package (name "$pkg->{name}") (version "$pkg->{vlabel}") (source (origin (method url-fetch) (uri "$pkg->{url}") (sha256 (base32 "$pkg->{sha256guix}")))) (build-system gnu-build-system) (native-inputs EOF print ' (list'; for (inputs_for $pkg_mapping, @{$pkg->{'native-inputs'}}) { print "\n $_"; } print "))\n"; print " (inputs\n"; print ' (list'; for (inputs_for $pkg_mapping, @{$pkg->{inputs}}) { print "\n $_"; } print "))\n"; print <<~EOF; (arguments (list #:make-flags #~(list (string-append "PREFIX=" #\$output)) #:phases #~(modify-phases %standard-phases (delete 'configure)))) (synopsis "$pkg->{description}") (description "$pkg->{'long-description'}") (home-page "$pkg->{homepage}") (license licenses:$license))) EOF } print '(list'; for (@pkgs) { print "\n $_"; } print ")\n"; } sub emit_guix_channel_key() { my $json = load_json(); my $name = $json->{maintainer}{name}; my $id = $json->{maintainer}{email}; print <<~EOF; .POSIX: $name.key: gpg --armour --export $id > \$\@ EOF } sub emit_debian() { my $json = load_json(); my @debs = (); my @targets = (); for my $pkg (@{$json->{pkgs}}) { push @targets, <<~EOF; \$(DIR)/debian/tarballs/$pkg->{fname}: mkdir -p \$(\@D) wget -O \$\@ \\ '$pkg->{url}' \$(DIR)/debian/builddirs/$pkg->{name}-$pkg->{vversion}: \$(DIR)/debian/tarballs/$pkg->{fname} mkdir -p \$(\@D) tar -C \$(\@D) -xf \$(DIR)/debian/tarballs/$pkg->{fname} touch \$\@ EOF next if $pkg->{architectures} ne 'any'; my $deb_name = "$pkg->{name}_" . ( $pkg->{label} eq 'latest' ? 'latest' : $pkg->{vversion} ) . "_all.deb"; my $deb_path = "\$(DIR)/debian/public/$deb_name"; push @debs, "\t$deb_path \\\n"; my $ver = $pkg->{label} eq 'latest' ? '0.' . $pkg->{vversion} . '.latest' : $pkg->{vversion}; $ver =~ s/^v//; push @targets, <<~EOF; \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}/DEBIAN: \$(DIR)/debian/builddirs/$pkg->{name}-$pkg->{vversion} \$(MAKE) \\ -C \$(DIR)/debian/builddirs/$pkg->{name}-$pkg->{vversion} \\ install \\ PREFIX=/usr \\ DESTDIR="\$\$PWD"/\$(\@D) mkdir -p \$\@ touch \$\@ \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}/DEBIAN/control: \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}/DEBIAN printf '' > \$\@ printf 'Package: $pkg->{name}\\n' >> \$\@ printf 'Version: $ver\\n' >> \$\@ printf 'Section: custom\\n' >> \$\@ printf 'Depends:\\n' >> \$\@ printf 'Priority: optional\\n' >> \$\@ printf 'Architecture: all\\n' >> \$\@ printf 'Essential: no\\n' >> \$\@ printf 'Maintainer: ' >> \$\@ printf '$pkg->{"maintainer-b64"}' | base64 -d >> \$\@ printf '\\n' >> \$\@ printf 'Description: ' >> \$\@ printf '$pkg->{"description-b64"}' | base64 -d >> \$\@ printf '\\n' >> \$\@ printf '$pkg->{'long-description-b64'}' | \\ base64 -d | \\ sed 's|^\$\$|.|' | \\ sed 's|^| |' >> \$\@ printf '\\n' >> \$\@ \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}.deb: \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}/DEBIAN/control dpkg-deb --build \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion} $deb_path: \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}.deb mkdir -p \$(\@D) cp \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}.deb \$\@ EOF } print <<~EOF; .POSIX: DIR = $json->{datadir} debs = \\ EOF print @debs, "\n"; print <<~EOF; GPGKEY = '$json->{maintainer}{name} <$json->{maintainer}{email}>' all: \\ \$(DIR)/debian/public/InRelease \$(DIR)/debian/public/Release.gpg \\ \$(DIR)/debian/public/public-key.asc debian.out.txt debian.out.txt: \@printf '\$(DIR)/debian/public/*\\n' > \$\@ \$(DIR)/debian/public/Packages: \$(debs) cd \$(\@D) && paku debian-packages *.deb > \$(\@F) \$(DIR)/debian/public/Release: \$(DIR)/debian/public/Packages paku debian-release \$(DIR)/debian/public/Packages > \$\@ \$(DIR)/debian/public/Release.gpg: \$(DIR)/debian/public/Release gpg -abs -o \$\@ \$(DIR)/debian/public/Release \$(DIR)/debian/public/InRelease: \$(DIR)/debian/public/Release gpg --default-key \$(GPGKEY) -a --clear-sign -o \$\@ \$(DIR)/debian/public/Release \$(DIR)/debian/public/public-key.asc: \$(debs) gpg --armour --export \$(GPGKEY) > \$\@ EOF print @targets; } sub emit_alpine() { my $d = $ARGV[0] or die 'Missing "src/alpine/" directory'; my $json = load_json(); for my $pkg (@{$json->{pkgs}}) { my @subpackages = (); if ($pkg->{manpages}) { push @subpackages, "$pkg->{name}-doc"; } if ($pkg->{i18n}) { push @subpackages, "$pkg->{name}-lang"; } my $date = $pkg->{date} =~ s/-//gr; my $ver = $pkg->{label} eq 'latest' ? "0.0.1_git$date" : $pkg->{vversion}; my $dir = "$d/$pkg->{'full-name'}"; mkdir "$dir"; open(my $fh, '>', "$dir/APKBUILD"); print $fh <<~EOF; # Maintainer: $pkg->{maintainer} pkgname='$pkg->{name}' pkgver='$ver' pkgrel='0' pkgdesc='$pkg->{description}' url='$pkg->{homepage}' arch='noarch' license='$pkg->{license}' depends='' makedepends='' checkdepends='' install='' subpackages='@subpackages' source='$pkg->{url}' pkgbasedir="\$startdir/.abuild/pkg" srcdir="\$startdir/.abuild/src" builddir="\$srcdir/$pkg->{name}-$pkg->{vversion}" build() { make PREFIX=/usr } check() { make check } package() { make install DESTDIR="\$pkgdir" PREFIX=/usr } cleanup_srcdir() { rm -rf "\$startdir/.abuild" } sha512sums=' $pkg->{sha512} $pkg->{fname} ' EOF close $fh; } my @apks = (); my @targets = (); for my $pkg (@{$json->{pkgs}}) { next if $pkg->{architectures} ne 'any'; my $date = $pkg->{date} =~ s/-//gr; my $ver = $pkg->{label} eq 'latest' ? "0.0.1_git$date" : $pkg->{vversion}; my $recipe_dir = "src/alpine/$pkg->{'full-name'}/"; my $apk_name = "$pkg->{name}-$ver-r0.apk"; my $apk_path = "\$(DIR)/alpine/REPODEST/alpine/x86_64/$apk_name"; push @apks, "\t$apk_path \\\n"; push @targets, <<~EOF; $apk_path: $recipe_dir/APKBUILD cd $recipe_dir && abuild \\ -s \$\$OLDPWD/\$(DIR)/alpine/SRCDEST \\ -P \$\$OLDPWD/\$(DIR)/alpine/REPODEST \\ sanitycheck clean fetch verify unpack build check rootpkg index clean EOF } print <<~EOF; .POSIX: DIR = $json->{datadir} RSAKEY = $json->{maintainer}{email} apks = \\ EOF print @apks, "\n"; print "\n\n"; print <<~EOF; all: \$(apks) \$(DIR)/alpine/REPODEST/alpine/\$(RSAKEY).rsa.pub alpine.out.txt alpine.out.txt: \@printf '\$(DIR)/alpine/REPODEST/alpine/*\\n' > \$\@ \$(apks): src/secrets/\$(RSAKEY).rsa \$(DIR)/alpine/REPODEST/alpine/\$(RSAKEY).rsa.pub: src/secrets/\$(RSAKEY).rsa mkdir -p \$(\@D) cp src/secrets/\$(RSAKEY).rsa.pub \$\@ src/secrets/\$(RSAKEY).rsa: src/secrets/\$(RSAKEY).rsa.gpg gpg -d -o \$\@ \$\@.gpg chmod 400 \$\@ EOF print @targets; } sub pascal_case($s) { return join('', map(ucfirst, split '-', $s)) } sub emit_homebrew() { my $d = $ARGV[0] or die 'Missing "Formula/" directory'; my $json = load_json(); for my $pkg (@{$json->{pkgs}}) { open(my $fh, '>', "$d/$pkg->{'full-name'}.rb"); my $class_name = pascal_case $pkg->{'full-name'}; print $fh <<~EOF; class $class_name < Formula desc '$pkg->{description}' homepage '$pkg->{homepage}' url '$pkg->{url}' sha256 '$pkg->{sha256}' license 'AGPL-3.0-or-later' def install system 'make', "PREFIX=#{prefix}" system 'make', 'check', "PREFIX=#{prefix}" system 'make', 'install', "PREFIX=#{prefix}" end test do system "#{bin}/$pkg->{name}", '-V' end end EOF close $fh; } } sub emit_html() { my $json = load_json(); print <<~EOF; $json->{name} package index

$json->{name} package index

Guix instructions

Add this channel to your ~/.config/guix/channels.scm:

(cons*
		 (channel
		  (name '$channel_name)
		  (url "$json->{vcs}->{git}")
		  (branch "main")
		  (introduction
		   (make-channel-introduction
		    "d749e053e6db365069cb9b2ef47a78b06f9e7361"
		    (openpgp-fingerprint
		     "5BDA E9B8 B2F6 C6BC BB0D  6CE5 81F9 0EC3 CD35 6060"))))
		 %default-channels)

Afterwards, do a guix pull to make the packages in this channel available to your profile.

See also the Guix manual on channels for more information.

Debian instructions

Include my public key for validating the repository signatures:

\$ wget -qO- $json->{'base-url'}/debian/public-key.asc |
		    sudo tee /etc/apt/trusted.gpg.d/$json->{namespace}.asc

Afterwards, include this repository to the list of repositories that apt(8) uses for sources by adding its URL to /etc/apt/sources.list:

\$ echo 'deb $json->{'base-url'}/debian ./' |
		    sudo tee -a /etc/apt/sources.list
		\$ sudo apt update

After that the packages from this repository will be available.

Alpine instructions

Get my public key used to sign the repository:

\$ wget -qO- $json->{'base-url'}/alpine/$json->{maintainer}{email}.rsa.pub |
		    doas tee /etc/apk/keys/$json->{maintainer}{email}.rsa.pub

Then include this repository in the apk-repositories(5) list that apk(8) uses to retrive package files for installation:

\$ echo '$json->{'base-url'}/alpine' |
		    doas tee -a /etc/apk/repositories

After that the packages frmo this repository will be available.

Nix instructions

Add this repository as an overlay to your /etc/nixos/configuration.nix:

  nixpkgs = {
		    overlays = [
		      (import (fetchTarball {
		        url = "$json->{vcs}->{tarball}";
		      }) { inherit pkgs; })
		    ];
		  };

All the packages live under the $overlay_name attribute set, like. $overlay_name.$json->{pkgs}[0]->{name}, so it can be included in the list of packages:

  environment.systemPackages = with pkgs; [
		    ...
		    $overlay_name.$json->{pkgs}[0]->{name}
		  ];

To make the overlay available outside the system environment of /etc/nixos/configuration.nix, you need to add this overlay to your ~/.config/nixpkgs/overlays/ directory, like a ~/.config/nixpkgs/overlays/$json->{namespace}.nix file containing:

import (fetchTarball {
		  url = "$json->{vcs}->{tarball}";
		}) { pkgs = import <nixpkgs> {}; }

Then you'd be able to launch a shell with a package from this overlay:

\$ nix-shell -p $overlay_name.$json->{pkgs}[0]->{name}

Homebrew instructions

Add this repository as a tap:

\$ brew tap --force-auto-update $tap_name $json->{vcs}->{http}

The explicit --force-auto-update option is required, because homebrew(1) will only fetch updates automatically for repositories hosted on GitHub. With this option, repositories not on GitHub are treated equally.

EOF } sub run_template($name, $version, $template) { return ($template =~ s/\@name\@/$name/gr) =~ s/\@version\@/$version/gr; } sub set_difference($s1, $s2) { my %idx = (); for (@{$s1}) { $idx{$_} = 1; } for (@{$s2}) { delete $idx{$_}; } return keys(%idx); } sub emit_refresh() { my $json_str = do { local $/; ; }; my $json = JSON::decode_json($json_str); my $defaults = { datadir => '.paku', guix => {}, mappings => {}, maintainer => { name => '', email => '', }, namespace => '', name => '', 'base-url' => '', vcs => { git => '', http => '', tarball => '', }, pkgs => [], }; my $out = { %$defaults, %$json, }; my $default_package = { maintainer => $out->{defaults}{maintainer} || '', 'exclude-tags' => [], }; for my $package_any (@{$out->{packages}{any}}) { my $package = ref $package_any ? { %$default_package, %$package_any } : { %$default_package, name => $package_any }; my $repo_url = run_template($package->{name}, '', $out->{defaults}{templates}{repository}); my $dir = File::Temp::newdir(); `git clone '$repo_url' '$dir'`; die if $?; my @versions = (); my $tags_any = $package->{tags} || $out->{defaults}{versions}{tags}; my @tags = (); if (ref $tags_any) { push @tags, @$tags_any; } elsif ($tags_any eq 'all') { my @ret = `git -C '$dir' tag`; die if $?; chomp @ret; push @tags, set_difference(\@ret, $package->{'exclude-tags'}); } else { die "Unknown value of \"tags\": $tags_any"; } for (@tags) { my $ver = $_ =~ s/^v//gr; my %version = ( type => 'tag', vversion => $_, version => $ver, vname => $_, vlabel => $ver, label => $ver =~ s/\./-/gr, ); push @versions, \%version; } my $default_branch = $package->{default_branch} || $out->{defaults}{default_branch} || 'main'; my $branches = $package->{branches} || $out->{defaults}{versions}{branches}; for (@{$branches}) { my $revision = `git -C '$dir' rev-parse '$_'`; die if $?; chomp $revision; my $label = $_ eq $default_branch ? 'latest' : $_; my %version = ( type => 'branch', vversion => $revision, version => $_, vname => $label, vlabel => $revision, label => $label, ); push @versions, \%version; } for my $version (@versions) { my $pkg = { %$version, name => $package->{name}, architectures => 'any', }; my $full_name = $pkg->{name} . ( $pkg->{label} eq 'latest' ? '' : "-$pkg->{label}" ); $pkg->{'full-name'} = $full_name; my $revision = `git -C '$dir' rev-parse '$pkg->{vversion}'`; die if $?; chomp $revision; $pkg->{revision} = $revision; `git -C '$dir' checkout '$pkg->{vversion}'`; die if $?; my $url = run_template($package->{name}, $pkg->{vversion}, $out->{defaults}{templates}{tarball}); $pkg->{url} = $url; my $date = `git -C '$dir' log -1 --format=%cs`; die if $?; chomp $date; $pkg->{date} = $date; $pkg->{manpages} = -d "$dir/doc/" ? JSON::true : JSON::false; $pkg->{i18n} = -d "$dir/po/" ? JSON::true : JSON::false; for (qw(description long-description)) { my $s = do { local(@ARGV, $/) = "$dir/$_"; <> }; chomp $s; $pkg->{$_} = $package->{$_} || $s; } my @sha_guix = `guix download '$url'`; die if $?; chomp @sha_guix; $pkg->{sha256guix} = $sha_guix[-1]; my $sha_nix = `nix-prefetch-url --unpack '$url'`; die if $?; chomp $sha_nix; $pkg->{sha256nix} = $sha_nix; my $ff = File::Fetch->new(uri => $url); my $where = $ff->fetch(to => '/tmp'); my $fh; open ($fh, '<', $where) or die "Can't open \"$where\": $!"; my $sha256 = Digest::SHA->new(256)->addfile($fh)->hexdigest; close $fh; $pkg->{sha256} = $sha256; open ($fh, '<', $where) or die "Can't open \"$where\": $!"; my $sha512 = Digest::SHA->new(512)->addfile($fh)->hexdigest; close $fh; $pkg->{sha512} = $sha512; unlink $where; for (qw(base-url homepage changelog downloads-page fname)) { $pkg->{$_} = $package->{$_} || run_template( $pkg->{name}, $pkg->{vversion}, $out->{defaults}{templates}{$_} ); } for (qw(inputs native-inputs)) { $pkg->{$_} = $package->{$_} || []; } for (qw(maintainer license)) { $pkg->{$_} = $package->{$_} || $out->{defaults}{$_}; } for (qw(maintainer description long-description)) { $pkg->{"$_-b64"} = MIME::Base64::encode_base64($pkg->{$_}, ''); } push @{$out->{pkgs}}, $pkg; } } print JSON->new->pretty->canonical->encode($out); } my %actions = ( 'debian-packages' => \&emit_packages, 'debian-release' => \&emit_release, 'build-nix' => \&emit_build_nix, nix => \&emit_nix, guix => \&emit_guix, 'guix-channel-key' => \&emit_guix_channel_key, debian => \&emit_debian, alpine => \&emit_alpine, homebrew => \&emit_homebrew, html => \&emit_html, refresh => \&emit_refresh, ); my $action = $ARGV[0] or die "Missing ACTION"; shift; my $fn = $actions{$action} or die "Unknown ACTION: \"$action\""; &$fn;