blob: 137dc38f62c67469de5a1309ec6a15c1f7b87baa [file] [log] [blame]
# Written by Zack Weinberg <zackw at panix.com> in 2017, 2020, 2021.
# To the extent possible under law, Zack Weinberg has waived all
# copyright and related or neighboring rights to this work.
#
# See https://creativecommons.org/publicdomain/zero/1.0/ for further
# details.
package BuildCommon;
use v5.14; # implicit use strict, use feature ':5.14'
use warnings FATAL => 'all';
use utf8;
use open qw(:utf8);
no if $] >= 5.018, warnings => 'experimental::smartmatch';
no if $] >= 5.022, warnings => 'experimental::re_strict';
use if $] >= 5.022, re => 'strict';
use Cwd qw(realpath);
use File::Spec::Functions qw(
catfile
catpath
file_name_is_absolute
path
splitpath
);
use FindBin ();
use POSIX ();
our @EXPORT_OK;
use Exporter qw(import);
BEGIN {
@EXPORT_OK = qw(
ensure_C_locale
ensure_empty_stdin
error
get_status
get_status_and_output
popen
run
sh_split
sh_quote
subprocess_error
which
);
}
#
# Utilities for dealing with subprocesses.
#
# Diagnostics: report some kind of catastrophic internal error.
# Exit code 99 tells the Automake test driver to mark a test as
# 'errored' rather than 'failed'.
sub error { ## no critic (Subroutines::RequireArgUnpacking)
my $msg = join q{ }, @_;
print {*STDERR} $FindBin::Script, ': ERROR: ', $msg, "\n";
exit 99;
}
# Like 'error', but the problem was with a subprocess, detected upon
# trying to start the program named as @_.
sub invocation_error { ## no critic (Subroutines::RequireArgUnpacking)
my $err = "$!";
my $cmd = join q{ }, @_;
error("failed to invoke $cmd: $err");
}
# Like 'error', but the problem was with a subprocess, detected upon
# termination of the program named as @_; interpret both $! and $?
# appropriately.
sub subprocess_error { ## no critic (Subroutines::RequireArgUnpacking)
my $syserr = $!;
my $status = $?;
my $cmd = join q{ }, @_;
if ($syserr) {
error("system error with $cmd: $syserr");
} elsif ($status == 0) {
return;
} elsif (($status & 0xFF) == 0) {
# we wouldn't be here if the exit status was zero
error("$cmd: exit " . ($status >> 8));
} else {
my $sig = ($status & 0x7F);
# Neither Perl core nor the POSIX module exposes strsignal.
# This is the least terrible kludge I can presently find;
# it decodes the numbers to their <signal.h> constant names
# (e.g. "SIGKILL" instead of "Killed" for signal 9).
# Linear search through POSIX's hundreds of symbols is
# acceptable because this function terminates the process,
# so it can only ever be called once per run.
my $signame;
while (my ($name, $glob) = each %{'POSIX::'}) {
if ($name =~ /^SIG(?!_|RT)/ && (${$glob} // -1) == $sig) {
$signame = $name;
last;
}
}
$signame //= "signal $sig";
error("$cmd: killed by $signame");
}
}
# Split a string into words, exactly the way the Bourne shell would do
# it, with the default setting of IFS, when the string is the result
# of a variable expansion. If any of the resulting words would be
# changed by filename expansion, throw an exception, otherwise return
# a list of the words.
#
# Note: the word splitting process does *not* look for nested
# quotation, substitutions, or operators. For instance, if a
# shell variable was set with
# var='"ab cd"'
# then './a.out $var' would pass two arguments to a.out:
# '"ab' and 'cd"'.
sub sh_split {
my @words = split /[ \t\n]+/, shift;
for my $w (@words) {
die "sh_split: '$w' could be changed by filename expansion"
if $w =~ / (?<! \\) [\[?*] /ax;
}
return @words;
}
# Quote a string, or list of strings, so that they will pass
# unmolested through the shell. Avoids adding quotation whenever
# possible. Algorithm copied from Python's shlex.quote.
sub sh_quote { ## no critic (Subroutines::RequireArgUnpacking)
my @quoted;
for my $w (@_) {
if ($w =~ m{[^\w@%+=:,./-]}a) {
my $q = $w;
$q =~ s/'/'\\''/g;
$q =~ s/^/'/;
$q =~ s/$/'/;
push @quoted, $q;
} else {
push @quoted, $w;
}
}
return wantarray ? @quoted : $quoted[0];
}
# Emit a logging message for the execution of a subprocess whose
# argument vector is @_.
sub log_execution { ## no critic (Subroutines::RequireArgUnpacking)
print {*STDERR} '+ ', join(q{ }, sh_quote(@_)), "\n";
return;
}
# Run, and log execution of, a subprocess, with no I/O redirection.
# @_ should be an argument vector.
# Calls invocation_error() and/or subprocess_error() as appropriate.
# Does *not* call which(); do that yourself if you need it.
sub run { ## no critic (Subroutines::RequireArgUnpacking)
die 'run: no command to execute'
if scalar(@_) == 0;
log_execution(@_);
my $pid = fork
// invocation_error($_[0]);
if ($pid == 0) {
# child
{ exec {$_[0]} @_; };
print {*STDERR} "exec $_[0] failed: $!\n";
exit(127);
}
# parent
waitpid $pid, 0;
undef $!;
subprocess_error(@_) if $?;
}
# Run, and log execution of, a subprocess. @_ should be one of the
# open modes that creates a pipe, followed by an argument vector.
# An anonymous filehandle for the pipe is returned.
# Calls invocation_error() if open() fails.
# Does *not* call which(); do that yourself if you need it.
sub popen {
my ($mode, @args) = @_;
die "popen: inappropriate mode argument '$mode'"
unless $mode eq '-|' || $mode eq '|-';
die 'popen: no command to execute'
if scalar(@args) == 0;
log_execution(@args);
open my $fh, $mode, @args
or invocation_error($args[0]);
return $fh;
}
# Run, and log execution of, a subprocess. @_ should be an argument vector.
# If the subprocess exits normally (successful or unsuccessful),
# returns the exit status.
# If the subprocess could not be started because there is no such command,
# returns -1.
# Otherwise invocation_error/subprocess_error are called as appropriate.
sub get_status {
die 'run: no command to execute'
if scalar(@_) == 0;
log_execution(@_);
my $pid = fork
// invocation_error($_[0]);
if ($pid == 0) {
# child
{ exec {$_[0]} @_; };
exit(126) if $!{ENOENT};
print {*STDERR} "exec $_[0] failed: $!\n";
exit(127);
}
# parent
waitpid $pid, 0;
undef $!;
if ($? == 0x7F00 || ($? & 0x7F) != 0) {
subprocess_error(@_);
}
my $status = $? >> 8;
return -1 if $status == 126;
return $status;
}
# Run, and log execution of, a subprocess. Capture all of its output,
# including both stdout and stderr.
# @_ should be an argument vector.
# If the subprocess exits normally (successful or unsuccessful),
# returns a list whose first element is the exit status, followed by
# all the lines of output from the subprocess (stdout and stderr are
# intermingled).
# If the subprocess could not be started because there is no such command,
# returns (-1,).
# Otherwise invocation_error/subprocess_error are called as appropriate.
sub get_status_and_output {
die 'get_status_and_output: no command to execute'
if scalar(@_) == 0;
log_execution(@_);
my $pid = open(my $fh, '-|')
// invocation_error($_[0]);
if ($pid == 0) {
# child
open(STDERR, ">&STDOUT") or do {
print {*STDERR} "Can't dup STDOUT: $!\n";
exit(127);
};
{ exec {$_[0]} @_; };
exit(126) if $!{ENOENT};
print {*STDERR} "exec $_[0] failed: $!\n";
exit(127);
}
# parent
my @lines = <$fh>;
close $fh or do {
if ($! != 0 || ($? & 0x7F) != 0) {
subprocess_error(@_);
}
};
my $status = $? >> 8;
if ($status == 127) {
subprocess_error(@_);
}
if ($status == 126) {
$status = -1;
}
return ($status, @lines);
}
# Force use of the C locale for this process and all subprocesses.
# This is necessary because subprocesses' output may be locale-
# dependent. If the C.UTF-8 locale is available, it is used,
# otherwise the plain C locale. Note that we do *not*
# 'use locale' here or anywhere else!
sub ensure_C_locale {
use POSIX qw(setlocale LC_ALL);
for my $k (keys %ENV) {
if ($k eq 'LANG' || $k eq 'LANGUAGE' || $k =~ /^LC_/) {
delete $ENV{$k};
}
}
if (defined(setlocale(LC_ALL, 'C.UTF-8'))) {
$ENV{LC_ALL} = 'C.UTF-8'; ## no critic (RequireLocalizedPunctuationVars)
} elsif (defined(setlocale(LC_ALL, 'C'))) {
$ENV{LC_ALL} = 'C'; ## no critic (RequireLocalizedPunctuationVars)
} else {
error("could not set 'C' locale: $!");
}
return;
}
# Close standard input at the OS level and reopen it on /dev/null.
# This ensures that no subprocesses will get stuck trying to read from
# standard input.
sub ensure_empty_stdin {
use POSIX qw(open close dup2 O_RDONLY);
my $fd = open('/dev/null', O_RDONLY) // die "open('/dev/null'): $!\n";
dup2($fd, 0) // die("dup2($fd, 0): $!\n");
close($fd);
}
# Clean up $ENV{PATH}, and return the cleaned path as a list.
sub clean_PATH {
state @path;
if (!@path) {
for my $d (path()) {
# Discard all entries that are not absolute paths.
next unless file_name_is_absolute($d);
# Discard all entries that are not directories, or don't
# exist. (This is not just for tidiness; realpath()
# behaves unpredictably if called on a nonexistent
# pathname.)
next unless -d $d;
# Resolve symlinks in all remaining entries.
$d = realpath($d);
# Discard duplicates.
push @path, $d unless grep { $_ eq $d } @path;
}
error('nothing left after cleaning PATH')
unless @path;
# File::Spec knows internally whether $PATH is colon-separated
# or semicolon-separated, but it won't tell us. Assume it's
# colon-separated unless the first element of $PATH has a
# colon in it (and is therefore probably a DOS-style absolute
# path, with a drive letter).
my $newpath;
if ($path[0] =~ /:/) {
$newpath = join ';', @path;
} else {
$newpath = join ':', @path;
}
$ENV{PATH} = $newpath; ## no critic (RequireLocalizedPunctuationVars)
}
return @path;
}
# Locate a program that we need.
# $_[0] is the name of the program along with any options that are
# required to use it correctly. Split this into an argument list,
# exactly as /bin/sh would do it, and then search $PATH for the
# executable. If we find it, return a list whose first element is
# the absolute pathname of the executable, followed by any options.
# Otherwise return an empty list.
sub which {
my ($command) = @_;
my @PATH = clean_PATH();
# Split the command name from any options attached to it.
my ($cmd, @options) = sh_split($command);
my ($vol, $path, $file) = splitpath($cmd);
if ($file eq 'false') {
# Special case: the command 'false' is never considered to be
# available. Autoconf sets config variables like $CC and $NM to
# 'false' if it can't find the requested tool.
return ();
} elsif ($file ne $cmd) {
# $cmd was not a bare filename. Do not do path search, but do
# verify that $cmd exists and is executable, then convert it
# to a canonical absolute path.
#
# Note: the result of realpath() is unspecified if its
# argument does not exist, so we must test its existence
# first.
#
# Note: if $file is a symlink, we must *not* resolve that
# symlink, because that may change the name of the program,
# which in turn may change what the program does.
# For instance, suppose $CC is /usr/lib/ccache/cc, and this
# 'cc' is a symlink to /usr/bin/ccache. Resolving the symlink
# will cause ccache to be invoked as 'ccache' instead of 'cc'
# and it will error out because it's no longer being told
# it's supposed to run the compiler.
if (-f -x $cmd) {
return (catfile(realpath(catpath($vol, $path, q{})), $file),
@options);
} else {
return ();
}
} else {
for my $d (@PATH) {
my $cand = catfile($d, $cmd);
if (-f -x $cand) {
# @PATH came from clean_PATH, so all of the directories
# have already been canonicalized. If the last element
# of $cand is a symlink, we should *not* resolve it (see
# above). Therefore, we do not call realpath here.
return ($cand, @options);
}
}
return ();
}
}
1;