#!/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 <nixpkgs> { 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; <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" type="image/svg+html" href="favicon.svg" /> <title>$json->{name} package index</title> <style> body { max-width: 800px; margin: 0 auto; } ul { list-style: none; } li { margin-top: 2em; } pre { overflow: auto; white-space: pre; margin: 5px; padding: 5px; border-radius: 5px; } pre, code { background-color: #ddd; } \@media(prefers-color-scheme: dark) { :root { color: white; background-color: black; } a { color: hsl(211, 100%, 60%); } a:visited { color: hsl(242, 100%, 80%); } pre, code { background-color: #222; } } </style> </head> <body> <main> <h1> $json->{name} package index </h1> <ul> EOF for my $pkg (@{$json->{pkgs}}) { my $guix_suffix = $pkg->{label} eq 'latest' ? '' : "\@$pkg->{version}"; my $apt_suffix = $pkg->{label} eq 'latest' ? '' : "=$pkg->{label}"; my $id = $pkg->{name} . ( $pkg->{label} eq 'latest' ? '' : "-$pkg->{vversion}" ); print <<~EOF; <li id="$id"> <details> <summary> <a href="#$id">$pkg->{name}</a> ($pkg->{vname}) - $pkg->{description} </summary> <p> <a href="$pkg->{homepage}">Homepage</a> </p> <section> <h2>Guix</h2> <p> After following the <a href="#guix-instructions">Guix instructions</a> to include this channel, you can launch a shell that includes this package: </p> <pre><code>\$ guix shell $pkg->{name}$guix_suffix</code></pre> <p> Alternatively, you can install it imperatively: </p> <pre><code>\$ guix install $pkg->{name}$guix_suffix</code></pre> </section> <section> <h2>Debian</h2> <p> After following the <a href="#debian-instructions">Debian instructions</a> to include this repository to <code>/etc/apt/sources.list</code>, you can install it: </p> <pre><code># apt install $pkg->{name}$apt_suffix</code></pre> </section> <section> <h2>Alpine</h2> <p> After folowwing the <a href="#alpine-instructions">Alpine instructions</a> to include this repository to <code>/etc/apk/repositories</code>, you can install it: </p> <pre><code># apk add $pkg->{name}</code></pre> </section> <section> <h2>Nix</h2> <p> After following the <a href="#nix-instructions">Nix instructions</a> to include this repository as an overlay, you can launch a shell that includes this package: </p> <pre><code>\$ nix-shell -p $pkg->{'full-name'}</code></pre> <p> Alternatively, you can install it imperatively: </p> <pre><code>\$ nix-env -i $pkg->{'full-name'}</code></pre> </section> <section> <h2>Homebrew</h2> <p> After following the <a href="#homebrew-instructions">Homebrew instructions</a> to include this repository as a <code>tap</code>, you can install it with: </p> <pre><code>\$ brew install $pkg->{'full-name'}</code></pre> </section> </details> </li> EOF } my $channel_name = $json->{namespace} =~ s/\./-/gr; my $overlay_name = $json->{namespace} =~ s/\./-/gr; my $tap_name = $json->{namespace} =~ s/\./\//gr; print <<~EOF; </ul> <article id="guix-instructions"> <h2>Guix instructions</h2> <p> Add this channel to your <code>~/.config/guix/channels.scm</code>: </p> <pre><code>(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)</code></pre> <p> Afterwards, do a <code>guix pull</code> to make the packages in this channel available to your profile. </p> <p> See also the <a href="https://guix.gnu.org/manual/en/guix.html#Channels">Guix manual on channels</a> for more information. </p> </article> <article id="debian-instructions"> <h2>Debian instructions</h2> <p> Include my public key for validating the repository signatures: </p> <pre><code>\$ wget -qO- $json->{'base-url'}/debian/public-key.asc | sudo tee /etc/apt/trusted.gpg.d/$json->{namespace}.asc</code></pre> <p> Afterwards, include this repository to the list of repositories that <code>apt(8)</code> uses for sources by adding its URL to <code>/etc/apt/sources.list</code>: </p> <pre><code>\$ echo 'deb $json->{'base-url'}/debian ./' | sudo tee -a /etc/apt/sources.list \$ sudo apt update</code></pre> <p> After that the packages from this repository will be available. </p> </article> <article id="alpine-instructions"> <h2>Alpine instructions</h2> <p> Get my public key used to sign the repository: </p> <pre><code>\$ wget -qO- $json->{'base-url'}/alpine/$json->{maintainer}{email}.rsa.pub | doas tee /etc/apk/keys/$json->{maintainer}{email}.rsa.pub</code></pre> <p> Then include this repository in the <code>apk-repositories(5)</code> list that <code>apk(8)</code> uses to retrive package files for installation: </p> <pre><code>\$ echo '$json->{'base-url'}/alpine' | doas tee -a /etc/apk/repositories</code></pre> <p> After that the packages frmo this repository will be available. </p> </article> <article id="nix-instructions"> <h2>Nix instructions</h2> <p> Add this repository as an overlay to your <code>/etc/nixos/configuration.nix</code>: </p> <pre><code> nixpkgs = { overlays = [ (import (fetchTarball { url = "$json->{vcs}->{tarball}"; }) { inherit pkgs; }) ]; };</code></pre> <p> All the packages live under the <code>$overlay_name</code> attribute set, like. <code>$overlay_name.$json->{pkgs}[0]->{name}</code>, so it can be included in the list of packages: </p> <pre><code> environment.systemPackages = with pkgs; [ ... $overlay_name.$json->{pkgs}[0]->{name} ];</pre></code> <p> To make the overlay available outside the system environment of <code>/etc/nixos/configuration.nix</code>, you need to add this overlay to your <code>~/.config/nixpkgs/overlays/</code> directory, like a <code>~/.config/nixpkgs/overlays/$json->{namespace}.nix</code> file containing: </p> <pre><code>import (fetchTarball { url = "$json->{vcs}->{tarball}"; }) { pkgs = import <nixpkgs> {}; }</pre></code> <p> Then you'd be able to launch a shell with a package from this overlay: </p> <pre><code>\$ nix-shell -p $overlay_name.$json->{pkgs}[0]->{name}</code></pre> </article> <article id="homebrew-instructions"> <h2>Homebrew instructions</h2> <p> Add this repository as a tap: </p> <pre><code>\$ brew tap --force-auto-update $tap_name $json->{vcs}->{http}</code></pre> <p> The explicit <code>--force-auto-update</code> option is required, because <code>homebrew(1)</code> will only fetch updates automatically for repositories hosted on GitHub. With this option, repositories not on GitHub are treated equally. </p> </article> </main> </body> </html> 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 $/; <STDIN>; }; 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;