/* See LICENSE file for copyright and license details. */
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/**
* The name of the process, used in error messages
*/
static const char *argv0 = "makeenv";
/**
* Options found in .makeenv and in the command line
*/
static const char **options = NULL;
/**
* Macros found in .makeenv and in the command line
*/
static const char **macros = NULL;
/**
* Targets found in .makeenv and in the command line
*/
static const char **targets = NULL;
/**
* Number of elements in `.options`
*/
static size_t noptions = 0;
/**
* Number of elements in `.macros`
*/
static size_t nmacros = 0;
/**
* Number of elements in `.targets`
*/
static size_t ntargets = 0;
/**
* Number of elements allocated to `.options`
*/
static size_t options_size = 0;
/**
* Number of elements allocated to `.macros`
*/
static size_t macros_size = 0;
/**
* Number of elements allocated to `.targets`
*/
static size_t targets_size = 0;
/**
* Add an option to the list of options
*
* @param s The option to add
*/
static void
put_option(const char *s)
{
if (noptions == options_size) {
options_size += 16;
options = realloc(options, options_size * sizeof(*options));
if (!options) {
fprintf(stderr, "%s: failed to allocate enough memory to load .makeenv\n", argv0);
exit(125);
}
}
options[noptions++] = s;
}
/**
* Add an option to the list of options
*
* @param c The letter of the option to add, for example 'x' for "-x"
*/
static void
put_option_short(int c)
{
char *opt = malloc(sizeof("-x"));
if (!opt) {
fprintf(stderr, "%s: failed to allocate enough memory to load .makeenv\n", argv0);
exit(125);
}
opt[0] = '-';
opt[1] = (char)c;
opt[2] = '\0';
put_option(opt);
}
/**
* Add an option to the list of options
*
* @param s The option to add, it will be prefixed with a dash ("-")
*/
static void
put_option_prefix_dash(const char *s)
{
size_t len = strlen(s);
char *arg = malloc(sizeof("-") + len);
if (!arg) {
fprintf(stderr, "%s: failed to allocate enough memory to load .makeenv\n", argv0);
exit(125);
}
stpcpy(stpcpy(arg, "-"), s);
put_option(arg);
}
/**
* Add all options from a list to the list of options
*
* @parm s The list of options to add; elements are separated by spaces
*/
static void
put_options(char *s)
{
const char *word = s;
for (; *s; s++) {
if (isspace(*s)) {
*s = '\0';
if (*word)
put_option(word);
word = &s[1];
}
}
if (*word)
put_option(word);
}
/**
* Add a macro to the list of macros and to environ(3)
*
* This function should not be called directly when adding
* macros from the command line; use `put_operand` instead
* as it will detect and warn about mixing targets and macros
*
* @param s The macro to add
*/
static void
put_macro(const char *s)
{
if (putenv(*(char **)(void *)&s)) {
fprintf(stderr, "%s: failed to update environment: %s\n", argv0, strerror(errno));
exit(125);
}
if (nmacros == macros_size) {
macros_size += 16;
macros = realloc(macros, macros_size * sizeof(*macros));
if (!macros) {
fprintf(stderr, "%s: failed to allocate enough memory to load .makeenv\n", argv0);
exit(125);
}
}
macros[nmacros++] = s;
}
/**
* Add a target to the list of targets
*
* This function should not be called directly when adding
* targets from the command line; use `put_operand` instead
* as it will detect and warn about mixing targets and macros
*
* @param s The target to add
*/
static void
put_target(const char *s)
{
if (ntargets == targets_size) {
targets_size += 16;
targets = realloc(targets, targets_size * sizeof(*targets));
if (!targets) {
fprintf(stderr, "%s: failed to allocate enough memory to load .makeenv\n", argv0);
exit(125);
}
}
targets[ntargets++] = s;
}
/**
* Add all targets from a list to the list of targets
*
* @param s The list of targets to add; elements are separated by spaces
*/
static void
put_targets(char *s)
{
const char *word = s;
for (; *s; s++) {
if (isspace(*s)) {
*s = '\0';
if (*word)
put_target(word);
word = &s[1];
}
}
if (*word)
put_target(word);
}
/**
* Add an operand to either the list of macros or the list of targets,
* depending on the classification of the operand
*
* @param s The operand to add
*/
static void
put_operand(char *s)
{
/*
* Behaviour is unspecified if any target is specified before
* a macro, however make(1) shall allow options to be mixed in
* with both macros and targets
*/
static int warned_mixed = 0;
static int found_targets = 0;
if (strchr(s, '=')) {
/*
* Adding macro as a target as we don't want it to be
* but into the environment and we don't want to reorder
* them if the user mixes macros with targets (targets
* ared pulled into the command line after macros so
* this will not cause any problems)
*/
put_target(s);
if (found_targets && !warned_mixed) {
warned_mixed = 1;
fprintf(stderr, "%s: warning: mixing targets and macros in the "
"command line results in unspecified behaviour\n", argv0);
}
} else {
put_target(s);
found_targets = 1;
}
}
/**
* Check if a string appears as a word in another string
*
* @param set The string to search in; words are separated by spaces
* @param sought The string to search for
* @return 1 if the string is found, 0 otherwise
*/
#if defined(__GNUC__)
__attribute__((__pure__))
#endif
static int
contains(const char *set, const char *sought)
{
size_t m, n = strlen(sought);
const char *p, *q;
for (p = set; (q = strchr(p, ' ')); p = &q[1]) {
m = (size_t)(q - p);
if (m == n && !strncmp(p, sought, n))
return 1;
}
return !strcmp(p, sought);
}
/**
* Create the argument list for for make(1)
*
* The arguments will be listed in the order required by make(1posix):
* targets follow macros, however also, with POSIX.1-2017, Section 12.2,
* Utility Syntax Guidelines, except for Guideline 9 enforced: operands
* (macros and targets) follow options. The list will also begin with
* and unset element where the process name for make(1) can be placed.
*
* The function will insert a "--" argument to separate options from
* operands, to ensure it is put in a position, which does not cause
* any problems, due to argument reordering, when the user has added
* a "--" argument. The "--" argument will be added regardless of
* whether the user added a "--" argument; in fact any "--" argument
* from the user shall be skipped unless it can be determined that
* it is a target ranther than and option-list ender.
*
* @return `NULL`-terminated list of arguments
*/
static const char **
create_cmdline(void)
{
size_t i, j = 1, n = noptions + nmacros + ntargets + 3;
const char **args = malloc(n * sizeof(char *));
if (!args) {
fprintf(stderr, "%s: failed to allocate enough memory to execute make\n", argv0);
exit(125);
}
for (i = 0; i < noptions; i++)
args[j++] = options[i];
args[j++] = "--";
for (i = 0; i < nmacros; i++)
args[j++] = macros[i];
for (i = 0; i < ntargets; i++)
args[j++] = targets[i];
args[j] = NULL;
return args;
}
/**
* Trim leading and trailing whitespace from a string
*
* @param s The string to trim
* @return The trimmed string
*/
static char *
trim(char *s)
{
size_t i, last = (size_t)-1;
while (isspace(*s))
s++;
for (i = 0; s[i]; i++)
if (!isspace(s[i]))
last = i;
s[last + 1] = '\0';
return s;
}
/**
* Trim whitespace around an equals sign in a string
*
* @param s The string to trim, will be updated
*/
static void
trim_around_equals(char *s)
{
char *equals = strchr(s, '=');
char *p = equals, *val = &equals[1];
while (p != s && isspace(p[-1]))
p--;
*p++ = '=';
while (isspace(*val))
val++;
if (val != p)
memmove(p, val, strlen(val) + 1U);
}
/**
* Get an environment variable or a default value
*
* @param var The name of the environment variable
* @param def The default value to return if the environment variable is not set
* @return The value of the environment variable, or the default value if unset
*/
static const char *
get(const char *var, const char *def)
{
const char *ret = getenv(var);
return ret ? ret : def;
}
int
main(int argc, char *argv[])
{
int exitstatus, fd;
char *env = NULL;
size_t envsize = 0;
size_t envlen = 0;
ssize_t r;
size_t i;
char *line;
int has_equals;
const char **args;
const char *arg;
const char *unarged_opts;
const char *arged_opts;
const char *optatarged_opts;
const char *optarged_opts;
const char *unarged_longopts;
const char *arged_longopts;
const char *optarged_longopts;
int operand_found = 0;
int warned_reordered = 0;
args = (void *)argv;
argv0 = *argv++;
(void) argc;
/* Open .makeenv */
fd = open(".makeenv", O_RDONLY);
if (fd < 0) {
if (errno == ENOENT)
goto exec;
fprintf(stderr, "%s: failed to open .makeenv for reading: %s\n", argv0, strerror(errno));
return 125;
}
/* Read .makeenv */
for (;;) {
if (envlen == envsize) {
envsize += 8096;
env = realloc(env, envsize);
if (!env) {
fprintf(stderr, "%s: failed to allocate enough memory to load .makeenv\n", argv0);
return 125;
}
}
r = read(fd, &env[envlen], envsize - envlen);
if (r <= 0) {
if (!r)
break;
fprintf(stderr, "%s: failed to read .makeenv\n", argv0);
return 125;
}
envlen += (size_t)r;
}
/* If .makeenv is empty, use make(1) without any additional arguments */
if (!envlen) {
close(fd);
goto exec;
}
/* Ensure .makeenv is LF-terminated */
env = realloc(env, envlen + 1);
if (!env) {
fprintf(stderr, "%s: failed to allocate enough memory to load .makeenv\n", argv0);
return 125;
}
env[envlen++] = '\n';
/* Close .makeenv */
close(fd);
/* Add options, macros, and targets from .makeenv */
line = env;
has_equals = 0;
for (i = 0; i < envlen; i++) {
if (env[i] == '\n') {
env[i] = '\0';
line = trim(line);
if (line[0] == '-' && line[1]) {
put_options(line);
} else if (has_equals) {
trim_around_equals(line);
if (*line == '=') {
fprintf(stderr, "%s: empty variable name listed in .makeenv\n", argv0);
return 125;
} else if (*line != '#') {
put_macro(line);
}
} else {
if (*line && *line != '#')
put_targets(line);
}
line = &env[i + 1];
has_equals = 0;
} else if (env[i] == '=') {
has_equals = 1;
}
}
/* Get option syntax from the environment */
unarged_opts = get("MAKEENV_OPTS_NO_ARG", "eiknpqrSst");
arged_opts = get("MAKEENV_OPTS_ARG", "fW");
optatarged_opts = get("MAKEENV_OPTS_OPT_ATTACHED_ARG", "");
optarged_opts = get("MAKEENV_OPTS_OPT_ARG", "");
unarged_longopts = get("MAKEENV_LONG_OPTS_NO_ARG", "");
arged_longopts = get("MAKEENV_LONG_OPTS_ARG", "");
optarged_longopts = get("MAKEENV_LONG_OPTS_OPT_ARG", "");
/* Add arguments from the command line */
for (; *argv; argv++) {
/* Stop processing options when "--" is found */
if (!strcmp(*argv, "--")) {
argv++;
break;
}
/* Add operands listed before "--", but do not stop processing subsequent arguments as options */
if ((*argv)[0] != '-' || !(*argv)[1]) {
/* do not break, as make(1) allows mixing options and operands */
put_operand(*argv);
operand_found = 1;
continue;
}
/* Warn when reordering operands to be placed after options */
if (operand_found && !warned_reordered) {
warned_reordered = 1;
fprintf(stderr, "%s: warning: reordering operands to after options\n", argv0);
}
/* Add options */
if ((*argv)[1] == '-') {
/* Long option */
arg = *argv;
if (strchr(arg, '=') || contains(unarged_longopts, arg)) {
/* The option either has an attached argument (so it's always safe to
* add) or it is recognised to never have an argument */
put_option(arg);
} else if (contains(arged_longopts, arg)) {
/* Long option with detached argument */
put_option(arg);
if (!argv[1]) {
fprintf(stderr, "%s: argument for option %s missing\n", argv0, arg);
return 125;
}
put_option(*++argv);
} else if (contains(optarged_longopts, arg)) {
/* Long option with either detached argument or no argument */
put_option(arg);
if (argv[1] && argv[1][0] != '-') {
/* The option has a detached argument */
put_option(*++argv);
}
} else {
fprintf(stderr, "%s: option %s not recognised\n", argv0, arg);
return 125;
}
} else {
/* Short options */
arg = &(*argv)[1];
while (*arg) {
if (strchr(unarged_opts, *arg)) {
/* Option cannot have an argument */
put_option_short(*arg++);
} else if (strchr(arged_opts, *arg)) {
/* Option must have an argument */
if (arg[1]) {
/* Argument is attached to option */
put_option_short(*arg++);
put_option(arg);
break;
} else if (argv[1]) {
/* Argument is detached from option*/
put_option_short(*arg++);
put_option(*++argv);
break;
} else {
fprintf(stderr, "%s: argument for option -%c missing\n", argv0, *arg);
return 125;
}
} else if (strchr(optatarged_opts, *arg)) {
/* Option may have an attached argument, but it cannot have a detached argument */
put_option_prefix_dash(arg);
break;
} else if (strchr(optarged_opts, *arg)) {
/* Option may have an attached or detached argument */
if (arg[1]) {
/* Argument is attached to option */
put_option_prefix_dash(arg);
} else {
/* Either there is no argument, or it is detached from the option */
put_option_short(*arg++);
if (argv[1] && argv[1][0] != '-') {
/* Argument exist and is detached. We assume that if the next
* argument in the command line is the option's argument unless
* it starts with '-'; however some implementations can be more
* intelligent about determining what is an option argument and
* what is an operands. This means that in some cases makeenv(1)
* would require the user to attach the argument even when their
* implementation of make(1) does not require this. */
put_option(*++argv);
}
}
break;
} else {
fprintf(stderr, "%s: option -%c not recognised\n", argv0, *arg);
return 125;
}
}
}
}
/* Add operands listed after "--" */
for (; *argv; argv++)
put_operand(*argv);
/* Create the arguments for make(1); the zeroth argument will be unset and to be filled in later */
args = create_cmdline();
exec:
/* Execute make(1) */
args[0] = get("MAKEENV_MAKE", get("MAKE", "make"));
execvp(args[0], (void *)args);
exitstatus = errno == ENOENT ? 127 : 126;
fprintf(stderr, "%s: failed to execute %s: %s\n", argv0, args[0], strerror(errno));
return exitstatus;
}