blob: 17ef588115b13e3e0e32d9ac0733cd8b3e0176d1 [file] [log] [blame]
#!/usr/bin/env python
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import os
import re
from collections import defaultdict
################################################################################
################################# Configurations ###############################
################################################################################
# Project name.
PROJECT_NAME = "quickstep"
# Don't scan these directories.
EXCLUDED_DIRECTORIES = frozenset(
[ "./build",
"./cmake",
"./parser/preprocessed",
"./release",
"./storage/tests",
"./third_party" ])
# Don't scan these files.
EXCLUDED_FILES = frozenset(
[ "./empty_src.cpp" ])
# Third-party libraries.
THIRD_PARTY_LIBRARIES = \
{ "farmhash" : [ "farmhash" ],
"gflags" : [ "${GFLAGS_LIB_NAME}" ],
"glog" : [ "glog" ],
"gtest/gtest.h" : [ "gtest", "gtest_main" ],
"gtest/gtest_prod.h" : [ "gtest" ],
"re2" : [ "re2" ],
"tmb" : [ "tmb" ] }
# Explicitly ignored targets.
IGNORED_TARGETS = frozenset(
[ "quickstep_cli_LineReader",
"quickstep_cli_shell",
"quickstep_parser_SqlLexer",
"quickstep_parser_SqlParser",
"quickstep_parser_SqlParserWrapper",
"quickstep_threading_ConditionVariable",
"quickstep_threading_Mutex",
"quickstep_threading_SharedMutex",
"quickstep_threading_Thread" ])
# Do not move these targets around.
ANCHORED_TARGETS = frozenset(
[ "quickstep_cli_shell",
"quickstep_utility_TextBasedTestDriver"] )
# Do not move these link dependencies around.
ANCHORED_LINKS = frozenset(
[ "quickstep_cli_shell" ] )
# Source code file extensions under consideration.
SOURCE_CODE_FILE_EXTENSIONS = [ ".hpp", ".cpp" ]
# Maximum number of characters in one line.
MAX_LINE_WIDTH = 120
# Dummy source file name.
DUMMY_CPP_FILE_NAME = "empty_src.cpp"
# Whether to auto-fix target names according to default naming rule.
# E.g. ./types/containers/ColumnVector.*
# -> quickstep_types_containers_ColumnVector
FORCE_DEFAULT_NAMING_RULE_FOR_LIBRARY = True
FORCE_DEFAULT_NAMING_RULE_FOR_EXECUTABLE = False
################################################################################
############################### Utility Functions ##############################
################################################################################
def TopDownVisitor(functor):
def Wrapper(*args):
# First invoke functor.
functor(*args)
# Recursively process all child nodes.
for node in args[0].getChildren():
getattr(node, functor.__name__)(*list(args)[1:])
return Wrapper
def BottomUpVisitor(functor):
def Wrapper(*args):
# Recursively process all child nodes.
for node in args[0].getChildren():
getattr(node, functor.__name__)(*list(args)[1:])
# Then invoke functor.
functor(*args)
return Wrapper
def ResolveIndirectFilename(path, filename):
if filename.startswith("${CMAKE_CURRENT_SOURCE_DIR}"):
return filename.replace("${CMAKE_CURRENT_SOURCE_DIR}", path)
if filename.startswith("${PROJECT_SOURCE_DIR}"):
return filename.replace("${PROJECT_SOURCE_DIR}", ".")
return None
################################################################################
################################ Utility Classes ###############################
################################################################################
class LineStream:
"""A stream of lines
"""
def __init__(self, lines):
"""
Parameters
----------
lines : List[String]
"""
self.lines = lines
self.pos = 0
def hasNextLine(self):
return self.pos < len(self.lines)
def top(self):
return self.lines[self.pos]
def pop(self):
line = self.lines[self.pos]
self.pos += 1
return line
def size(self):
return len(self.lines)
class CMakeGlobalContext:
"""Context for global information across all cmake files.
Attributes
----------
document : DocumentRoot
referenced_files : Dict[String, Bool]
file_index : Dict[String, String]
target_index : Dict[String, String]
dependencies : Dict[String, List[Tuple[String, List[String]]]]
prior_dependencies : Dict[String, List[Tuple[String, List[String]]]]
modules : Dict[String, List[String]]
tests : Dict[String, Bool]
subdirectories : List[String]
"""
def __init__(self, document):
"""
Parameters
----------
document : DocumentRoot
"""
self.document = document
self.referenced_files = {}
self.file_index = {}
self.target_index = {}
self.dependencies = defaultdict(list)
self.prior_dependencies = defaultdict(list)
self.modules = defaultdict(list)
self.tests = {}
self.subdirectories = []
def addFileReference(self, filepath):
"""
Parameters
----------
filepath : String
"""
self.referenced_files[filepath] = True
class CMakeLocalContext:
"""Context for processing a cmake file
Attributes
----------
sources : Dict[String, SourceGroup]
"""
def __init__(self, sources):
"""
Parameters
----------
sources : Dict[String, SourceGroup]
"""
self.sources = sources
class CMakeFormat:
"""Format information of a cmake node
Attributes
----------
indents : Int
"""
def __init__(self):
self.indents = 0
def setIndentation(self, indents):
"""
Parameters
----------
indents : Int
"""
self.indents = indents
def getIndentationString(self):
"""
Returns
-------
String
"""
return " " * self.indents
################################################################################
################# CMake Command Intermediate Representations ###################
################################################################################
class CMakeNode(object):
"""Base class for various cmake nodes
Attributes
----------
mutated : Bool
"""
def __init__(self):
self.mutated = False
def setMutated(self, mutated):
"""
Parameters
----------
mutated : Bool
"""
self.mutated = mutated
def isMutated(self):
"""
Returns
-------
Bool
"""
return self.mutated
def getChildren(self):
"""
Returns
-------
List[CMakeNode]
"""
return []
@staticmethod
def GenerateDefaultTarget(path, name = None):
"""
Parameters
----------
path : String
name : String
Returns
-------
String
"""
target = PROJECT_NAME + path[1:].replace("_", "").replace("/", "_")
if name is not None:
target += "_" + name
return target
@TopDownVisitor
def autofixTargets(self, local_ctx):
"""
Parameters
----------
local_ctx : CMakeLocalContext
"""
pass
@TopDownVisitor
def collectReferencedFiles(self, output):
"""
Parameters
----------
output : List[String]
"""
pass
@TopDownVisitor
def buildTargetIndex(self, path, output):
"""
Parameters
----------
path : String
output : Dict[String, String]
"""
pass
@TopDownVisitor
def collectTargets(self, output):
"""
Parameters
----------
output : List[CMakeTarget]
"""
pass
@TopDownVisitor
def collectLinks(self, output):
"""
Parameters
----------
output : List[CMakeLink]
"""
pass
@TopDownVisitor
def collectSubdirectories(self, output):
"""
Parameters
----------
output : List[CMakeSubdirectory]
"""
pass
@TopDownVisitor
def collectDependencies(self, path, global_ctx, conditions):
"""
Parameters
----------
path : String
global_ctx : CMakeGlobalContext
conditions : List[String]
"""
pass
@TopDownVisitor
def collectTests(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
pass
@TopDownVisitor
def autofixDependencies(self, global_ctx):
"""
Parameters
----------
local_ctx : CMakeLocalContext
"""
pass
class CMakeApacheLicense(CMakeNode):
"""Apache license header
"""
def writeToStream(self, output):
"""
Parameters
----------
output : List[String]
"""
header = """# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License."""
output.append(header + "\n")
class CMakeLine(CMakeNode):
"""An unrecognized cmake line
Attributes
----------
line : String
"""
def __init__(self, stream):
"""
Parameters
----------
stream : LineStream
"""
super(CMakeLine, self).__init__()
if stream is None:
return
self.line = stream.pop().rstrip()
@staticmethod
def CreateEmptyLine():
node = CMakeLine(None)
node.line = ""
return node
@staticmethod
def CreateLine(line):
node = CMakeLine(None)
node.line = line
return node
def writeToStream(self, output):
"""
Parameters
----------
output : List[String]
"""
output.append(self.line + "\n")
class CMakeIf(CMakeNode):
"""A cmake if() command
Attributes
----------
condition : String
nodes : List[CMakeNode]
"""
def __init__(self, stream):
"""
Parameters
----------
stream : LineStream
"""
super(CMakeIf, self).__init__()
depth = 0
lines = []
while stream.hasNextLine():
line = stream.pop()
lines.append(line)
if "endif(" in line or "endif (" in line:
depth -= 1
if depth == 0:
break;
else:
line = line.strip()
if line.startswith("if") and not line.endswith("{"):
depth += 1
self.nodes = []
if len(lines) == 0:
return
stmt = lines[0]
self.condition = stmt[stmt.find('(') + 1 : stmt.find(')')].strip()
self.nodes.append(CMakeLine.CreateLine(lines[0].rstrip()))
self.nodes.append(CMakeGroup(LineStream(lines[1:-1])))
if len(lines) < 2:
return
self.nodes.append(CMakeLine.CreateLine(lines[-1].rstrip()))
def getChildren(self):
"""
Returns
-------
List[CMakeNode]
"""
return self.nodes
def autofixTargets(self, local_ctx):
"""Conditional targets require further semantic analysis.
"""
pass
def collectTargets(self, output):
"""Conditional targets require further semantic analysis.
"""
pass
def collectLinks(self, output):
"""Conditional targets require further semantic analysis.
"""
pass
def collectSubdirectories(self, output):
"""
Parameters
----------
output : List[CMakeDirectory]
"""
for node in self.nodes:
node.collectSubdirectories(output)
def collectDependencies(self, path, global_ctx, conditions):
"""
Parameters
----------
path : String
global_ctx : CMakeGlobalContext
conditions : List[String]
"""
child_conds = conditions + [self.condition]
for node in self.nodes:
node.collectDependencies(path, global_ctx, child_conds)
def collectTests(self, global_ctx):
"""Conditional targets require further semantic analysis.
"""
pass
def autofixDependencies(self, global_ctx):
"""Conditional targets require further semantic analysis.
"""
pass
def writeToStream(self, output):
"""
Parameters
----------
output : List[String]
"""
for node in self.nodes:
node.writeToStream(output)
class CMakeSubdirectory(CMakeNode):
"""A cmake add_subdirectory() command
Attributes
----------
format : CMakeFormat
name : String
arguments : List[String]
multiline : Bool
"""
# Static member variables
COMMAND_SPLIT_PATTERN = re.compile("[ \t\r\n]+")
def __init__(self, stream):
"""
Parameters
----------
stream : LineStream
"""
super(CMakeSubdirectory, self).__init__()
if stream is None:
return
stmt = ""
while stream.hasNextLine():
line = stream.pop()
stmt += line
# NOTE(jianqiao): We assume that the original CMakeLists.txt file
# is relatively "well written".
if ")" in line:
break
self.format = CMakeFormat()
self.format.setIndentation(len(stmt) - len(stmt.lstrip()))
# Extract the content
stmt = stmt[stmt.find('(') + 1 : stmt.find(')')]
self.multiline = "\n" in stmt
# Split into target + file list
items = CMakeTarget.COMMAND_SPLIT_PATTERN.split(stmt)
self.name = items[0]
self.arguments = items[1:]
@staticmethod
def CreateDirectory(name):
node = CMakeSubdirectory(None)
node.format = CMakeFormat()
node.name = name
node.arguments = []
node.multiline = False
return node
def collectSubdirectories(self, output):
"""
Parameters
----------
output : List[CMakeDirectory]
"""
output.append(self)
def isRegular(self):
if self.name is None:
return False
return not (self.name.startswith("\"") or self.name.startswith("$"))
def toStringSingleLine(self):
"""
Returns
-------
String
"""
return self.format.getIndentationString() + "add_subdirectory(" + \
" ".join([self.name] + self.arguments) + ")"
def toStringMultipleLines(self):
"""
Returns
-------
String
"""
output = self.format.getIndentationString() + "add_subdirectory("
spaces = "\n" + " " * len(output)
output += spaces.join([self.name] + self.arguments) + ")"
return output
def writeToStream(self, output):
"""
Parameters
----------
output : List[String]
"""
if self.name is None:
return
item = self.toStringSingleLine()
# Use multi-line format if the single-line output is too long
# or the original command is in multi-line mode and is not mutated.
if (len(item) > MAX_LINE_WIDTH
or (self.multiline and not self.isMutated())):
item = self.toStringMultipleLines()
output.append(item + "\n")
class CMakeTest(CMakeNode):
"""A cmake add_test() command
Attributes
----------
format : CMakeFormat
target : String
dependencies : List[String]
multiline: Bool
"""
# Static member variables
COMMAND_SPLIT_PATTERN = re.compile("[ \t\r\n]+")
def __init__(self, stream):
"""
Parameters
----------
stream : LineStream
"""
super(CMakeTest, self).__init__()
stmt = ""
while stream.hasNextLine():
line = stream.pop()
stmt += line
# NOTE(jianqiao): We assume that the original CMakeLists.txt file
# is relatively "well written".
if ")" in line:
break
self.format = CMakeFormat()
self.format.setIndentation(len(stmt) - len(stmt.lstrip()))
# Extract the content
stmt = stmt[stmt.find('(') + 1 : stmt.find(')')]
self.multiline = "\n" in stmt
# Split into test name + target
items = CMakeTarget.COMMAND_SPLIT_PATTERN.split(stmt)
assert len(items) >= 2
self.target = items[0]
self.dependencies = items[1:]
def collectTests(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
global_ctx.tests[self.target] = True
def toStringSingleLine(self):
"""
Returns
-------
String
"""
return self.format.getIndentationString() + "add_test(" + \
" ".join([self.target] + self.dependencies) + ")"
def toStringMultipleLines(self):
"""
Returns
-------
String
"""
output = self.format.getIndentationString() + "add_test("
spaces = "\n" + " " * len(output)
output += spaces.join([self.target] + self.dependencies) + ")"
return output
def writeToStream(self, output):
"""
Parameters
----------
output : List[String]
"""
item = self.toStringSingleLine()
# Use multi-line format if the single-line output is too long
# or the original command is in multi-line mode and is not mutated.
if (len(item) > MAX_LINE_WIDTH
or (self.multiline and not self.isMutated())):
item = self.toStringMultipleLines()
output.append(item + "\n")
class CMakeTarget(CMakeNode):
"""A cmake add_library() or add_executable() command
Attributes
----------
format : CMakeFormat
type : String
target : String
name : String
files : List[String]
multiline: Bool
"""
# Static member variables
COMMAND_SPLIT_PATTERN = re.compile("[ \t\r\n]+")
INCLUDE_SPLIT_PATTERN = re.compile("[/.]")
def __init__(self, stream):
"""
Parameters
----------
stream : LineStream
"""
super(CMakeTarget, self).__init__()
if stream is None:
return
stmt = ""
while stream.hasNextLine():
line = stream.pop()
stmt += line
# NOTE(jianqiao): We assume that the original CMakeLists.txt file
# is relatively "well written".
if ")" in line:
break
self.format = CMakeFormat()
self.format.setIndentation(len(stmt) - len(stmt.lstrip()))
# Figure out whether the target type.
for type in ["add_library", "add_executable"]:
if type in stmt:
self.type = type
break
assert self.type is not None
# Extract the content
stmt = stmt[stmt.find('(') + 1 : stmt.find(')')]
self.multiline = "\n" in stmt
# Split into target + file list
items = CMakeTarget.COMMAND_SPLIT_PATTERN.split(stmt)
self.target = items[0]
self.files = items[1:]
self.name = None
self.inferTargetName()
@staticmethod
def CreateLibrary(path, name, group):
node = CMakeTarget(None)
node.format = CMakeFormat()
node.type = "add_library"
node.target = CMakeNode.GenerateDefaultTarget(path, name)
node.name = name
node.multiline = False
files = [(ext, name + ext) for ext in group.files]
if not any([ext.startswith(".c") for ext in group.files]):
depth = group.path.count("/")
empty_cpp = "/".join([".."] * depth) + "/" + DUMMY_CPP_FILE_NAME
files.append((".cpp", empty_cpp))
node.files = [it[1] for it in sorted(files, key = lambda s : s[0])]
return node
def inferTargetName(self):
"""Get target name if this target contains only matched .hpp/.cpp files.
"""
target_name = None
for filename in self.files:
if DUMMY_CPP_FILE_NAME in filename:
continue
if "/" in filename or "$" in filename:
return
name, _ = os.path.splitext(filename)
if target_name is None:
target_name = name
elif target_name != name:
return
if target_name is None:
return
self.name = target_name
def isTest(self):
# Hardcoded rule that identifies extra test files.
return "test" in self.target
def isRegular(self, path):
if self.isTest():
return False
if self.target == CMakeNode.GenerateDefaultTarget(path):
return False
return True
def collectTests(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
if self.isTest():
global_ctx.tests[self.target] = True
def autofixTargets(self, local_ctx):
"""
Parameters
----------
local_ctx : CMakeLocalContext
"""
name = self.name
# TODO(quickstep-team): Handle the non-regular cases.
if name is None:
return
if self.target in IGNORED_TARGETS:
return
if name not in local_ctx.sources:
self.setMutated(True)
self.files = []
return
group = local_ctx.sources[name]
files = [(ext, name + ext) for ext in group.files]
if not any([ext.startswith(".c") for ext in group.files]):
depth = group.path.count("/")
empty_cpp = "/".join([".."] * depth) + "/" + DUMMY_CPP_FILE_NAME
files.append((".cpp", empty_cpp))
files = [it[1] for it in sorted(files, key = lambda s : s[0])]
if self.files != files:
self.setMutated(True)
self.files = files
# Auto-fix target name.
if ((FORCE_DEFAULT_NAMING_RULE_FOR_LIBRARY and self.type == "add_library")
or (FORCE_DEFAULT_NAMING_RULE_FOR_EXECUTABLE and self.type == "add_executable")):
target = CMakeNode.GenerateDefaultTarget(group.path, name)
if target.startswith(self.target):
# Likely a module-all-in-one target.
pass
elif self.target != target:
self.setMutated(True)
self.target = target
def collectReferencedFiles(self, output):
"""
Parameters
----------
output : List[String]
"""
for filename in self.files:
output.append(filename)
def buildTargetIndex(self, path, output):
"""
Parameters
----------
path : String
output : Dict[String, String]
"""
for filename in self.files:
filepath = path + "/" + filename
# Prefer regular targets.
if (filepath in output and self.name is None):
continue
output[filepath] = self.target
def collectTargets(self, output):
"""
Parameters
----------
output : List[String]
"""
output.append(self)
def collectDependencies(self, path, global_ctx, conditions):
"""
Parameters
----------
path : String
global_ctx : CMakeGlobalContext
conditions : List[String]
"""
dependencies = []
for filename in self.files:
# Remove double quotes.
if filename.startswith("\""):
filename = filename[1:-1]
# Resolve indirect file name.
if filename.startswith("$"):
source_fp = ResolveIndirectFilename(path, filename)
if source_fp is None:
continue
else:
source_fp = path + "/" + filename
if source_fp not in global_ctx.file_index:
continue
# Get source file.
source = global_ctx.file_index[source_fp]
for ic in source.includes:
if ic.isConditional():
continue
item = ic.item
# Check if it is third-party include.
if item in THIRD_PARTY_LIBRARIES:
dependencies += THIRD_PARTY_LIBRARIES[item]
continue
label = CMakeTarget.INCLUDE_SPLIT_PATTERN.split(item)[0]
if label in THIRD_PARTY_LIBRARIES:
dependencies += THIRD_PARTY_LIBRARIES[label]
continue
# Check if it is a protobuf header.
if item.endswith(".pb.h"):
proto = CMakeNode.GenerateDefaultTarget("./" + item[:-5], "proto")
dependencies.append(proto)
continue
# Get file path.
filepath = "./" + item
if filepath not in global_ctx.target_index:
continue
dependencies.append(global_ctx.target_index[filepath])
global_ctx.dependencies[self.target] = \
[(dependency, conditions) for dependency in dependencies]
def toStringSingleLine(self):
"""
Returns
-------
String
"""
return self.format.getIndentationString() + self.type + "(" + \
" ".join([self.target] + self.files) + ")"
def toStringMultipleLines(self):
"""
Returns
-------
String
"""
output = self.format.getIndentationString() + self.type + "("
spaces = "\n" + " " * len(output)
output += spaces.join([self.target] + self.files) + ")"
return output
def writeToStream(self, output):
"""
Parameters
----------
output : List[String]
"""
if len(self.files) == 0:
return
item = self.toStringSingleLine()
# Use multi-line format if the single-line output is too long
# or the original command is in multi-line mode and is not mutated.
if (len(item) > MAX_LINE_WIDTH
or (self.multiline and not self.isMutated())):
item = self.toStringMultipleLines()
output.append(item + "\n")
class CMakeLink(CMakeNode):
"""A cmake target_link_libraries() command
Attributes
----------
format : CMakeFormat
target : String
dependencies : List[String]
multiline : Bool
"""
# Static member variables
COMMAND_SPLIT_PATTERN = re.compile("[ \t\r\n]+")
def __init__(self, stream):
"""
Parameters
----------
stream : LineStream
"""
super(CMakeLink, self).__init__()
if stream is None:
return
stmt = ""
while stream.hasNextLine():
line = stream.pop()
stmt += line
# NOTE(jianqiao): We assume that the original CMakeLists.txt file
# is relatively "well written".
if ")" in line:
break
self.format = CMakeFormat()
self.format.setIndentation(len(stmt) - len(stmt.lstrip()))
# Extract the content
stmt = stmt[stmt.find('(') + 1 : stmt.find(')')]
self.multiline = "\n" in stmt
# Split into target + file list
items = CMakeLink.COMMAND_SPLIT_PATTERN.split(stmt)
self.target = items[0]
self.dependencies = items[1:]
self.sortDependencies()
def isTest(self):
# Hardcoded rule that identifies extra test targets.
return "test" in self.target
def isRegular(self, path):
if self.isTest():
return False
if self.target == CMakeNode.GenerateDefaultTarget(path):
return False
return True
def collectLinks(self, output):
"""
Parameters
----------
output : List[CMakeLink]
"""
output.append(self)
def collectDependencies(self, path, global_ctx, conditions):
"""
Parameters
----------
path : String
global_ctx : CMakeGlobalContext
conditions : List[String]
"""
global_ctx.prior_dependencies[self.target] += \
[(dependency, conditions) for dependency in self.dependencies]
def sortDependencies(self):
self.dependencies = sorted(set(self.dependencies))
def autofixDependencies(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
target = self.target
# TODO(quickstep-team): Handle tests.
if target in global_ctx.tests:
return
# TODO(quickstep-team): Handle protobuf files.
if target.endswith("proto"):
return
if target in IGNORED_TARGETS:
return
self.setMutated(True)
self.dependencies = [x for x in self.dependencies if x in IGNORED_TARGETS]
if target in global_ctx.modules:
normal_dependencies = {}
conditional_dependencies = {}
if target in global_ctx.prior_dependencies:
for it in global_ctx.prior_dependencies[target]:
if len(it[1]) == 0:
normal_dependencies[it[0]] = True
else:
conditional_dependencies[it[0]] = True
for submodule in global_ctx.modules[target]:
if (submodule == target or submodule in global_ctx.tests):
continue
if (submodule not in global_ctx.dependencies
and submodule not in global_ctx.prior_dependencies):
continue
if (submodule in normal_dependencies
or submodule not in conditional_dependencies):
self.dependencies.append(submodule)
self.sortDependencies()
return
if target not in global_ctx.dependencies:
self.dependencies = []
return
# TODO(quickstep-team): Handle dependencies with conditions.
self.dependencies += [it[0] for it in global_ctx.dependencies[target]
if it[0] != target and len(it[1]) == 0]
self.sortDependencies()
def toStringSingleLine(self):
"""
Returns
-------
String
"""
return self.format.getIndentationString() + "target_link_libraries(" + \
" ".join([self.target] + self.dependencies) + ")"
def toStringMultipleLines(self):
"""
Returns
-------
String
"""
output = self.format.getIndentationString() + "target_link_libraries("
spaces = "\n" + " " * len(output)
output += spaces.join([self.target] + self.dependencies) + ")"
return output
def writeToStream(self, output):
"""
Parameters
----------
output : List[String]
"""
if len(self.dependencies) == 0:
return
item = None
if not self.multiline:
item = self.toStringSingleLine()
if item is None or len(item) > MAX_LINE_WIDTH:
item = self.toStringMultipleLines()
output.append(item + "\n")
class CMakeGroup(CMakeNode):
"""A group of cmake commands
Attributes
----------
nodes : List[CMakeNode]
"""
def __init__(self, stream):
"""
Parameters
----------
stream : LineStream
"""
super(CMakeGroup, self).__init__()
self.nodes = []
if stream is None:
return
while stream.hasNextLine():
line = stream.top().lstrip()
if line.startswith("add_library"):
self.nodes.append(CMakeTarget(stream))
continue
if line.startswith("add_executable"):
self.nodes.append(CMakeTarget(stream))
continue
if line.startswith("add_subdirectory"):
self.nodes.append(CMakeSubdirectory(stream))
continue
if line.startswith("add_test"):
self.nodes.append(CMakeTest(stream))
continue
if line.startswith("if"):
self.nodes.append(CMakeIf(stream))
continue
if line.startswith("target_link_libraries"):
self.nodes.append(CMakeLink(stream))
continue
self.nodes.append(CMakeLine(stream))
def getChildren(self):
"""
Returns
-------
List[CMakeNode]
"""
return self.nodes
def writeToStream(self, output):
"""
Parameters
----------
output : List[String]
"""
for node in self.nodes:
node.writeToStream(output)
################################################################################
############################# CMakeList Abstraction ############################
################################################################################
class CMakeLists:
"""A CMakeLists.txt file
Attributes
----------
path : String
root : CMakeGroup
newfile : Bool
"""
def __init__(self, path):
"""
Parameters
----------
path : String
"""
self.path = path
if "CMakeLists.txt" in os.listdir(path):
with open(self.path + "/CMakeLists.txt", "rb") as file:
self.root = CMakeGroup(LineStream(file.readlines()))
self.newfile = False
else:
self.root = CMakeGroup(None)
self.root.nodes.append(CMakeApacheLicense())
self.root.nodes.append(CMakeLine.CreateEmptyLine())
self.newfile = True
def autofixTargets(self, ctx):
"""
Parameters
----------
ctx : CMakeLocalContext
"""
self.root.autofixTargets(ctx)
def collectReferences(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
# Collect all referenced files.
unresolved_files = []
self.root.collectReferencedFiles(unresolved_files)
# Resolve file path.
referenced_files = {}
for filename in unresolved_files:
if DUMMY_CPP_FILE_NAME in filename:
continue
if filename.startswith("\""):
filename = filename[1:-1]
if filename.startswith("$"):
filepath = ResolveIndirectFilename(self.path, filename)
if filepath is not None:
referenced_files[filepath] = True
continue
filepath = filename
if not filename.startswith("/"):
filepath = self.path + "/" + filename
referenced_files[filepath] = True
# Register all references into global context.
for filepath in referenced_files:
global_ctx.addFileReference(filepath)
def addMissingTargets(self, local_ctx, global_ctx):
"""
Parameters
----------
ctx : CMakeLocalContext
"""
targets = {}
# Collect all existing targets.
for node in self.root.nodes:
if isinstance(node, CMakeTarget):
targets[node.target] = node
# Get unreferenced file groups.
unreferenced_names = []
for name in local_ctx.sources:
group = local_ctx.sources[name]
for filepath in group.getFilePaths():
if filepath not in global_ctx.referenced_files:
unreferenced_names.append(name)
break
# Add targets for unferenced file groups.
for name in unreferenced_names:
node = CMakeTarget.CreateLibrary(self.path, name, local_ctx.sources[name])
self.root.nodes.append(CMakeLine.CreateEmptyLine())
self.root.nodes.append(node)
def buildTargetIndex(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.root.buildTargetIndex(self.path, global_ctx.target_index)
def collectDependencies(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.root.collectDependencies(self.path, global_ctx, [])
def collectTests(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.root.collectTests(global_ctx)
def addMissingDependencies(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
# First add missing target link entries.
links = []
self.root.collectLinks(links)
links = set([node.target for node in links])
targets = []
self.root.collectTargets(targets)
for e in targets:
if (not e.isTest()) and e.target not in links:
node = CMakeLink(None)
node.format = CMakeFormat()
node.target = e.target
node.dependencies = []
node.multiline = True
self.root.nodes.append(CMakeLine.CreateEmptyLine())
self.root.nodes.append(node)
# Then register non-test targets into the current module.
module = CMakeNode.GenerateDefaultTarget(self.path)
global_ctx.modules[module] += [node.target for node in targets
if not node.isTest()]
# Then register the current module into upper level module.
if "test" not in module and not self.isEmpty():
global_ctx.modules[module[:module.rfind("_")]].append(module)
def addMissingSubdirectories(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
if self.isEmpty():
return
nodes = []
self.root.collectSubdirectories(nodes)
subdirectories = {}
for node in nodes:
name = node.name
if name.startswith("\""):
name = name[1:-1]
# Resolve indirect name.
if name.startswith("$"):
name = ResolveIndirectFilename(self.path, name)
if name is None:
continue
if name in subdirectories:
print "Warning: Possibly duplicated add_subdirectory(" + \
name + ") in", self.path
subdirectories[name] = node
residuals = []
for subdir in global_ctx.subdirectories:
if subdir.startswith(self.path):
name = subdir[len(self.path)+1:]
if name not in subdirectories:
self.root.nodes.append(CMakeLine.CreateEmptyLine())
self.root.nodes.append(CMakeSubdirectory.CreateDirectory(name))
else:
del subdirectories[name]
else:
residuals.append(subdir)
for node in subdirectories.values():
node.name = None
global_ctx.subdirectories = residuals + [self.path]
def autofixDependencies(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.root.autofixDependencies(global_ctx)
def layoutTargets(self):
nodes = self.root.nodes
num_nodes = len(nodes)
anchor_pos = -1
for i in range(num_nodes):
node = nodes[i]
if isinstance(node, CMakeTarget):
anchor_pos = i
break
if anchor_pos < 0:
return
targets = []
suffix = []
for i in range(anchor_pos, num_nodes):
node = nodes[i]
if (isinstance(nodes[i], CMakeTarget)
and node.isRegular(self.path)
and node.target not in ANCHORED_TARGETS):
targets.append((node.target, node))
else:
suffix.append(node)
output = [nodes[i] for i in range(anchor_pos)]
output += [it[1] for it in sorted(targets, key = lambda s : s[0])]
output += suffix
self.root.nodes = output
def layoutSubdirectories(self):
nodes = self.root.nodes
num_nodes = len(nodes)
anchor_pos = -1
for i in range(num_nodes):
node = nodes[i]
if isinstance(node, CMakeSubdirectory) and node.isRegular():
anchor_pos = i
break
if anchor_pos < 0:
return
subdirectories = []
suffix = []
for i in range(anchor_pos, num_nodes):
node = nodes[i]
if isinstance(nodes[i], CMakeSubdirectory) and node.isRegular():
subdirectories.append((node.name, node))
else:
suffix.append(node)
output = [nodes[i] for i in range(anchor_pos)]
output += [it[1] for it in sorted(subdirectories, key = lambda s : s[0])]
output += suffix
self.root.nodes = output
def layoutLinkDependencies(self):
nodes = self.root.nodes
num_nodes = len(nodes)
anchor_pos = -1
for i in range(num_nodes):
node = nodes[i]
if isinstance(node, CMakeLink):
anchor_pos = i
break
if anchor_pos < 0:
return
links = []
suffix = []
for i in range(anchor_pos, num_nodes):
node = nodes[i]
if (isinstance(nodes[i], CMakeLink)
and node.isRegular(self.path)
and node.target not in ANCHORED_LINKS):
links.append((node.target, node))
else:
suffix.append(node)
output = [nodes[i] for i in range(anchor_pos)]
output += [it[1] for it in sorted(links, key = lambda s : s[0])]
output += suffix
self.root.nodes = output
def collapseEmptyLines(self):
output = []
state = False
for node in self.root.nodes:
if (not isinstance(node, CMakeLine)
or len(node.line) != 0):
output.append(node)
state = False
continue
if not state:
output.append(node)
state = True
self.root.nodes = output
def layout(self):
"""Rearrange the commands.
"""
self.layoutSubdirectories()
self.layoutTargets()
self.layoutLinkDependencies()
self.collapseEmptyLines()
def isEmpty(self):
return self.newfile and len(self.root.nodes) <= 2
def writeToFile(self):
if self.isEmpty():
return
output = []
self.root.writeToStream(output)
while len(output) > 0:
line = output.pop()
if len(line.strip()) != 0:
output.append(line)
break
if len(output) == 0:
return
with open(self.path + "/CMakeLists.txt", "wb") as file:
file.write("".join(output))
file.close()
################################################################################
########################## Source Code File Information ########################
################################################################################
class IncludeItem:
"""A #include item
Attributes
----------
item : String
dependencies : List[String]
"""
# Static member variables
INCLUDE_PATTERN = re.compile("#include [<\"](.*)[>\"]")
DEPENDENCY_PATTERN = re.compile("#ifdef *([^ ]*)")
def __init__(self, text, dependencies):
"""Constructor
"""
match = IncludeItem.INCLUDE_PATTERN.search(text)
self.item = match.group(1)
self.dependencies = []
# Currently we consider only #ifdef dependencies
for line in dependencies:
if not line.startswith("#ifdef"):
continue
match = IncludeItem.DEPENDENCY_PATTERN.search(line)
self.dependencies.append(match.group(1))
def isConditional(self):
return len(self.dependencies) != 0
class SourceFile:
"""A source code file
Attributes
----------
path : String
name : String
includes : List[String]
"""
def __init__(self, path, name):
"""
Parameters
----------
path : String
name : String
"""
self.path = path
self.name = name
self.includes = []
# Parse include items
with open(self.path + "/" + self.name, "rb") as file:
dependencies = []
for line in file:
# Remove white spaces
line = line.strip()
# Record dependencies
if line.startswith("#if"):
dependencies.append(line)
if line.startswith("#endif"):
dependencies.pop()
# Add an include item
if line.startswith("#include"):
self.includes.append(IncludeItem(line, dependencies))
class SourceGroup:
"""hpp and/or cpp file(s) that form a library or executable
Attributes
----------
path : String
name : String
files : Dict[String, SourceFile]
"""
def __init__(self, path, name):
"""
Parameters
----------
path : String
name : String
"""
self.path = path
self.name = name
self.files = {}
def addExtension(self, ext):
"""Add a file with the specified extension (e.g. .hpp, .cpp) into this group
Parameters
----------
ext : String
"""
self.files[ext] = SourceFile(self.path, self.name + ext)
def getFileNames(self):
return [self.name + ext for ext in self.files]
def getFilePaths(self):
return [self.path + "/" + self.name + ext for ext in self.files]
################################################################################
######################### Directory / Top-level Wrapper ########################
################################################################################
class Directory:
"""Directory node in the document tree
Attributes
----------
path : String
directories : Dict[String, Directory]
sources : Dict[String, SourceGroup]
"""
def __init__(self, path):
"""
Parameters
----------
path : String
"""
self.path = path
self.directories = {}
self.sources = {}
# Parse directories.
for filename in os.listdir(path):
fullpath = path + "/" + filename
if (not os.path.isdir(fullpath)
or filename.startswith(".")
or fullpath in EXCLUDED_DIRECTORIES):
continue
self.directories[filename] = Directory(fullpath)
# Parse source code files.
for filename in os.listdir(path):
fullpath = path + "/" + filename
if (os.path.isdir(fullpath)
or filename.startswith(".")
or fullpath in EXCLUDED_FILES):
continue
name, ext = os.path.splitext(filename)
if ext not in SOURCE_CODE_FILE_EXTENSIONS:
continue
self.addSourceCodeFile(name, ext)
# Parse CMakeLists.txt file.
self.cmakelists = CMakeLists(path)
def addSourceCodeFile(self, name, ext):
"""Parse and add a source code file
Parameters
----------
path : String
ext : String
"""
if name not in self.sources:
self.sources[name] = SourceGroup(self.path, name)
self.sources[name].addExtension(ext)
def getChildren(self):
"""Get child directories.
Returns
----------
Dict[String, Directory]
"""
return self.directories.values()
@TopDownVisitor
def autofixTargets(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.cmakelists.autofixTargets(CMakeLocalContext(self.sources))
@TopDownVisitor
def collectReferences(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.cmakelists.collectReferences(global_ctx)
@TopDownVisitor
def addMissingTargets(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.cmakelists.addMissingTargets(CMakeLocalContext(self.sources), global_ctx)
@TopDownVisitor
def buildFileIndex(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
for name in self.sources:
group = self.sources[name]
for ext in group.files:
filepath = self.path + "/" + name + ext
global_ctx.file_index[filepath] = group.files[ext]
@TopDownVisitor
def buildTargetIndex(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.cmakelists.buildTargetIndex(global_ctx)
@TopDownVisitor
def collectDependencies(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.cmakelists.collectDependencies(global_ctx)
@TopDownVisitor
def collectTests(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.cmakelists.collectTests(global_ctx)
@TopDownVisitor
def addMissingDependencies(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.cmakelists.addMissingDependencies(global_ctx)
@TopDownVisitor
def autofixDependencies(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.cmakelists.autofixDependencies(global_ctx)
@BottomUpVisitor
def addMissingSubdirectories(self, global_ctx):
"""
Parameters
----------
global_ctx : CMakeGlobalContext
"""
self.cmakelists.addMissingSubdirectories(global_ctx)
@TopDownVisitor
def layoutCMakeLists(self):
"""Rearrange the commands in cmakelists.
"""
self.cmakelists.layout()
def writeCMakeLists(self):
# Recursively process subdirectories.
for name in self.directories:
self.directories[name].writeCMakeLists()
self.cmakelists.writeToFile()
class DocumentRoot:
"""The document tree root
Attributes
----------
path : String
directories : Dict[String, Directory]
"""
def __init__(self):
"""Constructor
"""
self.root = Directory(".")
def autofixCMakeLists(self):
# Create a global context for storing intermediate information.
global_ctx = CMakeGlobalContext(self)
# Fix existing targets.
self.root.autofixTargets(global_ctx)
# Collect referenced files.
self.root.collectReferences(global_ctx)
# Add missing targets.
self.root.addMissingTargets(global_ctx)
# Collect dependencies.
self.root.buildFileIndex(global_ctx)
self.root.buildTargetIndex(global_ctx)
self.root.collectDependencies(global_ctx)
# Collect tests.
self.root.collectTests(global_ctx)
# Add missing link dependencies.
self.root.addMissingDependencies(global_ctx)
# Fix link dependencies.
self.root.autofixDependencies(global_ctx)
# Add missing subdirectories.
self.root.addMissingSubdirectories(global_ctx)
# Rearrange the order of cmake commands.
self.root.layoutCMakeLists()
def writeCMakeLists(self):
self.root.writeCMakeLists()
################################################################################
################################# Entry Point ##################################
################################################################################
if __name__ == "__main__":
document = DocumentRoot()
document.autofixCMakeLists()
document.writeCMakeLists()