Sponsoring The Perl Toolchain Summit 2025: Help make this important event another success Learn more

#!/usr/bin/env perl
use strict;
use File::Path qw( remove_tree );
use File::Temp qw( tempdir );
use Cwd qw( cwd );
use Git::Version::Compare qw( cmp_git );
use System::Command 1.117; # loop_on
# command-line options
my %option = (
source => '/opt/src/git',
destination => '/opt/git',
limit => 0,
verbose => 0,
rc => 1,
);
GetOptions(
\%option, 'source=s', 'destination=s', 'verbose+', 'quiet',
'since=s', 'until=s', 'limit=i', 'force',
'list', 'missing', 'installed', 'rc!',
'fetch', 'test', 'docs',
'help', 'manual',
) or pod2usage( -verbose => 0 );
# simple help/manual
pod2usage( -verbose => 1 ) if $option{help};
pod2usage( -verbose => 2 ) if $option{manual};
# verbosity options
my %run_opt;
$option{quiet} = 1 if $option{test};
$option{verbose} = 0 if $option{quiet};
$run_opt{stderr} = undef if !$option{verbose};
$run_opt{stdout} = sub { print shift } if $option{verbose} > 1;
# git.git
my $r = Git::Repository->new( work_tree => $option{source} );
# fetch recent commits
$r->run( 'fetch' ) if $option{fetch};
# map version numbers to tags
my %tag_for = map { ( my $v = substr $_, 1 ) =~ y/-/./; ( $v => $_ ) }
grep /^v[^0]/ && !/^v1\.0rc/, # skip anything before 1.0
$r->run( tag => '-l', 'v*' );
# select the versions to build and install
my @versions = sort cmp_git @ARGV ? @ARGV : keys %tag_for;
# replace aliases with the canonical name
{
my %alias = (
'1.0.1' => '1.0.0a',
'1.0.2' => '1.0.0b',
);
my %seen;
@versions = grep !$seen{$_}++, map $alias{$_} || $_, @versions;
}
@versions = grep !/rc/, @versions if !$option{rc};
@versions = grep !is_installed($_), @versions if $option{missing};
@versions = grep is_installed($_), @versions if $option{installed};
@versions = grep cmp_git( $option{since}, $_ ) <= 0, @versions if $option{since};
@versions = grep cmp_git( $_, $option{until} ) <= 0, @versions if $option{until};
@versions = $option{limit} > 0
? @versions[ -$option{limit} .. -1 ] # <limit> most recent
: @versions[ 0 .. -$option{limit} - 1 ] # <limit> most ancient
if $option{limit};
# pick up invalid versions
my @nope = grep !exists $tag_for{$_}, @versions;
die "Can't compile non-existent versions: @nope\n" if @nope;
# just list the selected versions
print map "$_\n", @versions and exit if $option{list};
# test outputs TAP
if ( $option{test} ) {
require Test::More;
import Test::More;
plan( tests => scalar @versions );
$option{destination} = tempdir( CLEANUP => 1 );
}
# build install select versions
chdir $option{source} or die "Can't chdir to $option{source}: $!";
for my $version (@versions) {
# skip if that git already exists (and runs)
if ( is_installed($version) && !$option{force} ) {
print "*** GIT $version ALREADY INSTALLED ***\n" if !$option{quiet};
next;
}
else {
print "*** GIT $version ***\n" if !$option{quiet};
$r->run( checkout => '-f', '-q', $tag_for{$version} );
$r->run( clean => '-xqdf' );
# Fix various issues in the Git sources
# Fix GIT-VERSION-GEN to use `git describe` instead of `git-describe`
if (
cmp_git( $version, '1.3.3' ) <= 0
&& cmp_git( '1.1.0', $version ) <= 0
&& do { no warnings; `git-describe`; $? != 0 }
)
{
local ( $^I, @ARGV ) = ( '', 'GIT-VERSION-GEN' );
s/git-describe/git describe/, print while <>;
}
# fix GIT_VERSION in the Makefile
if ( cmp_git( $version, '1.0.9' ) == 0 ) {
local ( $^I, @ARGV ) = ( '', 'Makefile' );
s/^GIT_VERSION = .*/GIT_VERSION = $version/, print while <>;
}
# add missing #include <sys/resource.h>
elsif ( cmp_git( $version, '1.7.5.rc0' ) <= 0
&& cmp_git( '1.7.4.2', $version ) <= 0 )
{
$r->run( 'cherry-pick', '-n',
'ebae9ff95de2d0b36b061c7db833df4f7e01a41d' );
# force the expected version number
my $version_file = File::Spec->catfile( $r->work_tree, 'version' );
open my $fh, '>', $version_file
or die "Can't open $version_file: $!";
print $fh "$version\n";
}
# settings
my $prefix = File::Spec->catdir( $option{destination}, $version );
my @make = ( make => "prefix=$prefix" );
# clean up environment (possibly set by local::lib)
local $ENV{PERL_MB_OPT};
local $ENV{PERL_MM_OPT};
remove_tree( $prefix ) if -e $prefix;
# build
run_cmd( @make => '-j3' );
# install
run_cmd( @make => 'install' );
run_cmd( @make => 'install-doc' ) if $option{docs};
# test the installation and remove all
if ( $option{test} ) {
ok( is_installed($version), "$version installed successfully" );
remove_tree( $prefix );
}
}
}
sub run_cmd {
print "* @_\n" if !$option{quiet};
System::Command->loop_on( command => \@_, %run_opt ) or die "FAIL: @_\n";
}
sub is_installed {
my ($version) = @_;
my $git =
File::Spec->catfile( $option{destination}, $version, 'bin', 'git' );
return eval { Git::Repository->version_eq( $version, { git => $git } ) };
}
__END__
=pod
=head1 NAME
build-git - Build and install any Git
=head1 SYNOPSIS
# clone git.git
# build and install Git 1.7.2
$ build-git 1.7.2
# build and install all versions between 1.6.5 and 2.1.0
$ build-git --since 1.6.5 --until 2.1.0
# build and install all versions of Git (since 1.0.0)
$ build-git
# build and install the 5 most recent versions of the selection
$ build-git --limit 5 ...
# build and install the 5 most ancient versions of the selection
$ build-git --limit -5 ...
# fetch the latest commit and install the latest git
$ build-git --fetch --limit 1
=head1 OPTIONS AND ARGUMENTS
=head2 Options
--source <directory> The location of the git.git clone checkout
--destination <directory> The location of the Git collection
--fetch Start by doing a `git fetch`
--list List the selected versions and exit
--test Compile, install, test and uninstall
the selected versions. Outputs TAP.
(Implies --quiet and force --destination to
a temporary directory.)
--since <version> Select versions greater or equal to <version>
--until <version> Select versions less or equal to <version>
--missing Select only non-installed versions
--installed Select only installed versions
(incompatible with the --missing option)
--limit <count> Limit the number of versions in the selection
(if <count> is positive, keep the most recent
ones, if <count> is negative, keep the oldest)
--verbose Used once, shows the STDERR of the commands
run to compile and install Git.
Used twice, also shows the STDOUT.
--quiet Silence all output, including the progress status
=head2 Arguments
If no argument is given, all versions are selected.
=head1 DESCRIPTION
B<build-git> is a small utility to build and install any version of Git.
It automatically applies some necessary patches that are needed to compile
Git on recent systems.
It is used to test the L<Git::Repository> module against all versions
of Git.
=head1 AUTHOR
Philippe Bruhat (BooK) <book@cpan.org>
=head1 COPYRIGHT
Copyright 2016 Philippe Bruhat (BooK), all rights reserved.
=head1 LICENSE
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
=cut