69 """Map an object path back to its source.
71 Two shapes, and getting only the first is how src/widgets -- an entire CMake target --
72 silently reported zero translation units:
74 build/src/CMakeFiles/lib_ansel.dir/common/bar.c.o -> src/common/bar.c
75 build/src/widgets/CMakeFiles/ansel_widgets.dir/bar.c.o -> src/widgets/bar.c
77 The general rule covers both: drop the build directory, then drop the
78 `CMakeFiles/<target>.dir/` component wherever it sits, and what is left is the path
79 relative to the project root.
81 rel = os.path.relpath(obj, build_dir).replace(
"\\",
"/")
82 rel = re.sub(
r"CMakeFiles/[^/]*\.dir/",
"", rel)
83 if rel.endswith(OBJ_SUFFIX):
84 rel = rel[:-len(OBJ_SUFFIX)]
85 if os.path.isfile(os.path.join(REPO, rel)):
93 state_syms = collections.defaultdict(list)
94 calls = collections.defaultdict(set)
98 sys.exit(f
"error: no {OBJ_SUFFIX} files under {build_dir}")
105 row = re.compile(
r"^[0-9a-fA-F]+\s+(?P<flags>.{7})\s+(?P<section>\S+)\t[0-9a-fA-F]+\s+"
106 r"(?:\.(?:hidden|protected|internal)\s+)?(?P<name>\S+)")
113 coff_row = re.compile(
r"^\[\s*\d+\]\(sec\s+(?P<sec>-?\d+)\)\(fl [^)]*\)\(ty\s+[0-9a-fA-F]+\)"
114 r"\(scl\s+\d+\)\s+\(nx (?P<nx>\d+)\)\s+0x[0-9a-fA-F]+\s+(?P<name>\S+)")
121 out = subprocess.run([
"objdump",
"-t", obj], capture_output=
True, text=
True).stdout
122 except FileNotFoundError:
123 sys.exit(
"error: objdump not found")
129 for line
in out.splitlines():
132 if m.group(
"name") != m.group(
"section"):
133 symbols.append((m.group(
"name"), m.group(
"section")))
135 m = coff_row.match(line)
138 sec, name = int(m.group(
"sec")), m.group(
"name")
139 if sec > 0
and m.group(
"nx") ==
"1" and name.startswith(
"."):
140 section_names[sec] = name
144 symbols.append((name[len(
"__imp_"):]
if name.startswith(
"__imp_")
else name,
147 symbols.append((name, section_names.get(sec,
"")))
150 for name, section
in symbols:
151 if NOISE.match(name):
153 if section ==
"*ABS*":
155 if section ==
"*UND*":
157 elif READONLY_SECTIONS.match(section):
160 defines.setdefault(name, src)
161 elif MUTABLE_SECTIONS.match(section):
163 state_syms[src].append(name)
164 elif section.startswith(
".text"):
165 defines.setdefault(name, src)
166 return defines, state_syms, calls
170 """A mutable symbol defined in more than one object came from a header.
172 `static const char *dt_supported_extensions[]` in config.h and `loaders_info[]` in
173 common/image.h are emitted into every translation unit that includes them, so they show up
174 as "own state" for 170 files that never heard of them. They are still mutable storage --
175 `const char *` makes the pointee const, not the array -- and each copy is independently
176 writable, which is worth fixing at the header. But they are the header's problem, not each
177 includer's, and counting them per-includer buries every real finding.
179 seen = collections.Counter()
180 for syms
in state_syms.values():
181 seen.update(set(syms))
187 for dirpath, dirnames, filenames
in os.walk(os.path.join(REPO,
"src")):
188 dirnames[:] = [d
for d
in dirnames
if d
not in (
"external",
"build")]
190 if fn.endswith((
".h",
".hpp",
".cmake.h")):
192 with open(os.path.join(dirpath, fn), errors=
"ignore")
as fh:
193 header_text.append(fh.read())
196 headers =
"\n".join(header_text)
198 header_defined = set()
199 for sym, n
in seen.items():
202 bare = re.sub(
r"^_ZL\d+",
"", sym)
203 bare = re.sub(
r"\.\d+$",
"", bare)
206 r"^\s*(?:static\s+)?[A-Za-z_][\w \t*]*\b" + re.escape(bare) +
r"\s*(?:\[|=)",
208 r"^\s*}\s*" + re.escape(bare) +
r"\s*\[",
210 if any(re.search(pat, headers, re.M)
for pat
in patterns):
211 header_defined.add(sym)
212 own = {src: [s
for s
in syms
if s
not in header_defined]
213 for src, syms
in state_syms.items()}
214 return {k: v
for k, v
in own.items()
if v}, sorted(header_defined)
236 build =
"build-debug"
237 if "--build" in sys.argv:
238 build = sys.argv[sys.argv.index(
"--build") + 1]
240 if "--dir" in sys.argv:
241 only = sys.argv[sys.argv.index(
"--dir") + 1]
243 defines, raw_state, calls =
scan(os.path.join(REPO, build))
245 stateful =
propagate(defines, state_syms, calls)
247 files = sorted(set(list(calls) + list(state_syms)))
249 files = [f
for f
in files
if f.startswith(only)]
251 if "--json" in sys.argv:
252 print(json.dumps({f: {
"stateful": f
in stateful,
253 "own_state": sorted(state_syms.get(f, [])),
254 "chain": stateful.get(f, [])}
for f
in files}, indent=2))
257 direct = [f
for f
in files
if state_syms.get(f)]
258 indirect = [f
for f
in files
if f
in stateful
and not state_syms.get(f)]
259 clean = [f
for f
in files
if f
not in stateful]
261 scope = only
or "src/"
262 print(f
"{len(files)} translation unit(s) under {scope}: "
263 f
"{len(clean)} stateless, {len(direct)} with own state, {len(indirect)} reaching state\n")
265 if header_defined
and not only:
266 print(f
"--- MUTABLE DATA DEFINED IN HEADERS ({len(header_defined)}) ---")
267 print(
" One writable copy per including translation unit. Not any single file's")
268 print(
" state; fix at the header (usually a missing second const).")
269 for sym
in header_defined:
274 print(
"--- OWN STATE (mutable storage at file scope) ---")
277 print(f
" {f} ({len(syms)}): {', '.join(sorted(syms)[:6])}"
278 + (
" ..." if len(syms) > 6
else ""))
281 print(
"--- REACHES STATE (through what it calls) ---")
285 print(f
" via {' -> '.join(chain[1:4])}" + (
" ..." if len(chain) > 4
else ""))
288 print(f
"--- STATELESS ({len(clean)}) ---")