#!/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;