aboutsummaryrefslogblamecommitdiff
path: root/src/bin/paku.in
blob: 71f29c233e428780887a25fa4613c36de5f19f33 (plain) (tree)
1
2
3
4
5
6
7
8
9

                   



                                         
                                       
                   
 
            
 
                      


                   

                   
                    



                          

                                               


                         
 




                          
                                                                       
                                                                  

                                                 


                                                     
                                        

                                      
                                                  
 

                                                                            
 
 

                         



                                                                   
 
 

                                                                           
 





                                                                
                   

 







                              
 
 
         
                                             

                      

 



                      

 
 
                                    
 









                                                                       




















                                                       







                                                   

                                                                      

 











                                                      





























                                                                            
 
                    
                                                          



































                                                                                

 













                                                                                               
                
                               

                                    




                              
                                                               
                                        

                                                      
                                                                                   
 
                             
                                                                        
                                            
                                                 



                                          
                                                     



                                                      
 


                                                            
                           






                                                   
                                     
                                      
                           
                 












                                                            


                                                                  
                                                    



                                                   




                     

 
                 
                               

                                    
                     

                                             




                                                         
                     
                                                           






                                               

                                                                
                      
                                        

                                                      
                                                                                    
 
                                                
                             
                                                  
                          
                                         
                                              




                                           
                                                          




                                                   
                                                                           
                                            

                             
 

                                      
                                                                  
                                            

                             
 


                              
                                                                                





                                                        
                                                  
                                                 

                   





                              

 











                                                        
                   
                               
                         
                         
 
                                        
                                      
                                                              


                                                     
 
                                                                                                                     
                                                
                                                                                        
                                          
 
                           
 
 
                                                       
 

                                                  
                                                                               
                               
                                                                 

                                               
 
                                                                                                             
                               

                                      
                                                                                                                                            
                                           
                                                                                                    

                                                      
                                                                 


                                             
                                                                                                                                                          
                                                








                                                                                      
                                                                                      
                                                                                      
 
                                                                                      
                                                                                      
                                                                                      
 
                                                                            

                                                             

                                                                                      
 

                                                                                                                                                       
 
                                                                                            
                                                
                                                                                                 
 
 

                           
 
 






                                      
                          
                     
                                                                                 
 
 
                       

                                                                                            
 
                               
                                                                    
 
 
                                                        
                                                                          
 

                                                                                 
 

                                                                                
 

                                                                                                          
 
                                                              
                                                               
 
 
                   


                       
                   
                                                                  
                               
                                        







                                                               
                                                   
                                                                                          
                                                   














                                                        
                                                  
                                            

                                                           
                                                                         












                                                                           



                                                           





                                                     


                         
                                        

                                                       
                                                   
                                                                                          
 
                                                                   

                                                                                 
 
                                               

                                      




                                                                                                             








                                      

                                                   



                         
                          

                     
                                                                                              
 

                                                                             
 
                                                    
 


                                                                                             
 


                                                                          

                   
                       

 






                                                               
                                        

                                                                 


                                                    
                                                     




                                                     

                                                                        











                                                                        
                 
                               





















                                                                                          






























                                                           









                                                   
                                        

                                                                                       
                                         
                                                                            
                  
                             
                                     

                                     
                                                             
                                                                   

                                      
                                                                   








                                                                                  
                                                                                            


                                                                               
                                                                                              












                                                                                           










                                                                                      






                                                                                
                                                                                         


                                                                               
                                                                                       
                                      







                                                                                          
                                                                                         
                                      



                                    
 


                                                            









                                                                   

                                             





















                                                                                                                 
                            

                                                                                           

                                                                            
                                                                                


                                                             


                                                                                
                           
                                                                           
                                       

                                



















                                                                                                               








                                                                    
                                                        




                                                         
                                                                         
                                                                              
                                                                        
                            

                                                                              
                                                          

















                                                                                        
                                                                                                      
                                




                                                          
                                                                                                              







                                                                                 



                           


 






































































































                                                                                                           
                                                   












                                                         




                                                                                 






                                                                                    
                                                                                                                         







































































                                                                                               
               

                                              
                                               



                                                      
                                            

                                              
                                             

  

                                              

                                                                
#!/usr/bin/env perl

use v5.34;
use warnings;
use feature 'signatures';
no warnings ('experimental::signatures');
use Scalar::Util qw(looks_like_number);
use Getopt::Std ();

use JSON ();

use File::Basename ();
use File::Temp ();
use File::Fetch ();

use Digest::MD5 ();
use Digest::SHA ();
use MIME::Base64 ();

sub usage($fh) {
	print $fh <<~'EOF'
		Usage:
		  paku [-f FILE] ACTION
		  paku [-f FILE] -C CONFIG_PATH
		  paku -h
		EOF
}

sub help($fh) {
	print $fh <<~'EOF'


		Options:
		  -f FILE       use data from FILE (default: paku.lock)
		  -C CONFIG     get the final value of CONFIG_PATH
		  -h, --help    show this message

		  ACTION        what to emit, one of:
		                - guix
		                - debian
		                - alpine
		                - 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", and execute
		  them as is:

		    $ paku -f debian.json debian | make -f-


		  Find out what is the final value of "datadir":

		    $ paku -C 'datadir'
		EOF
}


for (@ARGV) {
	last if $_ eq '--';
	if ($_ eq '--help') {
		usage *STDOUT;
		help *STDOUT;
		exit;
	}
}

my %opts;
if (!Getopt::Std::getopts('f:C:h', \%opts)) {
	usage *STDERR;
	exit 2;
}

if ($opts{h}) {
	usage *STDOUT;
	help *STDOUT;
	exit;
}


my $fname = $opts{f} || 'paku.lock';


sub load_json() {
	my $json_str = do {
		open my $fh, $fname or die "Failed opening \"$fname\"";
		local $/;
		<$fh>;
	};
	return JSON::decode_json($json_str);
}



sub get_in($j, @l) {
	my ($head, @tail) = @l;
	if (!defined $head) {
		return ref $j ?
			JSON::encode_json $j :
			$j;
	} elsif (looks_like_number($head)) {
		return get_in($j->[$head], @tail);
	} else {
		return get_in($j->{$head}, @tail);
	}
}

if ($opts{C}) {
	say get_in(load_json(), split(/\./, $opts{C}));
	exit;
}


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_build_nix() {
	my $json = load_json();
	my $ns = $json->{namespace};
	$ns =~ s/\./-/g;
	print <<~EOF;
		{ pkgs ?
		  import <nixpkgs> { overlays = [ (import ./default.nix { inherit pkgs; }) ]; }
		}:

		map (name: pkgs.$ns."\${name}")
		    (builtins.attrNames pkgs.$ns)
		EOF
}

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->{pkgs}}) {
		my $long = $pkg->{'long-description'};
		$long =~ s/^(.)/          $1/gm;
		my $license = license_for 'nix', $license_mapping, $pkg->{license};

		print <<~EOF;
		    $pkg->{'full-name'} = pkgs.stdenv.mkDerivation rec {
		      name = "$pkg->{name}";
		      version = "$pkg->{vlabel}";

		      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->{pkgs}}) {
		my $long = $pkg->{'long-description'};
		$long =~ s/^(.)/          $1/gm;
		my $license = license_for 'guix', $license_mapping, $pkg->{license};

		push @pkgs, $pkg->{'full-name'};
		print <<~EOF;
		(define-public $pkg->{'full-name'}
		  (package
		    (name "$pkg->{name}")
		    (version "$pkg->{vlabel}")
		    (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    = ();
	my @targets = ();

	for my $pkg (@{$json->{pkgs}}) {
		push @targets, <<~EOF;
			\$(DIR)/debian/tarballs/$pkg->{fname}:
				mkdir -p \$(\@D)
				wget -O \$\@ \\
					'$pkg->{url}'

			\$(DIR)/debian/builddirs/$pkg->{name}-$pkg->{vversion}: \$(DIR)/debian/tarballs/$pkg->{fname}
				mkdir -p \$(\@D)
				tar -C \$(\@D) -xf \$(DIR)/debian/tarballs/$pkg->{fname}
				touch \$\@

			EOF


		next if $pkg->{architectures} ne 'any';


		my $deb_name = "$pkg->{name}_" . (
			$pkg->{label} eq 'latest' ? 'latest' : $pkg->{vversion}
		) . "_all.deb";
		my $deb_path = "\$(DIR)/debian/public/$deb_name";

		push @debs, "\t$deb_path \\\n";

		my $ver = $pkg->{label} eq 'latest' ? '0.' . $pkg->{vversion} . '.latest' : $pkg->{vversion};
		$ver =~ s/^v//;

		push @targets, <<~EOF;
			\$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}/DEBIAN: \$(DIR)/debian/builddirs/$pkg->{name}-$pkg->{vversion}
				\$(MAKE) \\
					-C \$(DIR)/debian/builddirs/$pkg->{name}-$pkg->{vversion} \\
					install \\
					PREFIX=/usr \\
					DESTDIR="\$\$PWD"/\$(\@D)
				mkdir -p \$\@
				touch \$\@

			\$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}/DEBIAN/control: \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}/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/destdirs/$pkg->{name}-$pkg->{vversion}.deb: \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}/DEBIAN/control
				dpkg-deb --build \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}

			$deb_path: \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}.deb
				mkdir -p \$(\@D)
				cp \$(DIR)/debian/destdirs/$pkg->{name}-$pkg->{vversion}.deb \$\@


			EOF
	}


	print <<~EOF;
		.POSIX:

		DIR = $json->{datadir}

		debs = \\
		EOF
	print @debs, "\n";
	print <<~EOF;
		GPGKEY = '$json->{maintainer}{name} <$json->{maintainer}{email}>'


		all: \\
			\$(DIR)/debian/public/InRelease \$(DIR)/debian/public/Release.gpg \\
			\$(DIR)/debian/public/public-key.asc debian.out.txt

		debian.out.txt:
			\@printf '\$(DIR)/debian/public/*\\n' > \$\@


		\$(DIR)/debian/public/Packages: \$(debs)
			cd \$(\@D) && paku debian-packages *.deb > \$(\@F)

		\$(DIR)/debian/public/Release: \$(DIR)/debian/public/Packages
			paku debian-release \$(DIR)/debian/public/Packages > \$\@

		\$(DIR)/debian/public/Release.gpg: \$(DIR)/debian/public/Release
			gpg -abs -o \$\@ \$(DIR)/debian/public/Release

		\$(DIR)/debian/public/InRelease: \$(DIR)/debian/public/Release
			gpg --default-key \$(GPGKEY) -a --clear-sign -o \$\@ \$(DIR)/debian/public/Release

		\$(DIR)/debian/public/public-key.asc: \$(debs)
			gpg --armour --export \$(GPGKEY) > \$\@


		EOF
	print @targets;
}

sub emit_alpine() {
	my $d = $ARGV[0] or die 'Missing "src/alpine/" directory';
	my $json = load_json();
	for my $pkg (@{$json->{pkgs}}) {
		my @subpackages = ();
		if ($pkg->{manpages}) {
			push @subpackages, "$pkg->{name}-doc";
		}
		if ($pkg->{i18n}) {
			push @subpackages, "$pkg->{name}-lang";
		}

		my $date = $pkg->{date} =~ s/-//gr;
		my $ver = $pkg->{label} eq 'latest' ? "0.0.1_git$date" : $pkg->{vversion};
		my $dir = "$d/$pkg->{'full-name'}";
		mkdir "$dir";
		open(my $fh, '>', "$dir/APKBUILD");
		print $fh <<~EOF;
			# Maintainer: $pkg->{maintainer}
			pkgname='$pkg->{name}'
			pkgver='$ver'
			pkgrel='0'
			pkgdesc='$pkg->{description}'
			url='$pkg->{homepage}'
			arch='noarch'
			license='$pkg->{license}'
			depends=''
			makedepends=''
			checkdepends=''
			install=''
			subpackages='@subpackages'
			source='$pkg->{url}'
			pkgbasedir="\$startdir/.abuild/pkg"
			srcdir="\$startdir/.abuild/src"
			builddir="\$srcdir/$pkg->{name}-$pkg->{vversion}"

			build() {
				make PREFIX=/usr
			}

			check() {
				make check
			}

			package() {
				make install DESTDIR="\$pkgdir" PREFIX=/usr
			}

			cleanup_srcdir() {
				rm -rf "\$startdir/.abuild"
			}

			sha512sums='
			$pkg->{sha512}  $pkg->{fname}
			'
			EOF
		close $fh;
	}
	my @apks    = ();
	my @targets = ();

	for my $pkg (@{$json->{pkgs}}) {
		next if $pkg->{architectures} ne 'any';

		my $date = $pkg->{date} =~ s/-//gr;
		my $ver = $pkg->{label} eq 'latest' ? "0.0.1_git$date" : $pkg->{vversion};

		my $recipe_dir = "src/alpine/$pkg->{'full-name'}/";
		my $apk_name = "$pkg->{name}-$ver-r0.apk";
		my $apk_path = "\$(DIR)/alpine/REPODEST/alpine/x86_64/$apk_name";

		push @apks, "\t$apk_path \\\n";

		push @targets, <<~EOF;
			$apk_path: $recipe_dir/APKBUILD
				cd $recipe_dir && abuild \\
					-s \$\$OLDPWD/\$(DIR)/alpine/SRCDEST  \\
					-P \$\$OLDPWD/\$(DIR)/alpine/REPODEST \\
					sanitycheck clean fetch verify unpack build check rootpkg index clean

			EOF
	}


	print <<~EOF;
		.POSIX:

		DIR = $json->{datadir}
		RSAKEY = $json->{maintainer}{email}


		apks = \\
		EOF

	print @apks, "\n";
	print "\n\n";
	print <<~EOF;
		all: \$(apks) \$(DIR)/alpine/REPODEST/alpine/\$(RSAKEY).rsa.pub alpine.out.txt

		alpine.out.txt:
			\@printf '\$(DIR)/alpine/REPODEST/alpine/*\\n' > \$\@

		\$(apks): src/secrets/\$(RSAKEY).rsa

		\$(DIR)/alpine/REPODEST/alpine/\$(RSAKEY).rsa.pub: src/secrets/\$(RSAKEY).rsa
			mkdir -p \$(\@D)
			cp src/secrets/\$(RSAKEY).rsa.pub \$\@

		src/secrets/\$(RSAKEY).rsa: src/secrets/\$(RSAKEY).rsa.gpg
			gpg -d -o \$\@ \$\@.gpg
			chmod 400 \$\@

		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->{pkgs}}) {
		open(my $fh, '>', "$d/$pkg->{'full-name'}.rb");
		my $class_name = pascal_case $pkg->{'full-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',            "PREFIX=#{prefix}"
			    system 'make', 'check',   "PREFIX=#{prefix}"
			    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;
		      }

		      pre {
		        overflow: auto;
		        white-space: pre;
		        margin: 5px;
		        padding: 5px;
		        border-radius: 5px;
		      }

		      pre, code {
		        background-color: #ddd;
		      }

		      \@media(prefers-color-scheme: dark) {
		        :root {
		          color: white;
		          background-color: black;
		        }

		        a {
		          color: hsl(211, 100%, 60%);
		        }

		        a:visited {
		          color: hsl(242, 100%, 80%);
		        }

		        pre, code {
		          background-color: #222;
		        }
		      }
		    </style>
		  </head>
		  <body>
		    <main>
		      <h1>
		        $json->{name} package index
		      </h1>
		      <ul>
		EOF

	for my $pkg (@{$json->{pkgs}}) {
		my $guix_suffix = $pkg->{label} eq 'latest' ? '' : "\@$pkg->{version}";
		my $apt_suffix  = $pkg->{label} eq 'latest' ? '' : "=$pkg->{label}";
		my $id = $pkg->{name} . (
			$pkg->{label} eq 'latest' ? '' : "-$pkg->{vversion}"
		);
		print <<~EOF;
		        <li id="$id">
		          <details>
		            <summary>
		              <a href="#$id">$pkg->{name}</a>
		              ($pkg->{vname}) - $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>Alpine</h2>
		              <p>
		                After folowwing the
		                <a href="#alpine-instructions">Alpine instructions</a>
		                to include this repository to
		                <code>/etc/apk/repositories</code>, you can
		                install it:
		              </p>
		              <pre><code># apk add $pkg->{name}</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->{'full-name'}</code></pre>
		              <p>
		                Alternatively, you can install it imperatively:
		              </p>
		              <pre><code>\$ nix-env -i $pkg->{'full-name'}</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->{'full-name'}</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(8)</code> uses for sources
		          by adding its URL to
		          <code>/etc/apt/sources.list</code>:
		        </p>
		        <pre><code>\$ echo 'deb $json->{'base-url'}/debian ./' |
		    sudo tee -a /etc/apt/sources.list
		\$ sudo apt update</code></pre>
		        <p>
		          After that the packages from this repository will
		          be available.
		        </p>
		      </article>
		      <article id="alpine-instructions">
		        <h2>Alpine instructions</h2>
		        <p>
		          Get my public key used to sign the repository:
		        </p>
		        <pre><code>\$ wget -qO- $json->{'base-url'}/alpine/$json->{maintainer}{email}.rsa.pub |
		    doas tee /etc/apk/keys/$json->{maintainer}{email}.rsa.pub</code></pre>
		        <p>
		          Then include this repository in the
		          <code>apk-repositories(5)</code> list that
		          <code>apk(8)</code> uses to retrive package files for
		          installation:
		        </p>
		        <pre><code>\$ echo '$json->{'base-url'}/alpine' |
		    doas tee -a /etc/apk/repositories</code></pre>
		        <p>
		          After that the packages frmo this repository will 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->{pkgs}[0]->{name}</code>,
		          so it can be included in the list of packages:
		        </p>
		        <pre><code>  environment.systemPackages = with pkgs; [
		    ...
		    $overlay_name.$json->{pkgs}[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->{pkgs}[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
}


sub run_template($name, $version, $template) {
	return ($template =~ s/\@name\@/$name/gr) =~ s/\@version\@/$version/gr;
}

sub set_difference($s1, $s2) {
	my %idx = ();
	for (@{$s1}) {
		$idx{$_} = 1;
	}
	for (@{$s2}) {
		delete $idx{$_};
	}
	return keys(%idx);
}

sub emit_refresh() {
	my $json_str = do {
		local $/;
		<STDIN>;
	};
	my $json = JSON::decode_json($json_str);

	my $defaults = {
		datadir => '.paku',
		guix => {},
		mappings => {},
		maintainer => {
			name => '',
			email => '',
		},
		namespace => '',
		name => '',
		'base-url' => '',
		vcs => {
			git => '',
			http => '',
			tarball => '',
		},
		pkgs => [],
	};
	my $out = {
		%$defaults,
		%$json,
	};

	my $default_package = {
		maintainer     => $out->{defaults}{maintainer} || '',
		'exclude-tags' => [],
	};
	for my $package_any (@{$out->{packages}{any}}) {
		my $package = ref $package_any ?
			{ %$default_package, %$package_any } :
			{ %$default_package, name => $package_any };

		my $repo_url = run_template($package->{name}, '', $out->{defaults}{templates}{repository});
		my $dir = File::Temp::newdir();
		`git clone '$repo_url' '$dir'`;
		die if $?;

		my @versions = ();
		my $tags_any = $package->{tags} || $out->{defaults}{versions}{tags};
		my @tags = ();
		if (ref $tags_any) {
			push @tags, @$tags_any;
		} elsif ($tags_any eq 'all') {
			my @ret = `git -C '$dir' tag`;
			die if $?;
			chomp @ret;

			push @tags, set_difference(\@ret, $package->{'exclude-tags'});
		} else {
			die "Unknown value of \"tags\": $tags_any";
		}

		for (@tags) {
			my $ver = $_ =~ s/^v//gr;
			my %version = (
				type     => 'tag',
				vversion => $_,
				version  => $ver,
				vname    => $_,
				vlabel   => $ver,
				label    => $ver =~ s/\./-/gr,
			);
			push @versions, \%version;
		}

		my $default_branch =
			$package->{default_branch} ||
			$out->{defaults}{default_branch} ||
			'main';
		my $branches = $package->{branches} || $out->{defaults}{versions}{branches};
		for (@{$branches}) {
			my $revision = `git -C '$dir' rev-parse '$_'`;
			die if $?;
			chomp $revision;

			my $label = $_ eq $default_branch ? 'latest' : $_;

			my %version = (
				type     => 'branch',
				vversion => $revision,
				version  => $_,
				vname    => $label,
				vlabel   => $revision,
				label    => $label,
			);
			push @versions, \%version;
		}

		for my $version (@versions) {
			my $pkg = {
				%$version,
				name => $package->{name},
				architectures => 'any',
			};

			my $full_name = $pkg->{name} . (
				$pkg->{label} eq 'latest' ? '' : "-$pkg->{label}"
			);
			$pkg->{'full-name'} = $full_name;

			my $revision = `git -C '$dir' rev-parse '$pkg->{vversion}'`;
			die if $?;
			chomp $revision;
			$pkg->{revision} = $revision;

			`git -C '$dir' checkout '$pkg->{vversion}'`;
			die if $?;
			my $url = run_template($package->{name}, $pkg->{vversion}, $out->{defaults}{templates}{tarball});
			$pkg->{url} = $url;

			my $date = `git -C '$dir' log -1 --format=%cs`;
			die if $?;
			chomp $date;
			$pkg->{date} = $date;

			$pkg->{manpages} = -d "$dir/doc/" ? JSON::true : JSON::false;
			$pkg->{i18n}     = -d "$dir/po/"  ? JSON::true : JSON::false;

			for (qw(description long-description)) {
				my $s = do { local(@ARGV, $/) = "$dir/$_"; <> };
				chomp $s;
				$pkg->{$_} = $package->{$_} || $s;
			}

			my @sha_guix = `guix download '$url'`;
			die if $?;
			chomp @sha_guix;
			$pkg->{sha256guix} = $sha_guix[-1];

			my $sha_nix = `nix-prefetch-url --unpack '$url'`;
			die if $?;
			chomp $sha_nix;
			$pkg->{sha256nix} = $sha_nix;


			my $ff = File::Fetch->new(uri => $url);
			my $where = $ff->fetch(to => '/tmp');
			my $fh;

			open ($fh, '<', $where) or die "Can't open \"$where\": $!";
			my $sha256 = Digest::SHA->new(256)->addfile($fh)->hexdigest;
			close $fh;
			$pkg->{sha256} = $sha256;

			open ($fh, '<', $where) or die "Can't open \"$where\": $!";
			my $sha512 = Digest::SHA->new(512)->addfile($fh)->hexdigest;
			close $fh;
			$pkg->{sha512} = $sha512;

			unlink $where;


			for (qw(base-url homepage changelog downloads-page fname)) {
				$pkg->{$_} = $package->{$_} ||
					run_template(
						$pkg->{name},
						$pkg->{vversion},
						$out->{defaults}{templates}{$_}
					);
			}

			for (qw(inputs native-inputs)) {
				$pkg->{$_} = $package->{$_} || [];
			}

			for (qw(maintainer license)) {
				$pkg->{$_} = $package->{$_} || $out->{defaults}{$_};
			}

			for (qw(maintainer description long-description)) {
				$pkg->{"$_-b64"} = MIME::Base64::encode_base64($pkg->{$_}, '');
			}

			push @{$out->{pkgs}}, $pkg;
		}
	}
	print JSON->new->pretty->canonical->encode($out);
}


my %actions = (
	'debian-packages'  => \&emit_packages,
	'debian-release'   => \&emit_release,
	'build-nix'        => \&emit_build_nix,
	nix                => \&emit_nix,
	guix               => \&emit_guix,
	'guix-channel-key' => \&emit_guix_channel_key,
	debian             => \&emit_debian,
	alpine             => \&emit_alpine,
	homebrew           => \&emit_homebrew,
	html               => \&emit_html,
	refresh            => \&emit_refresh,
);

my $action = $ARGV[0] or die "Missing ACTION";
shift;
my $fn = $actions{$action} or die "Unknown ACTION: \"$action\"";
&$fn;