aboutsummaryrefslogblamecommitdiff
path: root/src/bin/paku
blob: 55372b6ab1037d2e628a0860ef8dd41d9c746c64 (plain) (tree)
1
2
3
4
5
6
7
8

                   





                                         


                      



                          
                                                


                         
 




                          

                                                                               

                                                 





                                                     

                                                                            
 
 

                         



                                                                   
 





                                                                         
                   

 







                              
 
 
         
                                             

                      

 



                      

 
 
                                
                                    
 
                                              








































                                                                            
 
                    
                                                          



































                                                                                

 
                
                               

                                    




                              
                                            
                                                
                                     


                                                               
 

                                                      
 
                             
                                                                               





                                                  
                                                     



                                                      
 


                                                            
                           






                                                   
                                     
                                      
                           
                 












                                                            


                                                                        




                                                   




                     

 
                 
                               

                                    
                     

                                             




                                                         
                     
                                                           






                                               
                      
                                            
                                                
                                                   
                 
 

                                                      
 
                                                         
                             
                                                         
                          
                                                       
                                               




                                           
                                                          









                                                                             
 






                                                                             
 









                                                                             

                                                     

                   
                                               
                                     




                                                         

                           






                              

 
                   
                               







                                                        
 
                         
 
                                            

                                                                                                  
 
                                      
                                                       


                                                     
 
                                                                                                      
                                                
                                                                                            
                                          
 
                           
 
 
                                                       
 



                                                                                                                      
 
                                                                                                           
                               

                                      
                                                                                                                                  
                                           
                                                                                            

                                                      
                                                                                                             


                                             
                                                                                                                                                      









                                                                              
                                                                                      


                                                                              
                                                                                      

                                                                              
                                                                            




                                                                                   

                                                                                                                                                   
 
                                                                                                                                    
                                                
                                                                                               
 
 

                           
 
 

                               






                                          
 

                                              
 
 
                                                                                                      
 
                           
                                                 
 
 
                                                 
                                                                                                  
 

                                                                                       
 

                                                                  
 

                                                                                                   
 
                                                                     
                                                               
 
 




                       



































                                                                         
                 
                               
































                                                                                          

                                                                               
                             
                                                            

                                     

                                                                                    










































                                                                                           









                                                                                          



                                    
 

































                                                                                                                 























                                                                                                                                                                             
                                                                                                                   







                                                                 













                                                                                                                                       



                           



               
                                             



                                            
                                             




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

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}}) {
		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->{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->{'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 $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}}) {
		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->{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->{'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 $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) && perl \$\$OLDPWD/src/bin/paku debian-packages *.deb > \$(\@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 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->{'base-url'}'
			  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 $suffix = $pkg->{label} eq 'latest' ? '' : "-$pkg->{label}";
		my $apt_suffix = $suffix =~ s/-/=/r;
		print <<~EOF;
		        <li id="$pkg->{name}-$pkg->{label}">
		          <details>
		            <summary>
		              <a href="#$pkg->{name}-$pkg->{label}">$pkg->{name}</a>
		              ($pkg->{label}) - $pkg->{description}
		            </summary>
		            <p>
		            <a href="$pkg->{'base-url'}/">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}$suffix</code></pre>
		              <p>
		                Alternatively, you can install it imperatively:
		              </p>
		              <pre><code>\$ guix install $pkg->{name}$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
	}

	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 '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)</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- https://euandre.org/s/package-repository/debian/public-key.asc | sudo tee /etc/apt/trusted.gpg.d/euandre.org.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 https://euandre.org/s/package-repository/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 = "https://euandre.org/git/package-repository/snapshot/package-repository-main.tar.xz";
		      }) { inherit pkgs; })
		    ];
		  };</code></pre>
		        <p>
		          All the packages live under the
		          <code>org-euandre</code> attribute set.
		        </p>
		      </article>
		      <article id="homebrew-instructions">
		        <h2>Homebrew instructions</h2>
		        <p>
		          Add this repository as a tap:
		        </p>
		        <pre><code>\$ brew tap --force-auto-update org/euandre https://euandre.org/git/package-repository/</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;