#!/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;
$json->{name} package index
$json->{name} package index
EOF
for my $pkg (@{$json->{packages}}) {
my $suffix = $pkg->{label} eq 'latest' ? '' : "-$pkg->{label}";
my $apt_suffix = $suffix =~ s/-/=/r;
print <<~EOF;
-
$pkg->{name}
($pkg->{label}) - $pkg->{description}
Homepage
Guix
After following the
Guix instructions
to include this channel, you can launch a shell
that includes this package:
\$ guix shell $pkg->{name}$suffix
Alternatively, you can install it imperatively:
\$ guix install $pkg->{name}$suffix
Debian
After following the
Debian instructions
to include this repository to
/etc/apt/sources.list
, you can
install it:
# apt install $pkg->{name}$apt_suffix
Nix
After following the
Nix instructions
to include this repository as an overlay, you
can launch a shell that includes this package:
\$ nix-shell -p $pkg->{name}$suffix
Alternatively, you can install it imperatively:
\$ nix-env -i $pkg->{name}$suffix
EOF
}
print <<~EOF;
Guix instructions
Add this channel to your
~/.config/guix/channels.scm
:
(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)
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:
~/.config/guix/channels.scm
:
\$ wget -qO- https://euandre.org/s/package-repository/debian/public-key.asc | sudo tee /etc/apt/trusted.gpg.d/euandre.org.asc
Afterwards, include this repository to the list of
repositories that apt
uses for sources
by adding its URL to
/etc/apt/sources.list
:
\$ sudo apt-add-repository 'deb https://euandre.org/s/package-repository/debian ./'
apt-add-repository
will already perform
an apt update
, so the packages from the
new repository will already be available.
Nix instructions
Add this repository as an overlay to your
/etc/nixos/configuration.nix
:
nixpkgs = {
overlays = [
(import (fetchTarball {
url = "https://euandre.org/git/package-repository/snapshot/package-repository-main.tar.gz";
}) { inherit pkgs; })
];
};
All the packages live under the
org-euandre
attribute set.
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;