3 """Helps to keep BUILD.gn files in sync with the corresponding CMakeLists.txt.
5 For each BUILD.gn file in the tree, checks if the list of cpp files in
6 it is identical to the list of cpp files in the corresponding CMakeLists.txt
7 file, and prints the difference if not.
9 Also checks that each CMakeLists.txt file below unittests/ folders that define
10 binaries have corresponding BUILD.gn files.
12 If --write is passed, tries to write modified .gn files and adds one git
13 commit for each cmake commit this merges. If an error is reported, the state
14 of HEAD is unspecified; run `git reset --hard origin/main` if this happens.
17 from collections
import defaultdict
24 def patch_gn_file(gn_file
, add
, remove
):
25 with
open(gn_file
) as f
:
26 gn_contents
= f
.read()
28 srcs_tok
= "sources = ["
29 tokloc
= gn_contents
.find(srcs_tok
)
30 while gn_contents
.startswith("sources = []", tokloc
):
31 tokloc
= gn_contents
.find(srcs_tok
, tokloc
+ 1)
33 raise ValueError(gn_file
+ ": No source list")
34 if gn_contents
.find(srcs_tok
, tokloc
+ 1) != -1:
35 raise ValueError(gn_file
+ ": Multiple source lists")
36 if gn_contents
.find("# NOSORT", 0, tokloc
) != -1:
37 raise ValueError(gn_file
+ ": Found # NOSORT, needs manual merge")
38 tokloc
+= len(srcs_tok
)
40 gn_contents
= gn_contents
[:tokloc
] + ('"%s",' % a
) + gn_contents
[tokloc
:]
42 gn_contents
= gn_contents
.replace('"%s",' % r
, "")
43 with
open(gn_file
, "w") as f
:
47 gn
= os
.path
.join(os
.path
.dirname(__file__
), "..", "gn.py")
48 subprocess
.check_call([sys
.executable
, gn
, "format", "-q", gn_file
])
51 def sync_source_lists(write
):
52 # Use shell=True on Windows in case git is a bat file.
54 subprocess
.check_call(["git"] + args
, shell
=os
.name
== "nt")
57 return subprocess
.check_output(
58 ["git"] + args
, shell
=os
.name
== "nt", universal_newlines
=True
61 gn_files
= git_out(["ls-files", "*BUILD.gn"]).splitlines()
63 # Matches e.g. | "foo.cpp",|, captures |foo| in group 1.
64 gn_cpp_re
= re
.compile(r
'^\s*"([^$"]+\.(?:cpp|c|h|S))",$', re
.MULTILINE
)
65 # Matches e.g. | bar_sources = [ "foo.cpp" ]|, captures |foo| in group 1.
66 gn_cpp_re2
= re
.compile(
67 r
'^\s*(?:.*_)?sources \+?= \[ "([^$"]+\.(?:cpp|c|h|S))" ]$', re
.MULTILINE
69 # Matches e.g. | foo.cpp|, captures |foo| in group 1.
70 cmake_cpp_re
= re
.compile(r
"^\s*([A-Za-z_0-9./-]+\.(?:cpp|c|h|S))$", re
.MULTILINE
)
72 changes_by_rev
= defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
74 def find_gitrev(touched_line
, in_file
):
75 # re.escape() escapes e.g. '-', which works in practice but has
76 # undefined behavior according to the POSIX extended regex spec.
77 posix_re_escape
= lambda s
: re
.sub(r
"([.[{()\\*+?|^$])", r
"\\\1", s
)
83 # `\<` / `\>` cause issues on Windows (and is a GNU extension).
84 # `\b` is a GNU extension and stopped working in Apple Git-143
86 # `[:space:]` is over 10x faster than `^[:alnum:]` and hopefully
88 r
"-S[[:space:]]%s[[:space:]]" % posix_re_escape(touched_line
),
91 return git_out(cmd
).rstrip()
93 # Collect changes to gn files, grouped by revision.
94 for gn_file
in gn_files
:
95 # The CMakeLists.txt for llvm/utils/gn/secondary/foo/BUILD.gn is
96 # at foo/CMakeLists.txt.
97 strip_prefix
= "llvm/utils/gn/secondary/"
98 if not gn_file
.startswith(strip_prefix
):
100 cmake_file
= os
.path
.join(
101 os
.path
.dirname(gn_file
[len(strip_prefix
) :]), "CMakeLists.txt"
103 if not os
.path
.exists(cmake_file
):
106 def get_sources(source_re
, text
):
107 return set([m
.group(1) for m
in source_re
.finditer(text
)])
109 gn_cpp
= get_sources(gn_cpp_re
, open(gn_file
).read())
110 gn_cpp |
= get_sources(gn_cpp_re2
, open(gn_file
).read())
111 cmake_cpp
= get_sources(cmake_cpp_re
, open(cmake_file
).read())
113 if gn_cpp
== cmake_cpp
:
116 def by_rev(files
, key
):
118 rev
= find_gitrev(f
, cmake_file
)
119 changes_by_rev
[rev
][gn_file
][key
].append(f
)
121 by_rev(sorted(cmake_cpp
- gn_cpp
), "add")
122 by_rev(sorted(gn_cpp
- cmake_cpp
), "remove")
124 # Output necessary changes grouped by revision.
125 for rev
in sorted(changes_by_rev
):
126 commit_url
= 'https://github.com/llvm/llvm-project/commit'
127 print("[gn build] Port {0} -- {1}/{0}".format(rev
, commit_url
))
128 for gn_file
, data
in sorted(changes_by_rev
[rev
].items()):
129 add
= data
.get("add", [])
130 remove
= data
.get("remove", [])
132 patch_gn_file(gn_file
, add
, remove
)
133 git(["add", gn_file
])
137 print(" add:\n" + "\n".join(' "%s",' % a
for a
in add
))
139 print(" remove:\n " + "\n ".join(remove
))
142 git(["commit", "-m", "[gn build] Port %s" % rev
])
146 return bool(changes_by_rev
) and not write
149 def sync_unittests():
150 # Matches e.g. |add_llvm_unittest_with_input_files|.
151 unittest_re
= re
.compile(r
"^add_\S+_unittest", re
.MULTILINE
)
153 checked
= ["bolt", "clang", "clang-tools-extra", "lld", "llvm"]
156 for root
, _
, _
in os
.walk(os
.path
.join(c
, "unittests")):
157 cmake_file
= os
.path
.join(root
, "CMakeLists.txt")
158 if not os
.path
.exists(cmake_file
):
160 if not unittest_re
.search(open(cmake_file
).read()):
161 continue # Skip CMake files that just add subdirectories.
162 gn_file
= os
.path
.join("llvm/utils/gn/secondary", root
, "BUILD.gn")
163 if not os
.path
.exists(gn_file
):
166 "missing GN file %s for unittest CMake file %s"
167 % (gn_file
, cmake_file
)
173 src
= sync_source_lists(len(sys
.argv
) > 1 and sys
.argv
[1] == "--write")
174 tests
= sync_unittests()
179 if __name__
== "__main__":