aboutsummaryrefslogtreecommitdiff
path: root/src/bin/paku.in
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/paku.in')
-rwxr-xr-xsrc/bin/paku.in726
1 files changed, 726 insertions, 0 deletions
diff --git a/src/bin/paku.in b/src/bin/paku.in
new file mode 100755
index 0000000..9305ec9
--- /dev/null
+++ b/src/bin/paku.in
@@ -0,0 +1,726 @@
+#!/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, $id) {
+ return $licenses->{$target}->{$id};
+}
+
+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
+ for my $pkg (@{$json->{packages}}) {
+ my $long = $pkg->{'long-description'};
+ $long =~ s/^(.)/ $1/gm;
+ my $license = license_for 'nix', $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 @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', $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 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->{homepage}")
+ (license licenses:$license)))
+
+ EOF
+ }
+ print '(list';
+ for (@pkgs) {
+ print "\n $_";
+ }
+ print ")\n";
+}
+
+sub emit_debian() {
+ my $json = load_json();
+ 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//;
+
+ 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 '$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-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) && 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: \$(DIR)/debian/Release
+ 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;
+ <!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 $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;
+ <li id="$pkg->{name}-$ver">
+ <details>
+ <summary>
+ <a href="#$pkg->{name}-$ver">$pkg->{name}</a>
+ ($ver) - $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>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>
+ <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->{name}$suffix</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</code> uses for sources
+ by adding its URL to
+ <code>/etc/apt/sources.list</code>:
+ </p>
+ <pre><code>\$ sudo apt-add-repository 'deb $json->{'base-url'}/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 = "$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->{packages}[0]->{name}</code>,
+ so it can be included in the list of packages:
+ </p>
+ <pre><code> environment.systemPackages = with pkgs; [
+ ...
+ $overlay_name.$json->{packages}[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 &lt;nixpkgs&gt; {}; }</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->{packages}[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
+}
+
+
+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;