diff options
Diffstat (limited to 'src/bin/paku.in')
-rwxr-xr-x | src/bin/paku.in | 726 |
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 <nixpkgs> {}; }</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; |