#!/usr/bin/perl # A simple system for making software releases # (c) 2003--2012 Martin Mares package UCW::Release; use strict; use warnings; use Getopt::Long; our $verbose = 0; sub new($$) { my ($class,$basename) = @_; my $s = { "PACKAGE" => $basename, "rules" => [ # p=preprocess, s=subst, -=discard '(^|/)(CVS|\.arch-ids|{arch}|\.git|tmp)/' => '-', '\.(lsm|spec)$' => 'ps', '(^|/)README$' => 's' ], "conditions" => { # Symbols, which can serve as conditions for the preprocessor }, "DATE" => `date '+%Y-%m-%d' | tr -d '\n'`, "LSMDATE" => `date '+%y%m%d' | tr -d '\n'`, "distfiles" => [ # Files to be uploaded ], "uploads" => [ # Locations where we want to upload, e.g.: # { "url" => "ftp://metalab.unc.edu/incoming/linux/", # "filter" => '(\.tar\.gz|\.lsm)$', } ], "test_compile" => "make", # "archive_dir" => "/tmp/archives/$basename", # Options "do_test" => 1, "do_patch" => 0, "diff_against" => "", "do_upload" => 1, "do_sign" => 1, }; bless $s; return $s; } sub Confirm($) { my ($s) = @_; print " "; ; } sub GetVersionFromGit($) { my ($s) = @_; return if defined $s->{"VERSION"}; my $desc = `git describe --tags`; die "git describe failed\n" if $?; chomp $desc; my ($ver, $rest) = ($desc =~ /^v([0-9.]+)(.*)/) or die "Failed to understand output of git describe: $desc\n"; print "Detected version $ver from git description $desc\n"; if ($rest ne '') { print "WARNING: We are several commits past release tag... "; $s->Confirm; } $s->{"VERSION"} = $ver; return $ver; } sub GetVersionFromFile($) { my ($s,$file,$rx) = @_; return if defined $s->{"VERSION"}; open F, $file or die "Unable to open $file for version autodetection"; while () { chomp; if (/$rx/) { $s->{"VERSION"} = $1; print "Detected version $1 from $file\n" if $verbose; last; } } close F; if (!defined $s->{"VERSION"}) { die "Failed to auto-detect version"; } return $s->{"VERSION"}; } sub GetVersionsFromChangelog($) { my ($s,$file,$rx) = @_; return if defined $s->{"VERSION"}; open F, $file or die "Unable to open $file for version autodetection"; while () { chomp; if (/$rx/) { if (!defined $s->{"VERSION"}) { $s->{"VERSION"} = $1; print "Detected version $1 from $file\n" if $verbose; } elsif ($s->{"VERSION"} eq $1) { # do nothing } else { $s->{"OLDVERSION"} = $1; print "Detected previous version $1 from $file\n" if $verbose; last; } } } close F; if (!defined $s->{"VERSION"}) { die "Failed to auto-detect version"; } return $s->{"VERSION"}; } sub InitDist($) { my ($s,$dd) = @_; $s->{"DISTDIR"} = $dd; print "Initializing dist directory $dd\n" if $verbose; `rm -rf $dd`; die if $?; `mkdir -p $dd`; die if $?; if ($s->{"archive_dir"}) { unshift @{$s->{"uploads"}}, { "url" => "file:" . $s->{"archive_dir"} }; } } sub ExpandVar($$) { my ($s,$v) = @_; if (defined $s->{$v}) { return $s->{$v}; } else { die "Reference to unknown variable $v"; } } sub TransformFile($$$) { my ($s,$file,$action) = @_; my $preprocess = ($action =~ /p/); my $subst = ($action =~ /s/); my $dest = "$file.dist"; open I, "<", $file or die "open($file): $?"; open O, ">", "$dest" or die "open($dest): $!"; my @ifs = (); # stack of conditions, 1=satisfied my $empty = 0; # last line was empty my $is_makefile = ($file =~ /(Makefile|.mk)$/); while () { if ($subst) { s/@([0-9A-Za-z_]+)@/$s->ExpandVar($1)/ge; } if ($preprocess) { if (/^#/ || $is_makefile) { if (/^#?ifdef\s+(\w+)/) { if (defined ${$s->{"conditions"}}{$1}) { push @ifs, ${$s->{"conditions"}}{$1}; next; } push @ifs, 0; } elsif (/^#ifndef\s+(\w+)/) { if (defined ${$s->{"conditions"}}{$1}) { push @ifs, -${$s->{"conditions"}}{$1}; next; } push @ifs, 0; } elsif (/^#if\s+/) { push @ifs, 0; } elsif (/^#?endif/) { my $x = pop @ifs; defined $x or die "Improper nesting of conditionals"; $x && next; } elsif (/^#?else/) { my $x = pop @ifs; defined $x or die "Improper nesting of conditionals"; push @ifs, -$x; $x && next; } } @ifs && $ifs[$#ifs] < 0 && next; if (/^$/) { $empty && next; $empty = 1; } else { $empty = 0; } } print O; } close O; close I; ! -x $file or chmod(0755, "$dest") or die "chmod($dest): $!"; rename $dest, $file or "rename($dest,$file): $!"; } sub GenPackage($) { my ($s) = @_; $s->{"PKG"} = $s->{"PACKAGE"} . "-" . $s->{"VERSION"}; my $dd = $s->{"DISTDIR"}; my $pkg = $s->{"PKG"}; my $dir = "$dd/$pkg"; print "Generating $dir\n"; system "git archive --format=tar --prefix=$dir/ HEAD | tar xf -"; die if $?; my @files = `cd $dir && find . -type f`; die if $?; for my $f (@files) { chomp $f; $f =~ s/^\.\///; my $action = ""; my @rules = @{$s->{"rules"}}; while (@rules) { my $rule = shift @rules; my $act = shift @rules; if ($f =~ $rule) { $action = $act; last; } } if ($action eq '') { } elsif ($action =~ /-/) { unlink "$dir/$f" or die "Cannot unlink $dir/$f: $!\n"; print "$f (unlinked)\n" if $verbose; } else { print "$f ($action)\n" if $verbose; $s->TransformFile("$dir/$f", $action); } } return $dir; } sub GenFile($$) { my ($s,$f) = @_; my $sf = $s->{"DISTDIR"} . "/" . $s->{"PKG"} . "/$f"; my $df = $s->{"DISTDIR"} . "/$f"; print "Generating $df\n"; `cp $sf $df`; die if $?; push @{$s->{"distfiles"}}, $df; } sub SignFile($$) { my ($s, $file) = @_; $s->{'do_sign'} or return; print "Signing $file\n"; system "gpg", "--armor", "--detach-sig", "$file"; die if $?; rename "$file.asc", "$file.sign" or die "No signature produced!?\n"; push @{$s->{"distfiles"}}, "$file.sign"; } sub MakeArchive($) { my ($s) = @_; my $dd = $s->{"DISTDIR"}; my $pkg = $s->{"PKG"}; print "Creating $dd/$pkg.tar\n"; my $tarvv = $verbose ? "vv" : ""; `cd $dd && tar c${tarvv}f $pkg.tar $pkg >&2`; die if $?; print "Creating $dd/$pkg.tar.gz\n"; `gzip <$dd/$pkg.tar >$dd/$pkg.tar.gz`; die if $?; push @{$s->{"distfiles"}}, "$dd/$pkg.tar.gz"; # print "Creating $dd/$pkg.tar.bz2\n"; # `bzip2 <$dd/$pkg.tar >$dd/$pkg.tar.bz2`; die if $?; # push @{$s->{"distfiles"}}, "$dd/$pkg.tar.bz2"; $s->SignFile("$dd/$pkg.tar"); } sub ParseOptions($) { my ($s) = @_; GetOptions( "verbose!" => \$verbose, "test!" => \$s->{"do_test"}, "patch!" => \$s->{"do_patch"}, "diff-against=s" => \$s->{"diff_against"}, "version=s" => \$s->{"VERSION"}, "upload!" => \$s->{"do_upload"}, "sign!" => \$s->{"do_sign"}, ) || die "Syntax: release [--verbose] [--test] [--nopatch] [--version=] [--diff-against=] [--noupload] [--nosign]"; } sub Test($) { my ($s) = @_; $s->{"do_test"} or return; my $dd = $s->{"DISTDIR"}; my $pkg = $s->{"PKG"}; my $tdir = "$dd/$pkg.test"; $s->{"TESTDIR"} = $tdir; `cp -a $dd/$pkg $tdir`; die if $?; my $log = "$tdir.log"; print "Doing a test compilation\n"; my $make = $s->{"test_compile"}; `( cd $tdir && $make ) >$log 2>&1`; die "There were errors. Please inspect $log" if $?; `grep -q [Ww]arning $log`; $? or print "There were warnings! Please inspect $log.\n"; } sub MakePatch($) { my ($s) = @_; $s->{"do_patch"} or return; my $dd = $s->{"DISTDIR"}; my $pkg1 = $s->{"PKG"}; my $oldver; if ($s->{"diff_against"} ne "") { $oldver = $s->{"diff_against"}; } elsif (defined $s->{"OLDVERSION"}) { $oldver = $s->{"OLDVERSION"}; } else { print "WARNING: No previous version known. No patch generated.\n"; return; } my $pkg0 = $s->{"PACKAGE"} . "-" . $oldver; my $oldarch = $s->{"archivedir"} . "/" . $pkg0 . ".tar.gz"; -f $oldarch or die "MakePatch: $oldarch not found"; print "Unpacking $pkg0 from $oldarch\n"; `cd $dd && tar xzf $oldarch`; die if $?; my $diff = $s->{"PACKAGE"} . "-" . $oldver . "-" . $s->{"VERSION"} . ".diff.gz"; print "Creating a patch from $pkg0 to $pkg1: $diff\n"; `cd $dd && diff -ruN $pkg0 $pkg1 | gzip >$diff`; die if $?; push @{$s->{"distfiles"}}, "$dd/$diff"; $s->SignFile("$dd/$diff"); } sub Upload($) { my ($s) = @_; $s->{"do_upload"} or return; foreach my $u (@{$s->{"uploads"}}) { my $url = $u->{"url"}; print "Upload to $url :\n"; my @files = (); my $filter = $u->{"filter"} || ".*"; foreach my $f (@{$s->{"distfiles"}}) { if ($f =~ $filter) { print "\t$f\n"; push @files, $f; } } $s->Confirm; if ($url =~ m@^file:(.*)@) { my $dir = $1; $dir =~ s@^///@/@; `cp @files $dir/`; die if $?; } elsif ($url =~ m@^scp://([^/]+)(.*)@) { $, = " "; my $host = $1; my $dir = $2; $dir =~ s@^/~@~@; $dir =~ s@^/\./@@; my $cmd = "scp @files $host:$dir\n"; `$cmd`; die if $?; } elsif ($url =~ m@ftp://([^/]+)(.*)@) { my $host = $1; my $dir = $2; open FTP, "|ftp -v $host" or die; print FTP "cd $dir\n"; foreach my $f (@files) { (my $ff = $f) =~ s@.*\/([^/].*)@$1@; print FTP "put $f $ff\n"; } print FTP "bye\n"; close FTP; die if $?; } else { die "Don't know how to handle this URL scheme"; } } } 1;