4
MakefileVariables.py - This script can be run as a shell command or imported as
5
a module into a python script. Its purpose is to read a Makefile and obtain a
6
map of make variables and their values.
8
Shell usage: MakefileVariables.py [options] [filename]
11
-h | --help Print this message and exit
12
-m | --make Output in Makefile format
13
-p | --python Output as a python dictionary (default)
14
-v | --version Print the version number and exit
16
Python usage: import MakefileVariables
20
processMakefile(string) -> dict
21
Given the name of a Makefile, parse the file for make variable names and
22
values, make substitutions wherever '$(...)' is found in the values, and
23
then uniquify the resulting values.
25
parseMakefile(string) -> dict
26
Open filename and obtain make variable names and their raw (unevaluated)
29
evaluate(string,dict) -> string
30
Substitute variable values in dict for $(...) in string.
32
makeSubstitutions(dict) -> None
33
Interpret dict as varName/value pairs; wherever '$(...)' appears in
34
values, make appropriate substitutions.
36
uniqiufyList([string1, string2, ...], bool=False) -> None
37
Remove duplicate strings from the list.
39
uniquifyString(string, bool=False) -> string
40
Remove duplicate substrings from the string.
42
uniquifyDict(dict, bool=False) -> None
43
Uniquify each value of the given dictionary.
45
findBlock(string,int=0) -> (int,int)
46
Search string to find an open delimeter and return its index as the
47
first int. Search for the corresponding closing delimeter and return
48
its index as the second int. Be smart about nested blocks. Start
49
search from optional int argument.
51
joinContinuationLines([string, string, ...]) -> None
52
Combine any strings that end with '\' with its following string.
54
isNotBlankLine(string) -> bool
55
Return True if string is not blank.
57
specialNormPath(string) -> string
58
Normalize a path, even if it starts with -I, -L, etc.
62
__author__ = "Bill Spotz"
63
__date__ = "Sep 10 2005"
65
# Import python modules for command-line options, the operating system, regular
66
# expressions, and system functions
73
# Define regular expressions for Makefile assignments, blank line, continuation
74
# lines, include statements and variable references
75
assignRE = re.compile(r"^\s*([A-Za-z_][A-Za-z_0-9]*)\s*=\s*(.*)$")
76
blankRE = re.compile(r"^\s*$" )
77
continueRE = re.compile(r"(.*)\\\s*$" )
78
includeRE = re.compile(r"\s*include\s+(.+)" )
79
makeVarRE = re.compile(r"\$\(([A-Za-z_][A-Za-z0-9_]*)\)" )
80
shellRE = re.compile(r"\$\(shell" )
82
#############################################################################
84
def findBlock(text, pos=0):
85
"""Given the input text (potentially multiline) and an optional pos marking
86
the starting position, find an opening delimeter -- either (, [, {, single
87
quote, or double quote -- and return a tuple of integers indicating the
88
character indexes of the text block -- closed with ), ], }, single quote, or
89
double quote, respectively -- while correctly handling nested blocks."""
91
# Define delimeter strings
94
openDelimeters = "\(\[\{"
95
closeDelimeters = "\)\]\}"
97
# Define delimeter regular expressions
98
quote1RE = re.compile("([" + quote1Delimeter + "])", re.M)
99
quote2RE = re.compile("([" + quote2Delimeter + "])", re.M)
100
openRE = re.compile("([" + openDelimeters +
102
quote2Delimeter + "])", re.M)
103
anyRE = re.compile("([" + openDelimeters +
106
closeDelimeters + "])", re.M)
108
# Find the first opening delimeter
109
matchObject = openRE.search(text, pos)
110
if not matchObject: return (None, None)
112
# Initialize the loop
113
stack = [ matchObject.group() ]
114
start = matchObject.start()
117
# Find the end of the block
120
# Determine the active delimeter regular expression
121
if stack[-1] == quote1Delimeter:
123
elif stack[-1] == quote2Delimeter:
128
# Search for the next delimeter
129
matchObject = activeRE.search(text, pos)
131
delimeter = matchObject.group()
132
pos = matchObject.end()
134
# Check for matched delimeters
135
if (((stack[-1] == quote1Delimeter) and
136
(delimeter == quote1Delimeter)) or
137
((stack[-1] == quote2Delimeter) and
138
(delimeter == quote2Delimeter)) or
139
((stack[-1] == "(" ) and
140
(delimeter == ")" )) or
141
((stack[-1] == "[" ) and
142
(delimeter == "]" )) or
143
((stack[-1] == "{" ) and
144
(delimeter == "}" )) ):
145
stack.pop() # Remove the last element from the list
149
# Process unmatched delimeter
151
if (delimeter in openDelimeters or
152
delimeter == quote1Delimeter or
153
delimeter == quote2Delimeter ):
154
stack.append(delimeter) # Add the delimeter to the stack
156
raise RuntimeError, "findBlock: mismatched delimeters: " + \
157
stack[-1] + " " + delimeter
159
# We made it through all of text without finding the end of the block
160
raise RuntimeError, "findBlock: open block: " + join(stack)
162
#############################################################################
164
def joinContinuationLines(lines):
165
"""Given lines, a list of strings, check for the continuation character
166
('\') at the end of each line. If found, combine the appropriate strings
167
into one, leaving blank lines in the list to avoid duplication."""
168
for i in range(len(lines)-1):
170
match = continueRE.search(line)
172
lines[i+1] = match.group(1) + lines[i+1]
175
#############################################################################
177
def isNotBlankLine(s):
178
"""Return True if a string is not a blank line"""
183
#############################################################################
185
def parseMakefile(filename):
186
"""Open filename, read in the text and return a dictionary of make variable
187
names and values. If an include statement is found, this routine will be
188
called recursively."""
189
lines = open(filename,"r").readlines() # Read in the lines of the Makefile
190
lines = [s.split("#")[0] for s in lines] # Remove all comments
191
joinContinuationLines(lines) # Remove continuation lines
192
lines = filter(isNotBlankLine, lines) # Remove all blank lines
195
# Process include statements
196
match = includeRE.match(line)
198
files = evaluate(match.group(1),dict).split()
201
dict.update(parseMakefile(file))
205
# Process assignment statements
206
match = assignRE.match(line)
208
dict[match.group(1).strip()] = match.group(2).strip()
211
#############################################################################
213
def evaluate(value, dict):
214
"""Evaluate the string 'value' by applying the following algorithm: if value
215
contains substring '$(VARNAME)' and dict has key VARNAME, substitute
216
dict[VARNAME] for the variable reference. If VARNAME is not a key for the
217
dictionary, substitute the null string. If value contains substring
218
'$(shell ...)', then call this routine recursively on the obtained command
219
string. If the resulting command string is unchanged, execute it as a shell
220
command and substitutue the results. Return the evaluated string."""
223
originalValue = value
225
debug = "shell perl" in value
227
# Evaluate $(VARNAME)
228
match = makeVarRE.search(value)
230
subVarName = match.group(1)
231
start = match.start(1)-2
232
end = match.end(1) +1
233
if subVarName in dict.keys():
234
subValue = dict[subVarName]
237
value = value[:start] + subValue + value[end:]
240
# Evaluate $(shell ...)
241
match = shellRE.search(value)
243
# The shellRE only matches the opening '$(shell'. We need to find
244
# the closing ')', accounting for internal parenthetical or quoted
246
(start,end) = findBlock(value,match.start())
248
shellCmd = value[start+8:end-1]
249
newShellCmd = evaluate(shellCmd,dict)
250
while newShellCmd != shellCmd:
251
shellCmd = newShellCmd
252
newShellCmd = evaluate(shellCmd,dict)
253
(status,subValue) = commands.getstatusoutput(shellCmd)
255
print >>sys.stderr, "WARNING: %s gives\n%s" % (shellCmd,
258
value = value[:start] + subValue + value[end:]
261
if value == originalValue:
264
return evaluate(value,dict)
266
#############################################################################
268
def makeSubstitutions(dict):
269
"""Loop over the items of a dictionary of variable names and string values.
270
If the value contains the substring(s) '$(VARNAME)' and VARNAME is a key in
271
the dictionary, then substitute the appropriate value. If VARNAME is not a
272
key in the dictionary, then substitute the null string. For circular
273
substitutions, substitute the null string."""
275
dict[varName] = evaluate(dict[varName],dict)
277
#############################################################################
279
def specialNormPath(path):
280
"""Apply os.path.normpath to argument path, but remove a leading option
281
such as '-I' or '-L' before the call and add it back before the result is
283
if len(path) <= 2: return path
285
if path[0] == "-": start = 2
286
return path[:start] + os.path.normpath(path[start:])
288
#############################################################################
290
def uniquifyList(list,reverse=False):
291
"""Remove duplicate items from a list, preserving the forward order
292
(default) or reverse order if the reverse flag is set."""
293
if reverse: list.reverse()
296
if list[i] in list[:i]:
300
if reverse: list.reverse()
302
#############################################################################
304
def uniquifyString(s, delim=' ', reverse=False):
305
"""Split a string using the specified delimeter, apply specialNormPath to
306
each string, uniquify the resulting list (passing along the optional reverse
307
flag), and return a string that joins the unique list with the same
309
list = s.split(delim)
310
list = [specialNormPath(path.strip()) for path in list]
311
uniquifyList(list,reverse)
312
return delim.join(list)
314
#############################################################################
316
def uniquifyDict(dict,reverse=False):
317
"""Loop over each item in a dictionary of variable names and string values
318
and uniquify the string, passing along the optional reverse flag."""
320
dict[key] = uniquifyString(dict[key],reverse=reverse)
322
#############################################################################
324
def processMakefile(filename):
325
"""Open filename, read its contents and parse it for Makefile assignments,
326
creating a dictionary of variable names and string values. Substitute
327
variable values when '$(...)' appears in a string value."""
328
# We want to change directories to the directory that contains the
329
# Makefile. Then, when we make substitutions, any $(shell ...) expansions
330
# will be done from the assumed directory.
332
(path,name) = os.path.split(filename)
333
if path: os.chdir(path)
335
# Parse and substitute
336
dict = parseMakefile(name)
337
makeSubstitutions(dict)
339
# Change directory back to prevent confusion
342
# Remove duplicates from the dictionary values and return the dictionary
346
#############################################################################
349
"""This is the routine that gets called if MakefileVariable.py is invoked
350
from the shell. Process any command-line options that may be given, take
351
the first argument as a filename, process it to obtain a dictionary of
352
variable name/value pairs, and output the results."""
355
(progDir,progName) = os.path.split(sys.argv[0])
357
long_options = ["help", "make", "python", "version"]
360
# Get the options and arguemnts from the command line
361
(opts,args) = getopt(sys.argv[1:], options, long_options)
363
# Loop over options and implement
365
if flag[0] in ("-h","--help"):
368
elif flag[0] in ("-m", "--make"):
370
elif flag[0] in ("-p", "--python"):
372
elif flag[0] in ("-v", "--version"):
373
print progName, __version__, __date__
376
print "Unrecognized flag:", flag[0]
380
# Process the filename
381
dict = processMakefile(args[0])
383
# Output the variable names and values
384
if outStyle == "make":
388
print key, "=", dict[key]
389
elif outStyle == "python":
392
#############################################################################
393
# If called from the command line, call main()
394
#############################################################################
396
if __name__ == "__main__":