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

$json->{name} package index

Guix instructions

Add this channel to your ~/.config/guix/channels.scm:

(cons*
		 (channel
		  (name 'org-euandre)
		  (url "git://euandre.org/package-repository")
		  (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: ~/.config/guix/channels.scm:

\$ wget -qO- https://euandre.org/s/package-repository/debian/public-key.asc | sudo tee /etc/apt/trusted.gpg.d/euandre.org.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 https://euandre.org/s/package-repository/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 = "https://euandre.org/git/package-repository/snapshot/package-repository-main.tar.gz";
		      }) { inherit pkgs; })
		    ];
		  };

All the packages live under the org-euandre attribute set.

EOF } 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;