Subject: make: adding .export
To: None <tech-toolchain@netbsd.org>
From: Simon Gerraty <sjg@juniper.net>
List: tech-toolchain
Date: 10/02/2007 11:15:21
The patch below, adds a .export facility which is pretty analagous to
gmake's.  It is often handy to set macros for tools like PERL, and
export them to all children.

Actual export of vars that have '$' in their values is delayed until
we fork a child, while a bit expensive in compat mode, this ensures
accurate results and minimizes the ability of setenv to leak memory in
the parent.

--sjg

Index: compat.c
===================================================================
RCS file: /cvsroot/src/usr.bin/make/compat.c,v
retrieving revision 1.67
diff -u -p -r1.67 compat.c
--- compat.c	5 Apr 2007 14:11:35 -0000	1.67
+++ compat.c	2 Oct 2007 18:09:19 -0000
@@ -354,6 +354,7 @@ CompatRunCommand(ClientData cmdp, Client
     }
     if (cpid == 0) {
 	Check_Cwd(av);
+	Var_ExportVars();
 	if (local)
 	    (void)execvp(av[0], (char *const *)UNCONST(av));
 	else
Index: job.c
===================================================================
RCS file: /cvsroot/src/usr.bin/make/job.c,v
retrieving revision 1.125
diff -u -p -r1.125 job.c
--- job.c	1 Oct 2007 22:14:09 -0000	1.125
+++ job.c	2 Oct 2007 18:09:19 -0000
@@ -1343,6 +1343,8 @@ JobExec(Job *job, char **argv)
 	(void)setpgid(0, getpid());
 #endif
 
+	Var_ExportVars();
+
 	(void)execv(shellPath, argv);
 	execError("exec", shellPath);
 	_exit(1);
@@ -2057,14 +2059,14 @@ Shell_GetNewline(void)
 void
 Job_SetPrefix(void)
 {
-    char tmp[sizeof("${" MAKEJOBPREFIX "}") + 1];
+    char tmp[sizeof("${" MAKE_JOB_PREFIX "}") + 1];
     
     if (targPrefix) {
 	free(targPrefix);
-    } else if (!Var_Exists(MAKEJOBPREFIX, VAR_GLOBAL)) {
-	Var_Set(MAKEJOBPREFIX, "---", VAR_GLOBAL, 0);
+    } else if (!Var_Exists(MAKE_JOB_PREFIX, VAR_GLOBAL)) {
+	Var_Set(MAKE_JOB_PREFIX, "---", VAR_GLOBAL, 0);
     }
-    strncpy(tmp, "${" MAKEJOBPREFIX "}", sizeof(tmp));
+    strncpy(tmp, "${" MAKE_JOB_PREFIX "}", sizeof(tmp));
     targPrefix = Var_Subst(NULL, tmp, VAR_GLOBAL, 0);
 }
 
Index: main.c
===================================================================
RCS file: /cvsroot/src/usr.bin/make/main.c,v
retrieving revision 1.142
diff -u -p -r1.142 main.c
--- main.c	1 Oct 2007 22:14:10 -0000	1.142
+++ main.c	2 Oct 2007 18:09:19 -0000
@@ -1459,6 +1459,8 @@ Cmd_Exec(const char *cmd, const char **e
 	(void)dup2(fds[1], 1);
 	(void)close(fds[1]);
 
+	Var_ExportVars();
+
 	(void)execv(shellPath, UNCONST(args));
 	_exit(1);
 	/*NOTREACHED*/
Index: make.1
===================================================================
RCS file: /cvsroot/src/usr.bin/make/make.1,v
retrieving revision 1.132
diff -u -p -r1.132 make.1
--- make.1	1 Oct 2007 22:14:10 -0000	1.132
+++ make.1	2 Oct 2007 18:09:19 -0000
@@ -1182,6 +1182,18 @@ Conditional expressions are also precede
 character of a line.
 The possible conditionals are as follows:
 .Bl -tag -width Ds
+.It Ic .export Ar variable
+Export the specified global variable.
+If no variable is provided, all globals are exported
+except for internal variables (those that start with
+.Ql .
+).
+This is not affected by the
+.Fl X
+flag, so should be used with caution.
+Appending a variable name to
+.Va .MAKE.EXPORTED
+is equivalent to exporting a variable.
 .It Ic .undef Ar variable
 Un-define the specified global variable.
 Only global variables may be un-defined.
Index: make.h
===================================================================
RCS file: /cvsroot/src/usr.bin/make/make.h,v
retrieving revision 1.68
diff -u -p -r1.68 make.h
--- make.h	1 Oct 2007 22:14:11 -0000	1.68
+++ make.h	2 Oct 2007 18:09:19 -0000
@@ -397,7 +397,8 @@ extern char	*progname;	/* The program na
 
 #define	MAKEFLAGS	".MAKEFLAGS"
 #define	MAKEOVERRIDES	".MAKEOVERRIDES"
-#define	MAKEJOBPREFIX	".MAKE.JOB.PREFIX"
+#define	MAKE_JOB_PREFIX	".MAKE.JOB.PREFIX"
+#define	MAKE_EXPORTED	".MAKE.EXPORTED"
 
 /*
  * debug control:
Index: nonints.h
===================================================================
RCS file: /cvsroot/src/usr.bin/make/nonints.h,v
retrieving revision 1.43
diff -u -p -r1.43 nonints.h
--- nonints.h	4 Feb 2007 19:23:49 -0000	1.43
+++ nonints.h	2 Oct 2007 18:09:19 -0000
@@ -197,3 +197,5 @@ char *Var_GetHead(const char *);
 void Var_Init(void);
 void Var_End(void);
 void Var_Dump(GNode *);
+void Var_ExportVars(void);
+void Var_Export(char *, int);
Index: parse.c
===================================================================
RCS file: /cvsroot/src/usr.bin/make/parse.c,v
retrieving revision 1.135
diff -u -p -r1.135 parse.c
--- parse.c	1 Oct 2007 22:14:11 -0000	1.135
+++ parse.c	2 Oct 2007 18:09:20 -0000
@@ -1614,8 +1614,10 @@ Parse_DoVar(char *line, GNode *ctxt)
 	 */
 	Dir_InitCur(cp);
 	Dir_SetPATH();
-    } else if (strcmp(line, MAKEJOBPREFIX) == 0) {
+    } else if (strcmp(line, MAKE_JOB_PREFIX) == 0) {
 	Job_SetPrefix();
+    } else if (strcmp(line, MAKE_EXPORTED) == 0) {
+	Var_Export(cp, 0);
     }
     if (freeCp)
 	free(cp);
@@ -2435,6 +2437,11 @@ Parse_File(const char *name, int fd)
 		    *cp2 = '\0';
 		    Var_Delete(cp, VAR_GLOBAL);
 		    continue;
+		} else if (strncmp(cp, "export", 6) == 0) {
+		    for (cp += 6; isspace((unsigned char) *cp); cp++)
+			continue;
+		    Var_Export(cp, 1);
+		    continue;
 		}
 	    }
 
Index: var.c
===================================================================
RCS file: /cvsroot/src/usr.bin/make/var.c,v
retrieving revision 1.117
diff -u -p -r1.117 var.c
--- var.c	16 Jun 2007 19:47:29 -0000	1.117
+++ var.c	2 Oct 2007 18:09:20 -0000
@@ -187,8 +187,24 @@ typedef struct Var {
 #define VAR_KEEP	8	    /* Variable is VAR_JUNK, but we found
 				     * a use for it in some modifier and
 				     * the value is therefore valid */
+#define VAR_EXPORTED	16 	    /* Variable is exported */
+#define VAR_REEXPORT	32	    /* Indicate if var needs re-export.
+				     * This would be true if it contains $'s
+				     */
 }  Var;
 
+/*
+ * Exporting vars is expensive so skip it if we can
+ */
+#define VAR_EXPORTED_NONE	0
+#define VAR_EXPORTED_YES	1
+#define VAR_EXPORTED_ALL	2
+static int var_exportedVars = VAR_EXPORTED_NONE;
+/*
+ * We pass this to Var_Export when doing the initial export
+ * or after updating an exported var.
+ */
+#define VAR_EXPORT_FORCE 1
 
 /* Var*Pattern flags */
 #define VAR_SUB_GLOBAL	0x01	/* Apply substitution globally */
@@ -509,6 +525,9 @@ Var_Delete(const char *name, GNode *ctxt
 	Var 	  *v;
 
 	v = (Var *)Hash_GetValue(ln);
+	if ((v->flags & VAR_EXPORTED)) {
+	    unsetenv(v->name);
+	}
 	if (v->name != ln->name)
 		free(v->name);
 	Hash_DeleteEntry(&ctxt->context, ln);
@@ -517,6 +536,177 @@ Var_Delete(const char *name, GNode *ctxt
     }
 }
 
+
+/*
+ * Export a var.
+ * We ignore make internal variables (those which start with '.')
+ * Also we jump through some hoops to avoid calling setenv
+ * more than necessary since it can leak.
+ */
+static int
+Var_Export1(const char *name, int force)
+{
+    char tmp[BUFSIZ];
+    Var *v;
+    char *val = NULL;
+    int n;
+
+    if (*name == '.')
+	return 0;			/* skip internals */
+    if (!name[1]) {
+	/*
+	 * A single char.
+	 * If it is one of the vars that should only appear in
+	 * local context, skip it, else we can get Var_Subst
+	 * into a loop.
+	 */
+	switch (name[0]) {
+	case '@':
+	case '%':
+	case '*':
+	case '!':
+	    return 0;
+	}
+    }
+    v = VarFind(name, VAR_GLOBAL, 0);
+    if (v == (Var *)NIL) {
+	return 0;
+    }
+    if (!force &&
+	(v->flags & (VAR_EXPORTED|VAR_REEXPORT)) == VAR_EXPORTED) {
+	return 0;			/* nothing to do */
+    }
+    val = (char *)Buf_GetAll(v->val, NULL);
+    if (strchr(val, '$')) {
+	/* Flag this as something we need to re-export */
+	v->flags |= (VAR_EXPORTED|VAR_REEXPORT);
+	if (force) {
+	    /*
+	     * No point actually exporting it now though,
+	     * the child can do it at the last minute.
+	     */
+	    return 1;
+	}
+	n = snprintf(tmp, sizeof(tmp), "${%s}", name);
+	if (n < sizeof(tmp)) {
+	    val = Var_Subst(NULL, tmp, VAR_GLOBAL, 0);
+	    setenv(name, val, 1);
+	    free(val);
+	}
+    } else {
+	v->flags &= ~VAR_REEXPORT;	/* once will do */
+	if (force || !(v->flags & VAR_EXPORTED)) {
+	    setenv(name, val, 1);
+	}
+    }
+    /*
+     * This is so Var_Set knows to call Var_Export again...
+     */
+    v->flags |= VAR_EXPORTED;
+    return 1;
+}
+
+/*
+ * This gets called from our children.
+ */
+void
+Var_ExportVars(void)
+{
+    char tmp[BUFSIZ];
+    Hash_Entry         	*var;
+    Hash_Search 	state;
+    Var *v;
+    char *val;
+    int n;
+
+    if (VAR_EXPORTED_NONE == var_exportedVars)
+	return;
+
+    if (VAR_EXPORTED_ALL == var_exportedVars) {
+	/*
+	 * Ouch! This is crazy...
+	 */
+	for (var = Hash_EnumFirst(&VAR_GLOBAL->context, &state);
+	     var != NULL;
+	     var = Hash_EnumNext(&state)) {
+	    v = (Var *)Hash_GetValue(var);
+	    Var_Export1(v->name, 0);
+	}
+	return;
+    }
+    /*
+     * We have a number of exported vars,
+     */
+    n = snprintf(tmp, sizeof(tmp), "${" MAKE_EXPORTED ":O:u}");
+    if (n < sizeof(tmp)) {
+	char **av;
+	char *as;
+	int ac;
+	int i;
+	
+	val = Var_Subst(NULL, tmp, VAR_GLOBAL, 0);
+	av = brk_string(val, &ac, FALSE, &as);
+	for (i = 0; i < ac; i++) {
+	    Var_Export1(av[i], 0);
+	}
+	free(val);
+	free(as);
+	free(av);
+    }
+}
+
+/*
+ * This is called when .export is seen or
+ * .MAKE.EXPORTED is modified.
+ * It is also called when any exported var is modified.
+ */
+void
+Var_Export(char *str, int isExport)
+{
+    char *name;
+    char *val;
+    char **av;
+    char *as;
+    int ac;
+    int i;
+
+    if (isExport && (!str || !str[0])) {
+	var_exportedVars = VAR_EXPORTED_ALL; /* use with caution! */
+	return;
+    }
+
+    val = Var_Subst(NULL, str, VAR_GLOBAL, 0);
+    av = brk_string(val, &ac, FALSE, &as);
+    for (i = 0; i < ac; i++) {
+	name = av[i];
+	if (!name[1]) {
+	    /*
+	     * A single char.
+	     * If it is one of the vars that should only appear in
+	     * local context, skip it, else we can get Var_Subst
+	     * into a loop.
+	     */
+	    switch (name[0]) {
+	    case '@':
+	    case '%':
+	    case '*':
+	    case '!':
+		continue;
+	    }
+	}
+	if (Var_Export1(name, VAR_EXPORT_FORCE)) {
+	    if (VAR_EXPORTED_ALL != var_exportedVars)
+		var_exportedVars = VAR_EXPORTED_YES;
+	    if (isExport) {
+		Var_Append(MAKE_EXPORTED, name, VAR_GLOBAL);
+	    }
+	}
+    }
+    free(val);
+    free(as);
+    free(av);
+}
+
 /*-
  *-----------------------------------------------------------------------
  * Var_Set --
@@ -568,6 +758,9 @@ Var_Set(const char *name, const char *va
 	if (DEBUG(VAR)) {
 	    fprintf(debug_file, "%s:%s = %s\n", ctxt->name, name, val);
 	}
+	if ((v->flags & VAR_EXPORTED)) {
+	    Var_Export1(name, VAR_EXPORT_FORCE);
+	}
     }
     /*
      * Any variables given on the command line are automatically exported
Index: unit-tests/Makefile
===================================================================
RCS file: /cvsroot/src/usr.bin/make/unit-tests/Makefile,v
retrieving revision 1.20
diff -u -p -r1.20 Makefile
--- unit-tests/Makefile	11 May 2006 15:37:07 -0000	1.20
+++ unit-tests/Makefile	2 Oct 2007 18:09:20 -0000
@@ -21,6 +21,8 @@ UNIT_TESTS:= ${.PARSEDIR}
 SUBFILES= \
 	comment \
 	cond1 \
+	export \
+	export-all \
 	dotwait \
 	moderrs \
 	modmatch \
Index: unit-tests/export
===================================================================
RCS file: unit-tests/export
diff -N unit-tests/export
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ unit-tests/export	2 Oct 2007 18:09:20 -0000
@@ -0,0 +1,16 @@
+# $Id$
+
+UT_TEST=export
+UT_FOO=foo${BAR}
+UT_FU=fubar
+UT_ZOO=hoopie
+UT_NO=all
+
+.export UT_FU UT_FOO
+
+BAR=bar is ${UT_FU}
+
+.MAKE.EXPORTED+= UT_ZOO UT_TEST
+
+all:
+	@env | grep '^UT_' | sort
Index: unit-tests/export-all
===================================================================
RCS file: unit-tests/export-all
diff -N unit-tests/export-all
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ unit-tests/export-all	2 Oct 2007 18:09:20 -0000
@@ -0,0 +1,11 @@
+# $Id$
+
+UT_OK=good
+UT_F=fine
+
+.export
+
+.include "export"
+
+UT_TEST=export-all
+UT_ALL=even this gets exported
Index: unit-tests/test.exp
===================================================================
RCS file: /cvsroot/src/usr.bin/make/unit-tests/test.exp,v
retrieving revision 1.19
diff -u -p -r1.19 test.exp
--- unit-tests/test.exp	11 May 2006 18:48:33 -0000	1.19
+++ unit-tests/test.exp	2 Oct 2007 18:09:20 -0000
@@ -20,6 +20,18 @@ Passed:
 4 is not prime
 5 is  prime
 
+UT_FOO=foobar is fubar
+UT_FU=fubar
+UT_TEST=export
+UT_ZOO=hoopie
+UT_ALL=even this gets exported
+UT_F=fine
+UT_FOO=foobar is fubar
+UT_FU=fubar
+UT_NO=all
+UT_OK=good
+UT_TEST=export-all
+UT_ZOO=hoopie
 simple.1
 simple.1
 simple.2
@@ -88,10 +100,6 @@ paths=/bin /usr/bin /sbin /usr/sbin /hom
 PATHS=/BIN /USR/BIN /SBIN /USR/SBIN /HOMES/USER/BIN /OPT/XBIN
 LIST      = one two three four five six seven eigth nine ten
 LIST:O    = eigth five four nine one seven six ten three two
-# Note that 1 in every 10! trials two independently generated
-# randomized orderings will be the same.  The test framework doesn't
-# support checking probabilistic output, so we accept that the test
-# will incorrectly fail with probability 2.8E-7.
 LIST:Ox   = Ok
 LIST:O:Ox = Ok
 LISTX     = Ok