#!/usr/bin/env perl
use v5.34;
use warnings;
use feature 'signatures';
no warnings ('experimental::signatures');
use Getopt::Std ();
use JSON ();
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 $action = $ARGV[0] or die "Missing ACTION";
shift;
sub load_json() {
my $json_str = do {
open my $fh, $fname or die "Failed opening \"$fname\"";
local $/;
<$fh>;
};
return JSON::decode_json($json_str);
}
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_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->{packages}}) {
my $long = $pkg->{'long-description'};
$long =~ s/^(.)/ $1/gm;
my $license = license_for 'nix', $license_mapping, $pkg->{license};
my $suffix = $pkg->{label} eq 'latest' ? '' : "-$pkg->{label}";
print <<~EOF;
$pkg->{name}$suffix = pkgs.stdenv.mkDerivation rec {
name = "$pkg->{name}";
version = "$pkg->{version}";
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->{packages}}) {
my $long = $pkg->{'long-description'};
$long =~ s/^(.)/ $1/gm;
my $ver = $pkg->{version} =~ s/^v//r;
my $license = license_for 'guix', $license_mapping, $pkg->{license};
my $name = $pkg->{name} . (
$pkg->{label} eq 'latest' ? '' : "-$pkg->{label}"
);
push @pkgs, $name;
print <<~EOF;
(define-public $name
(package
(name "$pkg->{name}")
(version "$ver")
(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_debian() {
my $json = load_json();
my @debs = ("debs = \\\n");
my @targets = ();
for my $pkg (@{$json->{packages}}) {
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 \$(\@D) -xf \$(DIR)/tarballs/$pkg->{fname}
touch \$\@
EOF
next if $pkg->{architectures} ne 'any';
push @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//;
push @targets, <<~EOF;
\$(DIR)/builddirs/$pkg->{name}-$pkg->{version}/DEBIAN: \$(DIR)/checkouts/$pkg->{name}-$pkg->{version}
\$(MAKE) \\
-C \$(DIR)/checkouts/$pkg->{name}-$pkg->{version} \\
install \\
PREFIX=/usr \\
DESTDIR="\$\$PWD"/\$(\@D)
mkdir -p \$\@
touch \$\@
\$(DIR)/builddirs/$pkg->{name}-$pkg->{version}/DEBIAN/control: \$(DIR)/builddirs/$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 '$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)/builddirs/$pkg->{name}-$pkg->{version}.deb: \$(DIR)/builddirs/$pkg->{name}-$pkg->{version}/DEBIAN/control
dpkg-deb --build \$(DIR)/builddirs/$pkg->{name}-$pkg->{version}
\$(DIR)/debian/$pkg->{name}_$pkg->{version}_all.deb: \$(DIR)/builddirs/$pkg->{name}-$pkg->{version}.deb
mkdir -p \$(\@D)
cp \$(DIR)/builddirs/$pkg->{name}-$pkg->{version}.deb \$\@
EOF
}
print "DIR = $dir\n\n";
print @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) && paku debian-packages *.deb > \$(\@F)
\$(DIR)/debian/Release: \$(DIR)/debian/Packages
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: \$(debs)
gpg --armour --export \$(GPGKEY) > \$\@
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->{packages}}) {
my $name = $pkg->{name} . (
$pkg->{label} eq 'latest' ? '' : "-$pkg->{label}"
);
open(my $fh, '>', "$d/$name.rb");
my $class_name = pascal_case $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'
system 'make', 'check'
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
EOF
for my $pkg (@{$json->{packages}}) {
my $guix_suffix = $pkg->{label} eq 'latest' ? '' : '@' . $pkg->{label} =~ s/-/./gr;
my $suffix = $pkg->{label} eq 'latest' ? '' : "-$pkg->{label}";
my $apt_suffix = $suffix =~ s/-/=/r;
my $ver = $pkg->{label} eq 'latest' ? $pkg->{label} : $pkg->{version};
print <<~EOF;
$pkg->{name}
($ver) - $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}$guix_suffix
Alternatively, you can install it imperatively:
\$ guix install $pkg->{name}$guix_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
Homebrew
After following the
Homebrew instructions
to include this repository as a tap
, you can
install it with:
\$ brew install $pkg->{name}$suffix
EOF
}
my $channel_name = $json->{namespace} =~ s/\./-/gr;
my $overlay_name = $json->{namespace} =~ s/\./-/gr;
my $tap_name = $json->{namespace} =~ s/\./\//gr;
print <<~EOF;
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
uses for sources
by adding its URL to
/etc/apt/sources.list
:
\$ sudo apt-add-repository 'deb $json->{'base-url'}/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 = "$json->{vcs}->{tarball}";
}) { inherit pkgs; })
];
};
All the packages live under the
$overlay_name
attribute set, like.
$overlay_name.$json->{packages}[0]->{name}
,
so it can be included in the list of packages:
environment.systemPackages = with pkgs; [
...
$overlay_name.$json->{packages}[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->{packages}[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
}
my %actions = (
'debian-packages' => \&emit_packages,
'debian-release' => \&emit_release,
nix => \&emit_nix,
guix => \&emit_guix,
debian => \&emit_debian,
homebrew => \&emit_homebrew,
html => \&emit_html,
);
my $fn = $actions{$action} or die "Unknown ACTION: \"$action\"";
&$fn;