#!/usr/bin/perl =head1 NAME hdb-build-tarballs - discover Subversion tags/ folders and build tarballs from the lastes tag inside =head1 SYNOPSIS hdb-build-tarballs --status-file=tmp/visited-tags.json --output-folder=tmp/tarballs http://svn/repo/ --new-status create new status file or start from scratch --status-file location of status file --output-folder folder where tarballs will be put --lookin colon separated list of folders where to look for project/tags/ (default is to look in the base url) --trunks build/look for trunks instead of tags --pid-dir DIR folder where the pid file will be stored --mcpani add to CPAN::Mini::Inject Note: for repositories with basic authentication set DAVUSER, DAVPASS and DAVREALM environment variables =head1 DESCRIPTION The purpose is to discover SVN tags/ folders and then find the latest tag folder inside (based on revision version). This folder will be checkedout to temporary folder where `perl Build.PL && perl Build distmeta && perl Build dist` will be executed to get tarball. This tarball will be then copied to --output-folder. =cut use strict; use warnings; use Getopt::Long; use Pod::Usage; use HTTP::DAV::Browse; use File::Spec; use JSON::XS; use File::Slurp 'read_file', 'write_file'; use File::Temp 0.21; use IPC::Run qw( run timeout ); use File::Find::Rule; use File::Basename 'basename'; use Proc::PID::File; use File::Which 'which'; exit main(); sub main { my $help; my $status_filename = 'visited-tags.json'; my $output_folder = File::Spec->curdir(); my $new_status = 0; my $lookin = ''; my $cleanup_folder; my $trunks; my $pid_dir; my $mcpani; GetOptions( 'help|h' => \$help, 'status-file|s=s' => \$status_filename, 'new-status|n' => \$new_status, 'output-folder|f=s' => \$output_folder, 'cleanup-folder|c=s' => \$cleanup_folder, 'lookin|l=s' => \$lookin, 'trunks' => \$trunks, 'pid-dir=s' => \$pid_dir, 'mcpani' => \$mcpani, ) or pod2usage; pod2usage if $help; $output_folder = File::Spec->rel2abs($output_folder); $cleanup_folder = ($cleanup_folder ? File::Spec->rel2abs($cleanup_folder) : $output_folder); $status_filename = File::Spec->rel2abs($status_filename); $pid_dir = File::Spec->rel2abs($pid_dir); die "Already running!\n" if Proc::PID::File->running($pid_dir ? ('dir' => $pid_dir) : ()); my $base_uri = shift @ARGV; pod2usage if not $base_uri; # create output folder if not exists mkdir($output_folder) if not -d $output_folder; my $json = JSON::XS->new->pretty->utf8; my $browser = HTTP::DAV::Browse->new( 'base_uri' => $base_uri, 'username' => $ENV{'DAVUSER'}, 'password' => $ENV{'DAVPW'}, 'realm' => $ENV{'DAVREALM'}, ); my %visited_status = ( $new_status ? () : %{$json->decode(scalar read_file($status_filename))} ); my @lookin = split(/\s*,\s*/, $lookin); @lookin = ('') if not @lookin; foreach my $folder (@lookin) { my @folders = grep { not exists $visited_status{$_} } # only interrested in not jet discovered folders map { $folder.$_.($trunks ? 'trunk/' : 'tags/') } # prefix with current folder grep { $_ =~ m{/$} } # only interrested in folders $browser->ls($folder) # get list of all files ; # loop through newly discovered folders foreach my $folder (@folders) { my @tags = grep { # only interrested in folders of tags $trunks || ($_->{'rel_uri'} =~ m{/$}) } eval { # eval as it will throw an exception if there is no tags folder $browser->ls_detailed($folder) # get list of tags } ; # no tags no interrest if (not @tags) { $visited_status{$folder} = undef; next; } # found tags, mark for processing $visited_status{$folder} = { 'revision' => 0, 'author' => 'anonymous', }; } } # write back the status file write_file($status_filename, $json->encode(\%visited_status)); foreach my $tag_folder (keys %visited_status) { # skip non tags folders next if not defined $visited_status{$tag_folder}; # get the lastest tag (one with the greates revision number) my ($last_tag) = sort { $b->{'version-name'} <=> $a->{'version-name'} } # sort desc by revision number grep { # only interrested in folders of tags $trunks || ($_->{'rel_uri'} =~ m{/$}) } eval { # eval as it will throw an exception if there is no tags folder $browser->ls_detailed($tag_folder) # get list of tags } ; # no folders or list fail => nothing to do next if not $last_tag; # process if ($last_tag->{'version-name'} > $visited_status{$tag_folder}->{'revision'}) { # cleanup unlink(File::Spec->catfile($cleanup_folder, $visited_status{$tag_folder}->{'tarball'})) if $visited_status{$tag_folder}->{'tarball'}; $visited_status{$tag_folder} = {}; # path to latest tag my $last_tag_path = $tag_folder.($trunks ? '' : $last_tag->{'rel_uri'}->as_string); print 'processing ', $last_tag_path, ' made by ', ($last_tag->{'creator-displayname'} || 'unknown'), "\n"; # create tarball my $tarball = eval { make_tarball($base_uri.$last_tag_path, $output_folder, ($trunks ? $last_tag->{'version-name'} : ())); }; if ($@) { print '`perl Build dist` failed - ', $@; $visited_status{$tag_folder}->{'fail'} = $@; } else { $visited_status{$tag_folder}->{'tarball'} = $tarball; } # mark processed $visited_status{$tag_folder}->{'revision'} = $last_tag->{'version-name'}; $visited_status{$tag_folder}->{'author'} = $last_tag->{'creator-displayname'}; $visited_status{$tag_folder}->{'tag'} = $base_uri.$last_tag_path; $visited_status{$tag_folder}->{'time'} = time(); # write back the status file write_file($status_filename, $json->encode(\%visited_status)); # add file via CPAN::Mini::Inject if ($mcpani and $visited_status{$tag_folder}->{'tarball'}) { my @build_daily = ( $trunks ? ('-MBuild::Daily=version,'.$last_tag->{'version-name'}) : () ); my ($input, $output); run [ 'perl', @build_daily, which('mcpani'), '--authorid', 'LOCAL', '--add', '--discover-packages', '--file', File::Spec->catfile($output_folder, $tarball), ], \$input, \$output, \$output, timeout( 5*60 ) or chdirdie('mcpani failed: '.$output."\n"); } } } # write back the status file write_file($status_filename, $json->encode(\%visited_status)); return 0; } =head1 Perl functions =head2 make_tarball($url, $output_folder, $revision) Functions will checkout C<$url> try to make tarball and copy it to C<$output_folder>. Throws exceptions on failures. Returns name of the created tarball. =cut sub make_tarball { my $url = shift; my $output_folder = shift; my $revision = shift; my $file_temp_dir = File::Temp->newdir();; my $tmp_folder = $file_temp_dir->dirname(); #$tmp_folder = '/tmp/build-test/'; chdir($tmp_folder) or die 'chdir to tmp folder '.$tmp_folder.' failed: '.$!."\n"; my $input = ''; my $output = ''; # checkout the trunk my @cmd = ('svn', 'co', '--non-interactive', '--quiet', $url, $tmp_folder); run \@cmd, \$input, \$output, \$output, timeout( 30*60 ) or die '`'.join(' ',@cmd).'` failed: '.$output."\n"; my @build_daily = ( $revision ? ('-MBuild::Daily=version,'.$revision) : () ); my @cmds; if (-e 'Build.PL') { @cmds = ( [ 'perl', @build_daily, 'Build.PL' ], [ 'rm', '-f', 'MANIFEST' ], [ 'perl', @build_daily, 'Build', 'manifest' ], [ 'perl', @build_daily, 'Build', 'distmeta' ], [ 'perl', @build_daily, 'Build', 'dist' ] ); } elsif (-e 'Makefile.PL') { @cmds = ( [ 'perl', @build_daily, 'Makefile.PL' ], [ 'rm', '-f', 'MANIFEST' ], [ 'make', 'manifest' ], [ 'make', 'distmeta' ], [ 'make', 'dist' ] ); } # nothing to do if there is no Build.PL or Makefile.PL else { chdirdie('no Build.PL or Makefile.PL'."\n"); } foreach my $perl_cmd (@cmds) { run $perl_cmd, \$input, \$output, \$output, timeout( 5*60 ) or chdirdie('`'.join(' ',@{$perl_cmd}).'` failed: '.$output."\n"); } my @tarballs = File::Find::Rule ->file() ->maxdepth(1) ->name( '*.tar.gz' ) ->in($tmp_folder) ; die 'no tarball found in '.$tmp_folder."\n" if not @tarballs; die 'more than one tarball found '.join(', ', @tarballs)."\n" if @tarballs > 1; my $tarball = pop @tarballs; @cmd = ('cp', '-f', $tarball, $output_folder); run \@cmd, \$input, \$output, \$output, timeout( 5*60 ) or chdirdie('`'.join(' ',@cmd).'` failed: '.$output."\n"); chdir(File::Spec->tmpdir()); return basename($tarball); } sub chdirdie { my $msg = shift; chdir(File::Spec->tmpdir()); die $msg; } __END__ =head1 AUTHOR Jozef Kutej, C<< >> =cut