#!/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}}) {
my $ver = $pkg->{version};
$ver =~ s/^v//;
$ver =~ s/\./-/g;
if ($pkg->{type} eq 'latest') {
$ver = 'latest';
print <<~EOF;
$pkg->{name} = $pkg->{name}-latest;
EOF
}
my $long = $pkg->{'long-description'};
$long =~ s/^(.)/ $1/gm;
print <<~EOF;
$pkg->{name}-$ver = 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}}) {
my $ver = $pkg->{version};
$ver =~ s/^v//;
$ver =~ s/\./-/g;
if ($pkg->{type} eq 'latest') {
push @pkgs, "$pkg->{name}";
$ver = 'latest';
}
my $long = $pkg->{'long-description'};
$long =~ s/^(.)/ $1/gm;
push @pkgs, "$pkg->{name}-$ver";
print <<~EOF;
(define-public $pkg->{name}-$ver
(package
(name "$pkg->{name}-$ver")
(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->{type} 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() {
}
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;