The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

use strict;
use Config;
use Cwd qw(getcwd);
use Carp qw(croak);
require Exporter;
our @ISA = qw(Exporter);
our $Is_VMS = $^O eq 'VMS';
our $Is_MacOS = $^O eq 'MacOS';
our $Is_FreeBSD = $^O eq 'freebsd';
our @EXPORT = qw(which_perl perl_lib makefile_name makefile_backup
make make_run run make_macro calibrate_mtime
have_compiler slurp write_file
$Is_VMS $Is_MacOS
run_ok
hash2files
in_dir
);
# Setup the code to clean out %ENV
{
# Environment variables which might effect our testing
my @delete_env_keys = qw(
PERL_MM_OPT
PERL_MM_USE_DEFAULT
HARNESS_TIMER
HARNESS_OPTIONS
HARNESS_VERBOSE
PREFIX
MAKEFLAGS
PERL_INSTALL_QUIET
);
my %default_env_keys;
# Inform the BSDPAN hacks not to register modules installed for testing.
$default_env_keys{PORTOBJFORMAT} = 1 if $Is_FreeBSD;
$default_env_keys{ACTIVEPERL_CONFIG_SILENT} = 1;
# Remember the ENV values because on VMS %ENV is global
# to the user, not the process.
my %restore_env_keys;
sub clean_env {
for my $key (keys %default_env_keys) {
$ENV{$key} = $default_env_keys{$key} unless $ENV{$key};
}
for my $key (@delete_env_keys) {
if( exists $ENV{$key} ) {
$restore_env_keys{$key} = delete $ENV{$key};
}
else {
delete $ENV{$key};
}
}
}
END {
while( my($key, $val) = each %restore_env_keys ) {
$ENV{$key} = $val;
}
}
}
clean_env();
=head1 NAME
MakeMaker::Test::Utils - Utility routines for testing MakeMaker
=head1 SYNOPSIS
use MakeMaker::Test::Utils;
my $perl = which_perl;
perl_lib;
my $makefile = makefile_name;
my $makefile_back = makefile_backup;
my $make = make;
my $make_run = make_run;
make_macro($make, $targ, %macros);
my $mtime = calibrate_mtime;
my $out = run($cmd);
my $have_compiler = have_compiler();
my $text = slurp($filename);
=head1 DESCRIPTION
A consolidation of little utility functions used throughout the
MakeMaker test suite.
=head2 Functions
The following are exported by default.
=over 4
=item B<which_perl>
my $perl = which_perl;
Returns a path to perl which is safe to use in a command line, no
matter where you chdir to.
=cut
sub which_perl {
my $perl = $^X;
$perl ||= 'perl';
# VMS should have 'perl' aliased properly
return $perl if $Is_VMS;
$perl .= $Config{exe_ext} unless $perl =~ m/$Config{exe_ext}$/i;
my $perlpath = File::Spec->rel2abs( $perl );
unless( $Is_MacOS || -x $perlpath ) {
# $^X was probably 'perl'
# When building in the core, *don't* go off and find
# another perl
die "Can't find a perl to use (\$^X=$^X), (\$perlpath=$perlpath)"
if $ENV{PERL_CORE};
foreach my $path (File::Spec->path) {
$perlpath = File::Spec->catfile($path, $perl);
last if -x $perlpath;
}
}
$perlpath = qq{"$perlpath"}; # "safe... in a command line" even with spaces
return $perlpath;
}
=item B<perl_lib>
perl_lib;
Sets up environment variables so perl can find its libraries.
=cut
my $old5lib = $ENV{PERL5LIB};
my $had5lib = exists $ENV{PERL5LIB};
sub perl_lib {
my $basecwd = (File::Spec->splitdir(getcwd))[-1];
croak "Basename of cwd needs to be 't' but is '$basecwd'\n"
unless $basecwd eq 't';
# perl-src/t/
my $lib = $ENV{PERL_CORE} ? qq{../lib}
# ExtUtils-MakeMaker/t/
: qq{../blib/lib};
$lib = File::Spec->rel2abs($lib);
my @libs = ($lib);
push @libs, $ENV{PERL5LIB} if exists $ENV{PERL5LIB};
$ENV{PERL5LIB} = join($Config{path_sep}, @libs);
unshift @INC, $lib;
}
END {
if( $had5lib ) {
$ENV{PERL5LIB} = $old5lib;
}
else {
delete $ENV{PERL5LIB};
}
}
=item B<makefile_name>
my $makefile = makefile_name;
MakeMaker doesn't always generate 'Makefile'. It returns what it
should generate.
=cut
sub makefile_name {
return $Is_VMS ? 'Descrip.MMS' : 'Makefile';
}
=item B<makefile_backup>
my $makefile_old = makefile_backup;
Returns the name MakeMaker will use for a backup of the current
Makefile.
=cut
sub makefile_backup {
my $makefile = makefile_name;
return $Is_VMS ? "$makefile".'_old' : "$makefile.old";
}
=item B<make>
my $make = make;
Returns a good guess at the make to run.
=cut
sub make {
my $make = $Config{make};
$make = $ENV{MAKE} if exists $ENV{MAKE};
return $Is_VMS ? $make : qq{"$make"};
}
=item B<make_run>
my $make_run = make_run;
Returns the make to run as with make() plus any necessary switches.
=cut
sub make_run {
my $make = make;
$make .= ' -nologo' if $make eq 'nmake';
return $make;
}
=item B<make_macro>
my $make_cmd = make_macro($make, $target, %macros);
Returns the command necessary to run $make on the given $target using
the given %macros.
my $make_test_verbose = make_macro(make_run(), 'test',
TEST_VERBOSE => 1);
This is important because VMS's make utilities have a completely
different calling convention than Unix or Windows.
%macros is actually a list of tuples, so the order will be preserved.
=cut
sub make_macro {
my($make, $target) = (shift, shift);
my $is_mms = $make =~ /^MM(K|S)/i;
my @macros;
while( my($key,$val) = splice(@_, 0, 2) ) {
push @macros, qq{$key=$val};
}
my $macros = '';
if (scalar(@macros)) {
if ($is_mms) {
map { $_ = qq{"$_"} } @macros;
$macros = '/MACRO=(' . join(',', @macros) . ')';
}
else {
$macros = join(' ', @macros);
}
}
return $is_mms ? "$make$macros $target" : "$make $target $macros";
}
=item B<calibrate_mtime>
my $mtime = calibrate_mtime;
When building on NFS, file modification times can often lose touch
with reality. This returns the mtime of a file which has just been
touched.
=cut
sub calibrate_mtime {
my $file = "calibrate_mtime-$$.tmp";
open(FILE, ">$file") || die $!;
print FILE "foo";
close FILE;
my($mtime) = (stat($file))[9];
unlink $file;
return $mtime;
}
=item B<run>
my $out = run($command);
my @out = run($command);
Runs the given $command as an external program returning at least STDOUT
as $out. If possible it will return STDOUT and STDERR combined as you
would expect to see on a screen.
=cut
sub run {
my $cmd = shift;
# Unix, modern Windows and OS/2 from 5.005_54 up can handle 2>&1
# This makes our failure diagnostics nicer to read.
if (MM->can_redirect_error) {
return `$cmd 2>&1`;
}
else {
return `$cmd`;
}
}
=item B<run_ok>
my @out = run_ok($cmd);
Like run() but it tests that the result exited normally.
The output from run() will be used as a diagnostic if it fails.
=cut
sub run_ok {
my $tb = Test::Builder->new;
my @out = run(@_);
$tb->cmp_ok( $?, '==', 0, "run(@_)" ) || $tb->diag(@out);
return wantarray ? @out : join "", @out;
}
=item have_compiler
$have_compiler = have_compiler;
Returns true if there is a compiler available for XS builds.
=cut
sub have_compiler {
return 1 if $ENV{PERL_CORE};
my $have_compiler = 0;
in_dir(sub {
eval {
my $cb = ExtUtils::CBuilder->new(quiet=>1);
$have_compiler = $cb->have_compiler;
};
});
return $have_compiler;
}
=item slurp
$contents = slurp($filename);
Returns the $contents of $filename.
Will die if $filename cannot be opened.
=cut
sub slurp {
my $filename = shift;
local $/ = undef;
open my $fh, $filename or die "Can't open $filename for reading: $!";
my $text = <$fh>;
close $fh;
return $text;
}
=item write_file
write_file('filename', @contents);
Writes the content to the given file. Will die if errors occur.
=cut
sub write_file {
my ($file, @contents) = @_;
my $utf8 = ("$]" < 5.008 or !$Config{useperlio}) ? "" : ":utf8";
open my $fh, ">$utf8", $file or die "Can't create $file: $!";
print $fh @contents or die "Can't write to $file: $!";
close $fh or die "Can't close $file: $!";
}
=item hash2files
hash2files('dirname', { 'filename' => 'some content' });
Goes through given hash-ref, treating each key as a /-separated filename
under the specified directory, and writing the value into it. Will create
any necessary directories.
Will die if errors occur.
=cut
sub hash2files {
my ($prefix, $hashref) = @_;
while(my ($file, $text) = each %$hashref) {
# Convert to a relative, native file path.
$file = File::Spec->catfile(File::Spec->curdir, $prefix, split m{\/}, $file);
my $dir = dirname($file);
mkpath $dir;
write_file($file, $text);
# ensure file at least 1 second old for makes that assume
# files with the same time are out of date.
my $time = calibrate_mtime();
utime $time, $time - 1, $file;
}
}
=item in_dir
$retval = in_dir(\&coderef);
$retval = in_dir(\&coderef, $specified_dir);
$retval = in_dir { somecode(); };
$retval = in_dir { somecode(); } $specified_dir;
Does a C<chdir> to either a directory. If none is specified, one is
created with L<File::Temp> and then automatically deleted after. It ends
by C<chdir>ing back to where it started.
If the given code throws an exception, it will be re-thrown after the
re-C<chdir>.
Returns the return value of the given code.
=cut
sub in_dir(&;$) {
my $code = shift;
require File::Temp;
my $dir = shift || File::Temp::tempdir(TMPDIR => 1, CLEANUP => 1);
# chdir to the new directory
my $orig_dir = getcwd();
chdir $dir or die "Can't chdir to $dir: $!";
# Run the code, but trap the error so we can chdir back
my $return;
my $ok = eval { $return = $code->(); 1; };
my $err = $@;
# chdir back
chdir $orig_dir or die "Can't chdir to $orig_dir: $!";
# rethrow if necessary
die $err unless $ok;
return $return;
}
=back
=head1 AUTHOR
Michael G Schwern <schwern@pobox.com>
=cut
1;