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