#!/usr/bin/env perl
use v5.34;
use warnings;
use feature 'signatures';
no warnings ('experimental::signatures');
use Getopt::Std ();
use JSON ();
use MIME::Base64 ();
use File::Basename ();
use Digest::MD5 ();
use Digest::SHA ();
sub usage($fh) {
print $fh <<~'EOF'
Usage:
paku [-d DIR] [-f FILE] ACTION
paku -h
EOF
}
sub help($fh) {
print $fh <<~'EOF'
Options:
-d DIR use DIR for intermediary data (default: .paku/)
-f FILE use data from FILE (default: paku.lock)
-h, --help show this message
ACTION what to emit, one of:
- guix
- debian
- nix
- html
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", using
".debtmp/" as the temporary directory, and execute them
as is:
$ paku -d .debtemp/ -f debian.json debian | make -f-
EOF
}
for (@ARGV) {
last if $_ eq '--';
if ($_ eq '--help') {
usage *STDOUT;
help *STDOUT;
exit;
}
}
my %opts;
if (!Getopt::Std::getopts('d:f:h', \%opts)) {
usage *STDERR;
exit 2;
}
if ($opts{h}) {
usage *STDOUT;
help *STDOUT;
exit;
}
my $dir = $opts{d} || '.paku';
my $fname = $opts{f} || 'paku.lock';
my $json_str = do {
open my $fh, $fname or die "Failed opening \"$fname\"";
local $/;
<$fh>;
};
my $json = JSON::decode_json($json_str);
my $action = $ARGV[0] or die "Missing ACTION";
sub emit_release() {
my $f = $ARGV[1];
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_nix() {
my $ns = $json->{namespace};
$ns =~ s/\./-/g;
print <<~EOF;
{ pkgs }:
self: super: {
$ns = rec {
EOF
for my $pkg (@{$json->{packages}}) {
if ($pkg->{label} eq 'latest') {
print <<~EOF;
$pkg->{name} = $pkg->{name}-latest;
EOF
}
my $long = $pkg->{'long-description'};
$long =~ s/^(.)/ $1/gm;
print <<~EOF;
$pkg->{name}-$pkg->{label} = pkgs.stdenv.mkDerivation rec {
name = "$pkg->{name}";
version = "$pkg->{version}";
src = fetchTarball {
url =
"$pkg->{url}";
sha256 = "$pkg->{sha256}";
};
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->{'base-url'}/";
changelog = "$pkg->{'base-url'}/CHANGELOG.html";
downloadPage = "$pkg->{'base-url'}/#releases";
license = licenses.agpl3;
platforms = platforms.unix;
};
};
EOF
}
print <<~EOF;
};
}
EOF
}
sub emit_guix() {
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 @pkgs = ();
for my $pkg (@{$json->{packages}}) {
if ($pkg->{label} eq 'latest') {
push @pkgs, "$pkg->{name}";
}
my $long = $pkg->{'long-description'};
$long =~ s/^(.)/ $1/gm;
push @pkgs, "$pkg->{name}-$pkg->{label}";
print <<~EOF;
(define-public $pkg->{name}-$pkg->{label}
(package
(name "$pkg->{name}-$pkg->{label}")
(version "$pkg->{version}")
(source
(origin
(method url-fetch)
(uri "$pkg->{url}")
(sha256
(base32 "$pkg->{sha256base32}"))))
(build-system gnu-build-system)
(native-inputs
EOF
print ' (list';
for my $input (@{$pkg->{'native-inputs'}}) {
my $name = $json->{mappings}{$input}{guix} || $input;
print "\n $name";
}
print "))\n";
print " (inputs\n";
print ' (list';
for my $input (@{$pkg->{inputs}}) {
my $name = $json->{mappings}{$input}{guix} || $input;
print "\n $name";
}
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->{'base-url'}/")
(license licenses:agpl3+)))
EOF
if ($pkg->{type} eq 'latest') {
print <<~EOF;
(define-public $pkg->{name}
(package
(inherit $pkg->{name}-latest)
(name "$pkg->{name}")))
EOF
}
}
print '(list';
for (@pkgs) {
print "\n $_";
}
print ")\n";
}
sub emit_debian() {
my %vars = (
tarballs => ["tarballs = \\\n"],
checkouts => ["checkouts = \\\n"],
destdirs => ["debian-destdirs = \\\n"],
ctrlfiles => ["control-files = \\\n"],
destdebs => ["destdir-debs = \\\n"],
debs => ["debs = \\\n"],
);
my @targets = ();
for my $pkg (@{$json->{packages}}) {
push @{$vars{tarballs}}, "\t\$(DIR)/tarballs/$pkg->{fname} \\\n";
push @{$vars{checkouts}}, "\t\$(DIR)/checkouts/$pkg->{name}-$pkg->{version} \\\n";
push @targets, <<~EOF;
\$(DIR)/tarballs/$pkg->{fname}:
mkdir -p \$(\@D)
wget -O \$\@ \\
'$pkg->{url}'
\$(DIR)/checkouts/$pkg->{name}-$pkg->{version}: \$(DIR)/tarballs/$pkg->{fname}
mkdir -p \$(\@D)
tar -C \$(DIR)/checkouts/ -xf \$(DIR)/tarballs/$pkg->{fname}
touch \$\@
EOF
next if $pkg->{architectures} ne 'any';
push @{$vars{destdirs}}, "\t\$(DIR)/debian-destdir/$pkg->{name}-$pkg->{version}/DEBIAN \\\n";
push @{$vars{ctrlfiles}}, "\t\$(DIR)/debian-destdir/$pkg->{name}-$pkg->{version}/DEBIAN/control \\\n";
push @{$vars{destdebs}}, "\t\$(DIR)/debian-destdir/$pkg->{name}-$pkg->{version}.deb \\\n";
push @{$vars{debs}}, "\t\$(DIR)/debian/$pkg->{name}_$pkg->{version}_all.deb \\\n";
my $ver = $pkg->{label} eq 'latest' ? '0.' . $pkg->{version} . '.latest' : $pkg->{version};
$ver =~ s/^v//;
my $maintainer_b64 = MIME::Base64::encode_base64 $pkg->{maintainer}, '';
my $desc_b64 = MIME::Base64::encode_base64 $pkg->{description}, '';
my $long_desc_b64 = MIME::Base64::encode_base64 $pkg->{'long-description'}, '';
push @targets, <<~EOF;
\$(DIR)/debian-destdir/$pkg->{name}-$pkg->{version}/DEBIAN: \$(DIR)/checkouts/$pkg->{name}-$pkg->{version}
\$(MAKE) \\
-C \$(DIR)/checkouts/$pkg->{name}-$pkg->{version} \\
install \\
PREFIX=/usr \\
DESTDIR="\$\$PWD"/\$(DIR)/debian-destdir/$pkg->{name}-$pkg->{version}
mkdir -p \$\@
touch \$\@
\$(DIR)/debian-destdir/$pkg->{name}-$pkg->{version}/DEBIAN/control: \$(DIR)/debian-destdir/$pkg->{name}-$pkg->{version}/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 '$maintainer_b64' | base64 -d >> \$\@
printf '\\n' >> \$\@
printf 'Description: ' >> \$\@
printf '$desc_b64' | base64 -d >> \$\@
printf '\\n' >> \$\@
printf '$long_desc_b64' | \\
base64 -d | \\
sed 's|^\$\$|.|' | \\
sed 's|^| |' >> \$\@
printf '\\n' >> \$\@
\$(DIR)/debian-destdir/$pkg->{name}-$pkg->{version}.deb: \$(DIR)/debian-destdir/$pkg->{name}-$pkg->{version}/DEBIAN/control
dpkg-deb --build \$(DIR)/debian-destdir/$pkg->{name}-$pkg->{version}
\$(DIR)/debian/$pkg->{name}_$pkg->{version}_all.deb: \$(DIR)/debian-destdir/$pkg->{name}-$pkg->{version}.deb
mkdir -p \$(\@D)
cp \$(DIR)/debian-destdir/$pkg->{name}-$pkg->{version}.deb \$\@
EOF
}
print "DIR = $dir\n\n";
print
@{$vars{tarballs}}, "\n",
@{$vars{checkouts}}, "\n",
@{$vars{destdirs}}, "\n",
@{$vars{ctrlfiles}}, "\n",
@{$vars{destdebs}}, "\n",
@{$vars{debs}}, "\n";
print <<~EOF;
GPGKEY = '$json->{maintainer}'
all: \$(DIR)/debian/InRelease \$(DIR)/debian/Release.gpg \$(DIR)/debian/public-key.asc
public-dir:
\@printf '\$(DIR)/debian'
\$(DIR)/debian/Packages: \$(debs)
cd \$(\@D) && dpkg-scanpackages -m . > \$(\@F)
\$(DIR)/debian/Release: \$(DIR)/debian/Packages
perl src/bin/paku debian-release \$(DIR)/debian/Packages > \$\@
\$(DIR)/debian/Release.gpg: \$(DIR)/debian/Release
gpg -abs -o \$\@ \$(DIR)/debian/Release
\$(DIR)/debian/InRelease: \$(DIR)/debian/Release
gpg --default-key \$(GPGKEY) -a --clear-sign -o \$\@ \$(DIR)/debian/Release
\$(DIR)/debian/public-key.asc: \$(DIR)/debian/Release
gpg --armour --export \$(GPGKEY) > \$\@
EOF
print @targets;
}
sub emit_html() {
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;
}
</style>
</head>
<body>
<main>
<h1>
$json->{name} package index
</h1>
<ul>
EOF
for my $pkg (@{$json->{packages}}) {
my $suffix = $pkg->{label} eq 'latest' ? '' : "-$pkg->{label}";
my $apt_suffix = $suffix =~ s/-/=/r;
print <<~EOF;
<li id="$pkg->{name}-$pkg->{label}">
<details>
<summary>
<a href="#$pkg->{name}-$pkg->{label}">$pkg->{name}</a>
($pkg->{label}) - $pkg->{description}
</summary>
<p>
<a href="$pkg->{'base-url'}/">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}$suffix</code></pre>
<p>
Alternatively, you can install it imperatively:
</p>
<pre><code>\$ guix install $pkg->{name}$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>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->{name}$suffix</code></pre>
<p>
Alternatively, you can install it imperatively:
</p>
<pre><code>\$ nix-env -i $pkg->{name}$suffix</code></pre>
</section>
</details>
</li>
EOF
}
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 'org-euandre)
(url "git://euandre.org/package-repository")
(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:
<code>~/.config/guix/channels.scm</code>:
</p>
<pre><code>\$ wget -qO- https://euandre.org/s/package-repository/debian/public-key.asc | sudo tee /etc/apt/trusted.gpg.d/euandre.org.asc</code></pre>
<p>
Afterwards, include this repository to the list of
repositories that <code>apt</code> uses for sources
by adding its URL to
<code>/etc/apt/sources.list</code>:
</p>
<pre><code>\$ sudo apt-add-repository 'deb https://euandre.org/s/package-repository/debian ./'</code></pre>
<p>
<code>apt-add-repository</code> will already perform
an <code>apt update</code>, so the packages from the
new repository will already 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 = "https://euandre.org/git/package-repository/snapshot/package-repository-main.tar.gz";
}) { inherit pkgs; })
];
};</code></pre>
<p>
All the packages live under the
<code>org-euandre</code> attribute set.
</p>
</article>
</main>
</body>
</html>
EOF
}
my %actions = (
'debian-release' => \&emit_release,
nix => \&emit_nix,
guix => \&emit_guix,
debian => \&emit_debian,
html => \&emit_html,
);
my $fn = $actions{$action} or die "Unknown ACTION: \"$action\"";
&$fn;