1#!/usr/bin/env python
2# Dependencies.py - discover, read, and write dependencies file for make.
3# The format like the output from "g++ -MM" which produces a
4# list of header (.h) files used by source files (.cxx).
5# As a module, provides
6#	FindPathToHeader(header, includePath) -> path
7#	FindHeadersInFile(filePath) -> [headers]
8#	FindHeadersInFileRecursive(filePath, includePath, renames) -> [paths]
9#	FindDependencies(sourceGlobs, includePath, objExt, startDirectory, renames) -> [dependencies]
10#	ExtractDependencies(input) -> [dependencies]
11#	TextFromDependencies(dependencies)
12#	WriteDependencies(output, dependencies)
13#	UpdateDependencies(filepath, dependencies)
14#	PathStem(p) -> stem
15#	InsertSynonym(dependencies, current, additional) -> [dependencies]
16# If run as a script reads from stdin and writes to stdout.
17# Only tested with ASCII file names.
18# Copyright 2019 by Neil Hodgson <neilh@scintilla.org>
19# The License.txt file describes the conditions under which this software may be distributed.
20# Requires Python 2.7 or later
21
22import codecs, glob, os, sys
23
24from . import FileGenerator
25
26continuationLineEnd = " \\"
27
28def FindPathToHeader(header, includePath):
29	for incDir in includePath:
30		relPath = os.path.join(incDir, header)
31		if os.path.exists(relPath):
32			return relPath
33	return ""
34
35fhifCache = {}	# Remember the includes in each file. ~5x speed up.
36def FindHeadersInFile(filePath):
37	if filePath not in fhifCache:
38		headers = []
39		with codecs.open(filePath, "r", "utf-8") as f:
40			for line in f:
41				if line.strip().startswith("#include"):
42					parts = line.split()
43					if len(parts) > 1:
44						header = parts[1]
45						if header[0] != '<':	# No system headers
46							headers.append(header.strip('"'))
47		fhifCache[filePath] = headers
48	return fhifCache[filePath]
49
50def FindHeadersInFileRecursive(filePath, includePath, renames):
51	headerPaths = []
52	for header in FindHeadersInFile(filePath):
53		if header in renames:
54			header = renames[header]
55		relPath = FindPathToHeader(header, includePath)
56		if relPath and relPath not in headerPaths:
57				headerPaths.append(relPath)
58				subHeaders = FindHeadersInFileRecursive(relPath, includePath, renames)
59				headerPaths.extend(sh for sh in subHeaders if sh not in headerPaths)
60	return headerPaths
61
62def RemoveStart(relPath, start):
63	if relPath.startswith(start):
64		return relPath[len(start):]
65	return relPath
66
67def ciKey(f):
68	return f.lower()
69
70def FindDependencies(sourceGlobs, includePath, objExt, startDirectory, renames={}):
71	deps = []
72	for sourceGlob in sourceGlobs:
73		sourceFiles = glob.glob(sourceGlob)
74		# Sorting the files minimizes deltas as order returned by OS may be arbitrary
75		sourceFiles.sort(key=ciKey)
76		for sourceName in sourceFiles:
77			objName = os.path.splitext(os.path.basename(sourceName))[0]+objExt
78			headerPaths = FindHeadersInFileRecursive(sourceName, includePath, renames)
79			depsForSource = [sourceName] + headerPaths
80			depsToAppend = [RemoveStart(fn.replace("\\", "/"), startDirectory) for
81				fn in depsForSource]
82			deps.append([objName, depsToAppend])
83	return deps
84
85def PathStem(p):
86	""" Return the stem of a filename: "CallTip.o" -> "CallTip" """
87	return os.path.splitext(os.path.basename(p))[0]
88
89def InsertSynonym(dependencies, current, additional):
90	""" Insert a copy of one object file with dependencies under a different name.
91	Used when one source file is used to create two object files with different
92	preprocessor definitions. """
93	result = []
94	for dep in dependencies:
95		result.append(dep)
96		if (dep[0] == current):
97			depAdd = [additional, dep[1]]
98			result.append(depAdd)
99	return result
100
101def ExtractDependencies(input):
102	""" Create a list of dependencies from input list of lines
103	Each element contains the name of the object and a list of
104	files that it depends on.
105	Dependencies that contain "/usr/" are removed as they are system headers. """
106
107	deps = []
108	for line in input:
109		headersLine = line.startswith(" ") or line.startswith("\t")
110		line = line.strip()
111		isContinued = line.endswith("\\")
112		line = line.rstrip("\\ ")
113		fileNames = line.strip().split(" ")
114		if not headersLine:
115			# its a source file line, there may be headers too
116			sourceLine = fileNames[0].rstrip(":")
117			fileNames = fileNames[1:]
118			deps.append([sourceLine, []])
119		deps[-1][1].extend(header for header in fileNames if "/usr/" not in header)
120	return deps
121
122def TextFromDependencies(dependencies):
123	""" Convert a list of dependencies to text. """
124	text = ""
125	indentHeaders = "\t"
126	joinHeaders = continuationLineEnd + os.linesep + indentHeaders
127	for dep in dependencies:
128		object, headers = dep
129		text += object + ":"
130		for header in headers:
131			text += joinHeaders
132			text += header
133		if headers:
134			text += os.linesep
135	return text
136
137def UpdateDependencies(filepath, dependencies, comment=""):
138	""" Write a dependencies file if different from dependencies. """
139	FileGenerator.UpdateFile(os.path.abspath(filepath), comment.rstrip() + os.linesep +
140		TextFromDependencies(dependencies))
141
142def WriteDependencies(output, dependencies):
143	""" Write a list of dependencies out to a stream. """
144	output.write(TextFromDependencies(dependencies))
145
146if __name__ == "__main__":
147	""" Act as a filter that reformats input dependencies to one per line. """
148	inputLines = sys.stdin.readlines()
149	deps = ExtractDependencies(inputLines)
150	WriteDependencies(sys.stdout, deps)
151