#!/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;