#!/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 - guix-channel-key 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_guix_channel_key() { my $json = load_json(); my $name = $json->{maintainer}{name}; my $id = $json->{maintainer}{email}; print <<~EOF; .POSIX: $name.key: gpg --armour --export $id > \$\@ EOF } 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 ".POSIX:\n\n"; print "DIR = $dir\n\n"; print @debs, "\n"; print <<~EOF; GPGKEY = '$json->{maintainer}{name} <$json->{maintainer}{email}>' 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

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:

\$ echo 'deb $json->{'base-url'}/debian ./' |
		    sudo tee -a /etc/apt/sources.list
		\$ sudo apt update

After that the packages from the new repository will 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, 'guix-channel-key' => \&emit_guix_channel_key, debian => \&emit_debian, homebrew => \&emit_homebrew, html => \&emit_html, ); my $fn = $actions{$action} or die "Unknown ACTION: \"$action\""; &$fn;