-
Notifications
You must be signed in to change notification settings - Fork 10
/
validate_cmakelists.py
executable file
·522 lines (485 loc) · 23.8 KB
/
validate_cmakelists.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
#!/usr/bin/env python
"""Script to do basic sanity checking for target_link_libraries() commands in
CMakeLists.txt files.
Scans C++ sources specified in add_library() commands for includes that look
like they are in the Quickstep source tree, then makes sure that the
corresponding libraries appear in the target_link_libraries() command for the
library.
TODO List / Known Issues & Limitations:
- Script skips over targets that are built conditionally (i.e. that have
multiple add_library() commands) and just prints a warning to the user.
- Script only validates libraries, not executables.
- Script only checks quickstep includes and libraries, so it will not
detect missing third party libraries.
"""
# 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.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import os
import sys
# Don't scan these directories for quickstep modules.
EXCLUDED_TOP_LEVEL_DIRS = ["build", "third_party"]
# Explicitly ignored dependencies (special headers with no other quickstep
# dependencies).
IGNORED_DEPENDENCIES = frozenset(
["quickstep_cli_LineReaderDumb",
"quickstep_cli_LineReaderLineNoise",
"quickstep_cli_NetworkCli.grpc_proto",
"quickstep_storage_DataExchange.grpc_proto",
"quickstep_threading_WinThreadsAPI",
"quickstep_utility_textbasedtest_TextBasedTest",
"quickstep_utility_textbasedtest_TextBasedTestDriver",
"quickstep_storage_bitweaving_BitWeavingHIndexSubBlock",
"quickstep_storage_bitweaving_BitWeavingIndexSubBlock",
"quickstep_storage_bitweaving_BitWeavingVIndexSubBlock"])
# States when scanning a CMakeLists.txt file.
CMAKE_SCANNING_NONE = 0
CMAKE_SCANNING_LIBRARY = 1
CMAKE_SCANNING_TARGET_LINK_LIBRARIES = 2
CMAKE_SCANNING_IGNORE = 3
def convert_path_to_targetname(include_path):
"""Convert an included header file's path to a quickstep library target in
cmake.
Args:
include_path (str): A header file path taken from a C++ include
statement.
Returns:
str: The target name in CMake that corresponds to the specified header.
"""
path_components = include_path.split("/")
for idx in range(len(path_components) - 1):
path_components[idx] = path_components[idx].replace("_", "")
if path_components[-1].endswith("_gen.hpp"):
# Generated header (e.g. parser or lexer).
path_components[-1] = path_components[-1][:-8]
elif path_components[-1].endswith(".hpp"):
# Regular header.
path_components[-1] = path_components[-1][:-4]
elif path_components[-1].endswith(".pb.h"):
# Generated protobuf header.
path_components[-1] = path_components[-1][:-5] + "_proto"
return "quickstep_" + "_".join(path_components)
def convert_proto_path_to_targetname(import_path):
"""Convert an imported proto's path to a quickstep library target in CMake.
Args:
import_path (str): A proto definition file path taken from a protobuf
import statement.
Returns:
str: The target name in CMake that corresponds to the specified proto
definition.
"""
path_components = import_path.split("/")
for idx in range(len(path_components) - 1):
path_components[idx] = path_components[idx].replace("_", "")
if path_components[-1].endswith(".proto"):
path_components[-1] = path_components[-1][:-6] + "_proto"
return "quickstep_" + "_".join(path_components)
def get_module_targetname_for_cmakelists(cmakelists_filename):
"""Determine what the name for the all-in-one module target should be based
on the CMakeLists.txt filename with path.
Args:
cmakelists_filename (str): CMakeLists.txt filename with path from
quickstep root.
Returns:
str: The target name in CMake that corresponds to the special
all-in-one library for the module described by the CMakeLists.txt
file.
"""
components = []
(head, tail) = os.path.split(cmakelists_filename)
while head != "":
(head, tail) = os.path.split(head)
if tail != ".":
components.append(tail.replace("_", ""))
components.append("quickstep")
components.reverse()
return "_".join(components)
def get_dependency_set_from_cpp_src(src_filename, qs_module_dirs):
"""Read the C++ source file at 'src_filename' and return a set of all
quickstep libraries it includes headers for.
Args:
src_filename (str): A path to a C++ source file (may be header or
implementation).
qs_module_dirs (List[str]): List of directories for top-level quickstep
modules
Returns:
Set[str]: A set of CMake target names for the quickstep library targets
that the C++ file includes.
"""
dependency_set = set()
with open(src_filename, "r") as src_file:
for line in src_file:
if line.startswith("#include \""):
include_filename = line[len("#include \""):]
include_filename = (
include_filename[:include_filename.find("\"")])
# Skip over CMake-generated config headers and -inl companion
# headers.
if not (include_filename.endswith("Config.h")
or include_filename.endswith("-inl.hpp")):
for module_dir in qs_module_dirs:
if include_filename.startswith(module_dir):
dependency_set.add(
convert_path_to_targetname(include_filename))
break
return dependency_set
def get_dependency_set_from_proto_src(src_filename, qs_module_dirs):
"""Read the protobuf definition file at 'src_filename' and return a set of
all other Quickstep proto libraries it imports.
Args:
src_filename (str): A path to a proto definition file.
qs_module_dirs (List[str]): List of directories for top-level quickstep
modules
Returns:
Set[str]: A set of CMake target names for the quickstep library targets
that the proto file imports.
"""
dependency_set = set()
with open(src_filename, "r") as src_file:
for line in src_file:
if line.startswith("import \""):
import_filename = line[len("import \""):]
import_filename = import_filename[:import_filename.find("\"")]
for module_dir in qs_module_dirs:
if import_filename.startswith(module_dir):
dependency_set.add(
convert_proto_path_to_targetname(import_filename))
break
return dependency_set
def process_add_library(qs_module_dirs,
directory,
add_library_args,
deps_from_includes,
skipped_targets,
generated_targets):
"""Process a CMake add_library() command while scanning a CMakeLists.txt
file.
Args:
qs_module_dirs (List[str]): List of directories for top-level quickstep
modules
directory (str): The directory that the CMakeLists.txt file we are
currently scanning resides in.
add_library_args (str): The arguments to an add_library() command in
CMakeLists.txt
deps_from_includes (Map[str, Set[str]]): A map from a CMake target name
to the set of other CMake targets it depends on, deduced based on
what headers the C++/proto sources for the target include. A new
entry will be added to this map for the target specified by the
add_library() command.
skipped_targets (Set[str]): A set of CMake target names that have been
skipped for dependency checking because multiple add_library()
commands specified the same target name. This probably means that
the target in question is built differently depending on some
configuration options or platform checks.
generated_targets (Set[str]): A set of CMake target names that appear
to be built from dynamically-generated source code that we can't
scan. Note, however, that we can and do scan proto definitions and
flex/bison sources for dependencies. An entry will be added to this
set of the given add_library() command references unscannable
generated sources.
"""
components = add_library_args.split()
if components[0].startswith("quickstep"):
if components[0] in deps_from_includes:
skipped_targets.add(components[0])
deps_from_includes[components[0]] = set()
return
deps = set()
for src_filename in components[1:]:
if src_filename.startswith("${"):
if (src_filename.endswith("proto_srcs}")
or src_filename.endswith("proto_hdrs}")):
# Scan protobuf definition instead of C++ source.
#
# src_filename has the form module_File_proto_srcs, so we
# split it by '_' and get the third-from-last part (i.e.
# the base filename without extension).
src_filename = src_filename.split("_")[-3] + ".proto"
full_src_filename = os.path.join(directory, src_filename)
deps.update(
get_dependency_set_from_proto_src(full_src_filename,
qs_module_dirs))
continue
elif src_filename.startswith("${BISON_"):
# Scan Bison parser source.
src_filename = (
src_filename[len("${BISON_"):-len("_OUTPUTS}")]
+ ".ypp")
elif src_filename.startswith("${FLEX_"):
# Scan Flex lexer source.
src_filename = (
src_filename[len("${FLEX_"):-len("_OUTPUTS}")]
+ ".lpp")
else:
generated_targets.add(components[0])
return
elif src_filename.startswith("\"${CMAKE_CURRENT_SOURCE_DIR}/"):
src_filename = src_filename[
len("\"${CMAKE_CURRENT_SOURCE_DIR}/"):-1]
full_src_filename = os.path.join(directory, src_filename)
deps.update(get_dependency_set_from_cpp_src(full_src_filename,
qs_module_dirs))
deps_from_includes[components[0]] = deps
def process_target_link_libraries(target_link_libraries_args,
deps_in_cmake):
"""Process a CMake target_link_libraries() while scanning a CMakeLists.txt
file.
Args:
target_link_libraries_args (str): The arguments to a
target_link_libraries() command in CMakeLists.txt
deps_in_cmake (Map[str, Set[str]]): A map of CMake target names to
their sets of dependencies (also CMake target names) specified by
target_link_libraries() commands. If the target being processed
already has an entry in the map, its set will be expanded with any
additional dependencies, otherwise a new entry will be created with
all the dependencies from the current target_link_libraries()
command. This way, if multiple target_link_libraries() commands are
processed for the same target, we will build up the union of all
dependencies for it (just like CMake does).
"""
components = target_link_libraries_args.split()
if components[0].startswith("quickstep"):
deps = set()
# Intentionally count the first part for self-includes
for component in components:
if component.startswith("quickstep"):
deps.add(component)
if components[0] in deps_in_cmake:
deps_in_cmake[components[0]].update(deps)
else:
deps_in_cmake[components[0]] = deps
def process_cmakelists_file(cmakelists_filename, qs_module_dirs):
"""Scan a CMakeLists.txt file and report any mistakes (missing or
superfluous dependencies in target_link_libraries() commands).
This function will deduce what other libraries a given library target
should depend on based on what headers are included in its source code. It
will then collect the set of link dependencies actually specified in
target_link_libraries() commands, and will print warnings about libraries
that appear in one set but not the other.
Args:
cmakelists_filename (str): The path to a CMakeLists.txt file to scan
and validate.
qs_module_dirs (List[str]): List of directories for top-level quickstep
modules.
Returns:
Tuple[Set[str], Set[str], Set[str]]: First element is the set of
targets that failed validation because they had missing and/or
superfluous dependencies. Second element is the set of targets
that were skipped over because they had multiple add_library()
commands (probably because they are built differently depending on
configuration options or platform checks). Third element is the
set of targets that were skipped because they appear to be built
from dynamically-generated source code (although proto definitions
and flex/bison sources are detected and scannned for dependencies).
"""
directory = os.path.dirname(cmakelists_filename)
module_targetname = get_module_targetname_for_cmakelists(
cmakelists_filename)
deps_from_includes = {}
deps_in_cmake = {}
validation_failed_targets = set()
skipped_targets = set()
generated_targets = set()
scan_state = CMAKE_SCANNING_NONE
previous_state = CMAKE_SCANNING_NONE
stitched_string = ""
with open(cmakelists_filename, "r") as cmakelists_file:
for line in cmakelists_file:
if ("CMAKE_VALIDATE_IGNORE_BEGIN" in line and
scan_state != CMAKE_SCANNING_IGNORE):
previous_state = scan_state
scan_state = CMAKE_SCANNING_IGNORE
continue
if scan_state == CMAKE_SCANNING_IGNORE:
if "CMAKE_VALIDATE_IGNORE_END" in line:
scan_state = previous_state
elif "CMAKE_VALIDATE_IGNORE_BEGIN" in line:
print("Nested IGNORE_BEGIN directives found in: "
+ cmakelists_filename + ", exiting")
exit(-1)
else:
continue
elif scan_state == CMAKE_SCANNING_NONE:
add_library_pos = line.find("add_library(")
if add_library_pos != -1:
scan_state = CMAKE_SCANNING_LIBRARY
stitched_string = (
line[add_library_pos + len("add_library("):])
closing_paren_pos = stitched_string.find(")")
if closing_paren_pos != -1:
stitched_string = stitched_string[:closing_paren_pos]
process_add_library(qs_module_dirs,
directory,
stitched_string,
deps_from_includes,
skipped_targets,
generated_targets)
stitched_string = ""
scan_state = CMAKE_SCANNING_NONE
else:
target_link_libraries_pos = line.find(
"target_link_libraries(")
if target_link_libraries_pos != -1:
scan_state = CMAKE_SCANNING_TARGET_LINK_LIBRARIES
stitched_string = (
line[target_link_libraries_pos
+ len("target_link_libraries("):])
closing_paren_pos = stitched_string.find(")")
if closing_paren_pos != -1:
stitched_string = (
stitched_string[:closing_paren_pos])
process_target_link_libraries(stitched_string,
deps_in_cmake)
stitched_string = ""
scan_state = CMAKE_SCANNING_NONE
elif scan_state == CMAKE_SCANNING_LIBRARY:
closing_paren_pos = line.find(")")
if closing_paren_pos == -1:
stitched_string += line
else:
stitched_string += line[:closing_paren_pos]
process_add_library(qs_module_dirs,
directory,
stitched_string,
deps_from_includes,
skipped_targets,
generated_targets)
stitched_string = ""
scan_state = CMAKE_SCANNING_NONE
elif scan_state == CMAKE_SCANNING_TARGET_LINK_LIBRARIES:
closing_paren_pos = line.find(")")
if closing_paren_pos == -1:
stitched_string += line
else:
stitched_string += line[:closing_paren_pos]
process_target_link_libraries(stitched_string,
deps_in_cmake)
stitched_string = ""
scan_state = CMAKE_SCANNING_NONE
# After scanning, report any missing dependencies.
for target, include_deps in iter(deps_from_includes.items()):
if target in skipped_targets:
pass
elif len(include_deps) != 0:
if target not in deps_in_cmake:
if not (target in include_deps and len(include_deps) == 1):
validation_failed_targets.add(target)
print("Missing target_link_libraries() for " + target + ":")
for dep in sorted(include_deps):
print("\t" + dep)
else:
missing_deps = (include_deps
- deps_in_cmake[target]
- IGNORED_DEPENDENCIES)
if len(missing_deps) != 0:
validation_failed_targets.add(target)
print("Missing target_link_libraries() for " + target + ":")
for dep in sorted(missing_deps):
print("\t" + dep)
elif target == module_targetname:
# Special case hack for module all-in-one library
missing_deps = (frozenset(deps_from_includes.keys())
- deps_in_cmake[target])
# Filter out test-only libraries.
true_missing_deps = set()
for dep in missing_deps:
if not dep.startswith(module_targetname + "_tests"):
true_missing_deps.add(dep)
if len(true_missing_deps) != 0:
validation_failed_targets.add(target)
print("Missing target_link_libraries() for " + target + ":")
for dep in sorted(true_missing_deps):
print("\t" + dep)
# Also report possibly superfluous extra dependencies.
for target, cmake_deps in iter(deps_in_cmake.items()):
if (target not in skipped_targets) and (target in deps_from_includes):
extra_deps = cmake_deps - deps_from_includes[target]
if target in extra_deps:
extra_deps.remove(target)
if len(extra_deps) != 0 and target != module_targetname:
validation_failed_targets.add(target)
print("Possibly superfluous target_link_libraries() for "
+ target + ":")
for dep in sorted(extra_deps):
print("\t" + dep)
return (validation_failed_targets, skipped_targets, generated_targets)
def main(cmakelists_to_process):
"""Main function for script which scans and analyzes CMakeLists.txt files
and prints warnings about missing or superfluous dependencies, and about
targets that could not be automatically scanned and should be manually
checked.
Args:
cmakelists_to_process (List[str]): A list of relative paths of
CMakeLists.txt files to scan and report on. If empty, this function
will instead recursively walk the current working directory and
scan every CMakeLists.txt file that it finds.
Returns:
int: The total number of targets that failed validation because of
missing or superfluous dependencies.
"""
if not os.path.isfile("validate_cmakelists.py"):
print("WARNING: you don't appear to be running in the root quickstep "
"source directory. Don't blame me if something goes wrong.")
qs_module_dirs = []
for filename in os.listdir("."):
if (os.path.isdir(filename)
and not filename.startswith(".")
and filename not in EXCLUDED_TOP_LEVEL_DIRS):
qs_module_dirs.append(filename)
if len(cmakelists_to_process) == 0:
for (dirpath, dirnames, filenames) in os.walk('.'):
skip = False
for excluded_dir in EXCLUDED_TOP_LEVEL_DIRS:
if dirpath.startswith(excluded_dir):
skip = True
break
if not skip:
if "CMakeLists.txt" in filenames:
cmakelists_to_process.append(
os.path.join(dirpath, "CMakeLists.txt"))
global_validation_failed_targets = set()
global_skipped_targets = set()
global_generated_targets = set()
for cmakelists_filename in cmakelists_to_process:
(local_validation_failed_targets,
local_skipped_targets,
local_generated_targets) = (
process_cmakelists_file(cmakelists_filename, qs_module_dirs))
global_validation_failed_targets.update(
local_validation_failed_targets)
global_skipped_targets.update(local_skipped_targets)
global_generated_targets.update(local_generated_targets)
if len(global_skipped_targets) != 0:
print("WARNING: The following targets had multiple add_library() "
+ "commands and were NOT checked by this script (they should "
+ "be manually checked):")
for target in sorted(global_skipped_targets):
print("\t" + target)
if len(global_generated_targets) != 0:
print("INFO: The add_library() commands for the following targets "
+ "appear to reference generated sources, so they were not "
+ "checked):")
for target in sorted(global_generated_targets):
print("\t" + target)
return len(global_validation_failed_targets)
if __name__ == "__main__":
if main(sys.argv[1:]) > 0:
sys.exit(1)
else:
sys.exit(0)