Module Name:    src
Committed By:   rillig
Date:           Mon Jun 19 12:53:57 UTC 2023

Modified Files:
        src/usr.bin/make: cond.c make.h parse.c
        src/usr.bin/make/unit-tests: directive-include-guard.exp
            directive-include-guard.mk

Log Message:
make: if a makefile is protected by a guard, only include it once

"looks reasonable" sjg@


To generate a diff of this commit:
cvs rdiff -u -r1.346 -r1.347 src/usr.bin/make/cond.c
cvs rdiff -u -r1.321 -r1.322 src/usr.bin/make/make.h
cvs rdiff -u -r1.699 -r1.700 src/usr.bin/make/parse.c
cvs rdiff -u -r1.3 -r1.4 \
    src/usr.bin/make/unit-tests/directive-include-guard.exp
cvs rdiff -u -r1.4 -r1.5 \
    src/usr.bin/make/unit-tests/directive-include-guard.mk

Please note that diffs are not public domain; they are subject to the
copyright notices on the relevant files.

Modified files:

Index: src/usr.bin/make/cond.c
diff -u src/usr.bin/make/cond.c:1.346 src/usr.bin/make/cond.c:1.347
--- src/usr.bin/make/cond.c:1.346	Fri Jun 16 07:12:46 2023
+++ src/usr.bin/make/cond.c	Mon Jun 19 12:53:57 2023
@@ -1,4 +1,4 @@
-/*	$NetBSD: cond.c,v 1.346 2023/06/16 07:12:46 rillig Exp $	*/
+/*	$NetBSD: cond.c,v 1.347 2023/06/19 12:53:57 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -92,7 +92,7 @@
 #include "dir.h"
 
 /*	"@(#)cond.c	8.2 (Berkeley) 1/2/94"	*/
-MAKE_RCSID("$NetBSD: cond.c,v 1.346 2023/06/16 07:12:46 rillig Exp $");
+MAKE_RCSID("$NetBSD: cond.c,v 1.347 2023/06/19 12:53:57 rillig Exp $");
 
 /*
  * Conditional expressions conform to this grammar:
@@ -1123,6 +1123,7 @@ Cond_EvalLine(const char *line)
 
 		/* Return state for previous conditional */
 		cond_depth--;
+		Parse_GuardEndif();
 		return cond_states[cond_depth] & IFS_ACTIVE
 		    ? CR_TRUE : CR_FALSE;
 	}
@@ -1150,6 +1151,7 @@ Cond_EvalLine(const char *line)
 				Parse_Error(PARSE_FATAL, "if-less else");
 				return CR_TRUE;
 			}
+			Parse_GuardElse();
 
 			state = cond_states[cond_depth];
 			if (state == IFS_INITIAL) {
@@ -1185,6 +1187,7 @@ Cond_EvalLine(const char *line)
 			Parse_Error(PARSE_FATAL, "if-less elif");
 			return CR_TRUE;
 		}
+		Parse_GuardElse();
 		state = cond_states[cond_depth];
 		if (state & IFS_SEEN_ELSE) {
 			Parse_Error(PARSE_WARNING, "extra elif");
@@ -1234,6 +1237,57 @@ Cond_EvalLine(const char *line)
 	return res;
 }
 
+static bool
+skip_identifier(const char **pp)
+{
+	const char *p = *pp;
+
+	if (ch_isalpha(*p) || *p == '_') {
+		while (ch_isalnum(*p) || *p == '_')
+			p++;
+		*pp = p;
+		return true;
+	}
+	return false;
+}
+
+/*
+ * Tests whether the line is a conditional that forms a multiple-inclusion
+ * guard, and if so, extracts the guard variable name.
+ */
+char *
+Cond_ExtractGuard(const char *line)
+{
+	const char *p = line, *dir, *varname;
+	size_t dir_len;
+
+	if (!skip_string(&p, "."))
+		return NULL;
+	cpp_skip_hspace(&p);
+
+	dir = p;
+	while (ch_isalpha(*p))
+		p++;
+	dir_len = (size_t)(p - dir);
+	cpp_skip_hspace(&p);
+
+	if (dir_len == 2 && memcmp(dir, "if", 2) == 0) {
+		if (!skip_string(&p, "!defined("))
+			return NULL;
+		varname = p;
+		skip_identifier(&p);
+		if (p > varname && strcmp(p, ")") == 0)
+			return bmake_strsedup(varname, p);
+	}
+	if (dir_len == 6 && memcmp(dir, "ifndef", 6) == 0) {
+		varname = p;
+		skip_identifier(&p);
+		if (p > varname && *p == '\0')
+			return bmake_strsedup(varname, p);
+	}
+	return NULL;
+}
+
 void
 Cond_EndFile(void)
 {

Index: src/usr.bin/make/make.h
diff -u src/usr.bin/make/make.h:1.321 src/usr.bin/make/make.h:1.322
--- src/usr.bin/make/make.h:1.321	Fri Jun 16 07:12:46 2023
+++ src/usr.bin/make/make.h	Mon Jun 19 12:53:57 2023
@@ -1,4 +1,4 @@
-/*	$NetBSD: make.h,v 1.321 2023/06/16 07:12:46 rillig Exp $	*/
+/*	$NetBSD: make.h,v 1.322 2023/06/19 12:53:57 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990, 1993
@@ -793,6 +793,7 @@ void Compat_Make(GNode *, GNode *);
 extern unsigned int cond_depth;
 CondResult Cond_EvalCondition(const char *) MAKE_ATTR_USE;
 CondResult Cond_EvalLine(const char *) MAKE_ATTR_USE;
+char *Cond_ExtractGuard(const char *) MAKE_ATTR_USE;
 void Cond_EndFile(void);
 
 /* dir.c; see also dir.h */
@@ -857,6 +858,8 @@ void Parse_PushInput(const char *, unsig
 void Parse_MainName(GNodeList *);
 int Parse_NumErrors(void) MAKE_ATTR_USE;
 unsigned int CurFile_CondMinDepth(void) MAKE_ATTR_USE;
+void Parse_GuardElse(void);
+void Parse_GuardEndif(void);
 
 
 /* suff.c */

Index: src/usr.bin/make/parse.c
diff -u src/usr.bin/make/parse.c:1.699 src/usr.bin/make/parse.c:1.700
--- src/usr.bin/make/parse.c:1.699	Thu Jun  1 06:25:34 2023
+++ src/usr.bin/make/parse.c	Mon Jun 19 12:53:57 2023
@@ -1,4 +1,4 @@
-/*	$NetBSD: parse.c,v 1.699 2023/06/01 06:25:34 rillig Exp $	*/
+/*	$NetBSD: parse.c,v 1.700 2023/06/19 12:53:57 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990, 1993
@@ -105,7 +105,15 @@
 #include "pathnames.h"
 
 /*	"@(#)parse.c	8.3 (Berkeley) 3/19/94"	*/
-MAKE_RCSID("$NetBSD: parse.c,v 1.699 2023/06/01 06:25:34 rillig Exp $");
+MAKE_RCSID("$NetBSD: parse.c,v 1.700 2023/06/19 12:53:57 rillig Exp $");
+
+/* Detects a multiple-inclusion guard in a makefile. */
+typedef enum {
+	GS_START,		/* at the beginning of the file */
+	GS_COND,		/* after the guard condition */
+	GS_DONE,		/* after the closing .endif */
+	GS_NO			/* the file is not guarded */
+} GuardState;
 
 /*
  * A file being read.
@@ -128,6 +136,9 @@ typedef struct IncludedFile {
 	char *buf_ptr;		/* next char to be read */
 	char *buf_end;		/* buf_end[-1] == '\n' */
 
+	GuardState guard;
+	char *guardVarname;
+
 	struct ForLoop *forLoop;
 } IncludedFile;
 
@@ -299,6 +310,8 @@ static const struct {
 
 enum PosixState posix_state = PS_NOT_YET;
 
+static HashTable guards;
+
 static IncludedFile *
 GetInclude(size_t i)
 {
@@ -1212,6 +1225,7 @@ IncludeFile(const char *file, bool isSys
 {
 	Buffer buf;
 	char *fullname;		/* full pathname of file */
+	char *guardVarname;
 	int fd;
 
 	fullname = file[0] == '/' ? bmake_strdup(file) : NULL;
@@ -1231,6 +1245,14 @@ IncludeFile(const char *file, bool isSys
 		return;
 	}
 
+	guardVarname = HashTable_FindValue(&guards, fullname);
+	if (guardVarname != NULL
+	    && GNode_ValueDirect(SCOPE_GLOBAL, guardVarname) != NULL) {
+		DEBUG2(PARSE, "Skipping '%s' because '%s' is already set\n",
+		    fullname, guardVarname);
+		return;
+	}
+
 	if ((fd = open(fullname, O_RDONLY)) == -1) {
 		if (!silent)
 			Parse_Error(PARSE_FATAL, "Cannot open %s", fullname);
@@ -2174,6 +2196,8 @@ Parse_PushInput(const char *name, unsign
 	curFile->forBodyReadLines = readLines;
 	curFile->buf = buf;
 	curFile->depending = doing_depend;	/* restore this on EOF */
+	curFile->guard = forLoop == NULL ? GS_START : GS_NO;
+	curFile->guardVarname = NULL;
 	curFile->forLoop = forLoop;
 
 	if (forLoop != NULL && !For_NextIteration(forLoop, &curFile->buf))
@@ -2316,8 +2340,15 @@ ParseEOF(void)
 
 	Cond_EndFile();
 
+	if (curFile->guard == GS_DONE) {
+		HashTable_Set(&guards,
+		    curFile->name.str, curFile->guardVarname);
+		curFile->guardVarname = NULL;
+	}
+
 	FStr_Done(&curFile->name);
 	Buf_Done(&curFile->buf);
+	free(curFile->guardVarname);
 	if (curFile->forLoop != NULL)
 		ForLoop_Free(curFile->forLoop);
 	Vector_Pop(&includes);
@@ -2616,8 +2647,10 @@ static char *
 ReadHighLevelLine(void)
 {
 	char *line;
+	CondResult condResult;
 
 	for (;;) {
+		IncludedFile *curFile = CurFile();
 		line = ReadLowLevelLine(LK_NONEMPTY);
 		if (posix_state == PS_MAYBE_NEXT_LINE)
 			posix_state = PS_NOW_OR_NEVER;
@@ -2626,10 +2659,24 @@ ReadHighLevelLine(void)
 		if (line == NULL)
 			return NULL;
 
+		if (curFile->guard != GS_NO
+		    && ((curFile->guard == GS_START && line[0] != '.')
+			|| curFile->guard == GS_DONE))
+			curFile->guard = GS_NO;
 		if (line[0] != '.')
 			return line;
 
-		switch (Cond_EvalLine(line)) {
+		condResult = Cond_EvalLine(line);
+		if (curFile->guard == GS_START) {
+			char *varname;
+			if (condResult == CR_TRUE
+			    && (varname = Cond_ExtractGuard(line)) != NULL) {
+				curFile->guard = GS_COND;
+				curFile->guardVarname = varname;
+			} else
+				curFile->guard = GS_NO;
+		}
+		switch (condResult) {
 		case CR_FALSE:	/* May also mean a syntax error. */
 			if (!SkipIrrelevantBranches())
 				return NULL;
@@ -2784,6 +2831,22 @@ Parse_VarAssign(const char *line, bool f
 	return true;
 }
 
+void
+Parse_GuardElse(void)
+{
+	IncludedFile *curFile = CurFile();
+	if (cond_depth == curFile->condMinDepth + 1)
+		curFile->guard = GS_NO;
+}
+
+void
+Parse_GuardEndif(void)
+{
+	IncludedFile *curFile = CurFile();
+	if (cond_depth == curFile->condMinDepth && curFile->guard == GS_COND)
+		curFile->guard = GS_DONE;
+}
+
 static char *
 FindSemicolon(char *p)
 {
@@ -2974,6 +3037,7 @@ Parse_Init(void)
 	sysIncPath = SearchPath_New();
 	defSysIncPath = SearchPath_New();
 	Vector_Init(&includes, sizeof(IncludedFile));
+	HashTable_Init(&guards);
 }
 
 /* Clean up the parsing module. */
@@ -2981,6 +3045,8 @@ void
 Parse_End(void)
 {
 #ifdef CLEANUP
+	HashIter hi;
+
 	Lst_DoneCall(&targCmds, free);
 	assert(targets == NULL);
 	SearchPath_Free(defSysIncPath);
@@ -2988,6 +3054,10 @@ Parse_End(void)
 	SearchPath_Free(parseIncPath);
 	assert(includes.len == 0);
 	Vector_Done(&includes);
+	HashIter_Init(&hi, &guards);
+	while (HashIter_Next(&hi) != NULL)
+		free(hi.entry->value);
+	HashTable_Done(&guards);
 #endif
 }
 

Index: src/usr.bin/make/unit-tests/directive-include-guard.exp
diff -u src/usr.bin/make/unit-tests/directive-include-guard.exp:1.3 src/usr.bin/make/unit-tests/directive-include-guard.exp:1.4
--- src/usr.bin/make/unit-tests/directive-include-guard.exp:1.3	Sun Jun 18 20:43:52 2023
+++ src/usr.bin/make/unit-tests/directive-include-guard.exp	Mon Jun 19 12:53:57 2023
@@ -1,19 +1,19 @@
 Parse_PushInput: file guarded-ifndef.tmp, line 1
-Parse_PushInput: file guarded-ifndef.tmp, line 1
+Skipping 'guarded-ifndef.tmp' because 'GUARDED_IFNDEF' is already set
 Parse_PushInput: file comments.tmp, line 1
-Parse_PushInput: file comments.tmp, line 1
-Parse_PushInput: file guarded-if.tmp, line 1
+Skipping 'comments.tmp' because 'COMMENTS' is already set
 Parse_PushInput: file guarded-if.tmp, line 1
+Skipping 'guarded-if.tmp' because 'GUARDED_IF' is already set
 Parse_PushInput: file triple-negation.tmp, line 1
 Parse_PushInput: file triple-negation.tmp, line 1
 Parse_PushInput: file varname-mismatch.tmp, line 1
 Parse_PushInput: file varname-mismatch.tmp, line 1
 Parse_PushInput: file varname-indirect.tmp, line 1
-Parse_PushInput: file varname-indirect.tmp, line 1
+Skipping 'varname-indirect.tmp' because 'VARNAME_INDIRECT' is already set
 Parse_PushInput: file late-assignment.tmp, line 1
-Parse_PushInput: file late-assignment.tmp, line 1
-Parse_PushInput: file two-conditions.tmp, line 1
+Skipping 'late-assignment.tmp' because 'LATE_ASSIGNMENT' is already set
 Parse_PushInput: file two-conditions.tmp, line 1
+Skipping 'two-conditions.tmp' because 'TWO_CONDITIONS' is already set
 Parse_PushInput: file already-set.tmp, line 1
 Parse_PushInput: file already-set.tmp, line 1
 Parse_PushInput: file twice.tmp, line 1
@@ -31,5 +31,5 @@ Parse_PushInput: file if-elif.tmp, line 
 Parse_PushInput: file if-else.tmp, line 1
 Parse_PushInput: file if-else.tmp, line 1
 Parse_PushInput: file inner-if-elif-else.tmp, line 1
-Parse_PushInput: file inner-if-elif-else.tmp, line 1
+Skipping 'inner-if-elif-else.tmp' because 'INNER_IF_ELIF_ELSE' is already set
 exit status 0

Index: src/usr.bin/make/unit-tests/directive-include-guard.mk
diff -u src/usr.bin/make/unit-tests/directive-include-guard.mk:1.4 src/usr.bin/make/unit-tests/directive-include-guard.mk:1.5
--- src/usr.bin/make/unit-tests/directive-include-guard.mk:1.4	Sun Jun 18 20:43:52 2023
+++ src/usr.bin/make/unit-tests/directive-include-guard.mk	Mon Jun 19 12:53:57 2023
@@ -1,21 +1,20 @@
-# $NetBSD: directive-include-guard.mk,v 1.4 2023/06/18 20:43:52 rillig Exp $
+# $NetBSD: directive-include-guard.mk,v 1.5 2023/06/19 12:53:57 rillig Exp $
 #
 # Tests for multiple-inclusion guards in makefiles.
 #
 # A file that is guarded by a multiple-inclusion guard has the following form:
 #
 #	.ifndef GUARD_NAME
-#	GUARD_NAME=	# any value
+#	...
+#	GUARD_NAME=	# any value, may also be empty
 #	...
 #	.endif
 #
-# When such a file is included for the second time, it has no effect, as all
-# its content is skipped.
+# When such a file is included later and the guard variable is set, including
+# the file has no effect, as all its content is skipped.
 #
 # See also:
 #	https://gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html
-#
-# TODO: In these cases, do not include the file, to increase performance.
 
 
 # This is the canonical form of a multiple-inclusion guard.
@@ -25,7 +24,7 @@ LINES.guarded-ifndef= \
 	'GUARDED_IFNDEF=' \
 	'.endif'
 # expect: Parse_PushInput: file guarded-ifndef.tmp, line 1
-# expect: Parse_PushInput: file guarded-ifndef.tmp, line 1
+# expect: Skipping 'guarded-ifndef.tmp' because 'GUARDED_IFNDEF' is already set
 
 # Comments and empty lines have no influence on the multiple-inclusion guard.
 INCS+=	comments
@@ -38,7 +37,7 @@ LINES.comments= \
 	'.endif' \
 	'\# comment'
 # expect: Parse_PushInput: file comments.tmp, line 1
-# expect: Parse_PushInput: file comments.tmp, line 1
+# expect: Skipping 'comments.tmp' because 'COMMENTS' is already set
 
 # An alternative form uses the 'defined' function.  It is more verbose than
 # the canonical form.  There are other possible forms as well, such as with a
@@ -49,7 +48,7 @@ LINES.guarded-if= \
 	'GUARDED_IF=' \
 	'.endif'
 # expect: Parse_PushInput: file guarded-if.tmp, line 1
-# expect: Parse_PushInput: file guarded-if.tmp, line 1
+# expect: Skipping 'guarded-if.tmp' because 'GUARDED_IF' is already set
 
 # Triple negation is so uncommon that it's not recognized.
 INCS+=	triple-negation
@@ -69,23 +68,19 @@ LINES.varname-mismatch= \
 # expect: Parse_PushInput: file varname-mismatch.tmp, line 1
 # expect: Parse_PushInput: file varname-mismatch.tmp, line 1
 
-# The variable name in the assignment must only contain alphanumeric
-# characters and underscores, in particular, it must not be a dynamically
-# computed name.
+# The variable name in the guard condition must only contain alphanumeric
+# characters and underscores.  The guard variable is more flexible, it can be
+# set anywhere, as long as it is set when the guarded file is included next.
 INCS+=	varname-indirect
 LINES.varname-indirect= \
 	'.ifndef VARNAME_INDIRECT' \
 	'VARNAME_$${:UINDIRECT}=' \
 	'.endif'
 # expect: Parse_PushInput: file varname-indirect.tmp, line 1
-# expect: Parse_PushInput: file varname-indirect.tmp, line 1
+# expect: Skipping 'varname-indirect.tmp' because 'VARNAME_INDIRECT' is already set
 
-# The variable assignment for the guard must directly follow the conditional.
-#
-# This requirement may be dropped entirely later, as the guard variable could
-# also be undefined while reading the file or at a later point, and as long as
-# the implementation checks the guard variable before skipping the file, the
-# optimization is still valid.
+# The time at which the guard variable is set doesn't matter, as long as it is
+# set when the file is included the next time.
 INCS+=	late-assignment
 LINES.late-assignment= \
 	'.ifndef LATE_ASSIGNMENT' \
@@ -93,10 +88,10 @@ LINES.late-assignment= \
 	'LATE_ASSIGNMENT=' \
 	'.endif'
 # expect: Parse_PushInput: file late-assignment.tmp, line 1
-# expect: Parse_PushInput: file late-assignment.tmp, line 1
+# expect: Skipping 'late-assignment.tmp' because 'LATE_ASSIGNMENT' is already set
 
-# There must be no other condition between the guard condition and the
-# variable assignment.
+# The time at which the guard variable is set doesn't matter, as long as it is
+# set when the file is included the next time.
 INCS+=	two-conditions
 LINES.two-conditions= \
 	'.ifndef TWO_CONDITIONS' \
@@ -105,11 +100,11 @@ LINES.two-conditions= \
 	'.  endif' \
 	'.endif'
 # expect: Parse_PushInput: file two-conditions.tmp, line 1
-# expect: Parse_PushInput: file two-conditions.tmp, line 1
+# expect: Skipping 'two-conditions.tmp' because 'TWO_CONDITIONS' is already set
 
 # If the guard variable is already set before the file is included for the
-# first time, that file is not considered to be guarded.  It's a situation
-# that is uncommon in practice.
+# first time, the file is not considered guarded, as the makefile parser skips
+# all lines in the inactive part between the '.ifndef' and the '.endif'.
 INCS+=	already-set
 LINES.already-set= \
 	'.ifndef ALREADY_SET' \
@@ -123,18 +118,20 @@ ALREADY_SET=
 # several, even if they have the same effect.
 INCS+=	twice
 LINES.twice= \
-	'.ifndef TWICE' \
-	'TWICE=' \
+	'.ifndef TWICE_FIRST' \
+	'TWICE_FIRST=' \
 	'.endif' \
-	'.ifndef TWICE' \
-	'TWICE=' \
+	'.ifndef TWICE_SECOND' \
+	'TWICE_SECOND=' \
 	'.endif'
 # expect: Parse_PushInput: file twice.tmp, line 1
 # expect: Parse_PushInput: file twice.tmp, line 1
 
 # When multiple files use the same guard variable name, they exclude each
-# other.  It's the responsibility of the makefile authors to choose suitable
-# variable names.  Typical choices are ${PROJECT}_${DIR}_${FILE}_MK.
+# other.  It's the responsibility of the makefile authors to choose unique
+# variable names.  Typical choices are ${PROJECT}_${DIR}_${FILE}_MK.  This is
+# the same situation as in the 'already-set' test, and the file is not
+# considered guarded.
 INCS+=	reuse
 LINES.reuse= \
 	${LINES.guarded-if}
@@ -211,7 +208,8 @@ LINES.inner-if-elif-else = \
 	'.  endif' \
 	'.endif'
 # expect: Parse_PushInput: file inner-if-elif-else.tmp, line 1
-# expect: Parse_PushInput: file inner-if-elif-else.tmp, line 1
+# expect: Skipping 'inner-if-elif-else.tmp' because 'INNER_IF_ELIF_ELSE' is already set
+
 
 # Include each of the files twice.  The directive-include-guard.exp file
 # contains a single entry for the files whose multiple-inclusion guard works,

Reply via email to