Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
include_unused.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Find `#include`s that a file does not need.
3
4Two stages, because neither alone is trustworthy:
5
6 1. STATIC (fast, whole tree, no build). Index every project header by the names it
7 DECLARES (macros, typedefs, tags, enum constants, function and variable
8 declarations). For each file F and each header H that F includes directly, if F
9 mentions none of H's own names, H is a *candidate* for removal.
10
11 This over-reports: F may include H legitimately to reach something H itself
12 includes (a transitive dependency). That is exactly the coupling we want to make
13 explicit, but removing such an include still breaks the build, so a candidate is
14 a question, never a verdict.
15
16 2. VERIFY (slow, exact w.r.t. the current build config). Actually comment the
17 include out, recompile the affected object with ninja, and keep the removal only
18 if it still compiles. Run this on the candidates from stage 1.
19
20The verify stage cannot prove an include is unneeded on OTHER platforms: an include
21used only inside `#ifdef _WIN32` looks removable on Linux and is not. Candidates whose
22symbol use is under a conditional are flagged `platform-guarded` and must never be
23removed on the strength of a Linux-only build. See doc/include-hygiene-roadmap.md.
24
25Usage:
26 python3 tools/include_unused.py # static pass, summary
27 python3 tools/include_unused.py --headers # only .h files
28 python3 tools/include_unused.py --sources # only .c/.cc files
29 python3 tools/include_unused.py --file src/iop/x.c # one file, verbose
30 python3 tools/include_unused.py --json out.json # machine-readable
31 python3 tools/include_unused.py --verify --limit 40 # empirically test candidates
32"""
33import json
34import os
35import re
36import subprocess
37import sys
38from collections import defaultdict
39
40SRC = 'src'
41BUILD = 'build'
42
43INCLUDE_RE = re.compile(r'^([ \t]*#[ \t]*include[ \t]+")([^"]+)(".*)$', re.M)
44IDENT_RE = re.compile(r'\b[A-Za-z_][A-Za-z0-9_]*\b')
45
46# Headers included for their SIDE EFFECTS, not for names they declare. A static pass
47# will always call these unused; they must never be reported.
48SIDE_EFFECT_HEADERS = {
49 'config.h',
50 'common/poison.h', # #pragma-poisons forbidden libc calls
51 'win/win.h', # #undefs legacy windows.h macros
52 'common/module_api.h', # X-macro, generates struct members
53 'views/view_api.h',
54 'libs/lib_api.h',
55 'imageio/format/imageio_format_api.h',
56 'imageio/storage/imageio_storage_api.h',
57 'external/ThreadSafetyAnalysis.h',
58 'darktable.h', # the orchestrator: handled by its own migration
59}
60
61# Declaration shapes. Deliberately generous: a missed declaration turns into a false
62# "unused" report, which the verify stage then has to spend a compile to reject.
63DECL_PATTERNS = [
64 re.compile(r'^[ \t]*#[ \t]*define[ \t]+([A-Za-z_][A-Za-z0-9_]*)', re.M),
65 re.compile(r'^[ \t]*}[ \t]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*;', re.M), # } dt_foo_t;
66 re.compile(r'\btypedef\b[^;{]*?\b([A-Za-z_][A-Za-z0-9_]*)[ \t]*;', re.M), # typedef X dt_y_t;
67 re.compile(r'\b(?:struct|union|enum)[ \t]+([A-Za-z_][A-Za-z0-9_]*)', re.M),
68 re.compile(r'^[A-Za-z_][A-Za-z0-9_ \t\*]*?\b([A-Za-z_][A-Za-z0-9_]*)[ \t]*\‍(', re.M),
69 re.compile(r'^[ \t]*extern[^;=]*?\b([A-Za-z_][A-Za-z0-9_]*)[ \t]*(?:\[|;)', re.M),
70]
71
72# A file is "platform guarded" if it contains code THIS build does not compile, so a
73# clean local build cannot authorise removing an include that branch needs. Flagging
74# every `#if` (include guards included) would exclude the whole tree; flagging none let
75# three separate breakages reach CI during the darktable.h series.
76#
77# Two distinct cases, and conflating them makes the tool useless:
78# * an OS/compiler conditional (_WIN32, __APPLE__ ...) -- its body is never compiled
79# here, so the file is risky whether or not it has an #else;
80# * a feature conditional (HAVE_OPENCL ...) -- with the feature ON locally the body IS
81# compiled and is verified; only its #else/#elif is unverified.
82OS_MACROS = (r'_WIN32|WIN32|__WIN32__|__MINGW\w*|_MSC_VER|__APPLE__|__MACH__'
83 r'|GDK_WINDOWING_QUARTZ|__FreeBSD__|__NetBSD__|__OpenBSD__|__DragonFly__')
84OS_COND_RE = re.compile(r'^[ \t]*#[ \t]*(?:if|ifdef|ifndef|elif)\b[^\n]*\b(?:' + OS_MACROS + r')\b', re.M)
85COND_RE = re.compile(r'^[ \t]*#[ \t]*(if|ifdef|ifndef|elif|else|endif)\b([^\n]*)', re.M)
86FEATURE_RE = re.compile(r'\b(?:HAVE_[A-Z0-9_]+|__SSE2__|__ARM_NEON|_OPENMP)\b')
87
88
90 """True if the file contains a branch this build does not compile."""
91 if OS_COND_RE.search(text):
92 return True
93 stack = []
94 for m in COND_RE.finditer(text):
95 kind, rest = m.group(1), m.group(2)
96 if kind in ('if', 'ifdef', 'ifndef'):
97 stack.append(bool(FEATURE_RE.search(rest)))
98 elif kind == 'endif':
99 if stack:
100 stack.pop()
101 elif kind in ('else', 'elif'):
102 # the alternative branch of a feature conditional is not compiled here
103 if stack and stack[-1]:
104 return True
105 return False
106
107
108def read(path):
109 with open(path, encoding='utf-8', errors='replace') as fh:
110 return fh.read()
111
112
114 out = []
115 for root, dirs, names in os.walk(SRC):
116 dirs[:] = [d for d in dirs if d != 'external']
117 for n in names:
118 if n.endswith(('.c', '.cc', '.cpp', '.h', '.hpp')):
119 out.append(os.path.join(root, n))
120 return sorted(out)
121
122
124 """Crude but adequate: we only need identifier presence, not exact syntax."""
125 text = re.sub(r'/\*.*?\*/', ' ', text, flags=re.S)
126 text = re.sub(r'//[^\n]*', ' ', text)
127 text = re.sub(r'"(?:\\.|[^"\\])*"', ' ', text)
128 return text
129
130
131ATTRIBUTE_RE = re.compile(r'__attribute__\s*\‍(\‍((?:[^()]|\‍([^()]*\‍))*\‍)\‍)')
132
133
135 names = set()
136 body = strip_comments_and_strings(text)
137 # Drop __attribute__((...)) before looking for declarations: in
138 # static inline __attribute__((always_inline)) void dt_Lab_to_XYZ(...)
139 # the first identifier followed by "(" is __attribute__, so every attributed inline
140 # function was invisible. That made common/colorspaces_inline_conversions.h look as
141 # though it declared nothing, and its consumers were wrongly told they did not need
142 # it -- which broke three files in the CI nofeatures configuration.
143 body = ATTRIBUTE_RE.sub(' ', body)
144 for pat in DECL_PATTERNS:
145 for m in pat.finditer(body):
146 names.add(m.group(1))
147 # enum constants: every identifier inside an enum block
148 for m in re.finditer(r'\benum\b[^{;]*\{(.*?)\}', body, flags=re.S):
149 for ident in IDENT_RE.findall(m.group(1)):
150 names.add(ident)
151 names.discard('')
152 return names
153
154
155def resolve(inc, from_path, known):
156 cand = os.path.normpath(os.path.join(SRC, inc))
157 if cand in known:
158 return cand
159 cand2 = os.path.normpath(os.path.join(os.path.dirname(from_path), inc))
160 if cand2 in known:
161 return cand2
162 return None
163
164
166 files = project_files()
167 known = set(files)
168 text = {p: read(p) for p in files}
169 provides = {p: declared_names(text[p]) for p in files if p.endswith(('.h', '.hpp'))}
170
171 results = {}
172 for p in files:
173 body = strip_comments_and_strings(text[p])
174 used = set(IDENT_RE.findall(body))
175 guarded = has_unbuilt_branch(text[p])
176 cands = []
177 for m in INCLUDE_RE.finditer(text[p]):
178 inc = m.group(2)
179 if inc in SIDE_EFFECT_HEADERS:
180 continue
181 target = resolve(inc, p, known)
182 if target is None or target == p:
183 continue
184 names = provides.get(target)
185 if not names:
186 continue # header declares nothing we can see: stay silent
187 if used & names:
188 continue
189 annotated = bool(re.search(
190 r'^[ \t]*#[ \t]*include[ \t]+"%s"[ \t]*(?://|/\*)' % re.escape(inc),
191 text[p], re.M))
192 cands.append({'include': inc, 'header': target,
193 'platform_guarded': guarded, 'annotated': annotated})
194 if cands:
195 results[p] = cands
196 return results
197
198
199_TARGETS_CACHE = None
200
201
203 global _TARGETS_CACHE
204 if _TARGETS_CACHE is None:
205 try:
206 out = subprocess.run(['ninja', '-C', BUILD, '-t', 'targets', 'all'],
207 capture_output=True, text=True, timeout=300).stdout
208 except (OSError, subprocess.SubprocessError):
209 out = ''
210 _TARGETS_CACHE = [ln.split(':')[0].strip() for ln in out.splitlines()]
211 return _TARGETS_CACHE
212
213
215 """Best-effort mapping from a source file to one ninja object target."""
216 base = os.path.basename(path)
217 for tgt in _all_targets():
218 if tgt.endswith(base + '.o') or tgt.endswith(base + '.obj'):
219 return tgt
220 return None
221
222
223def verify(results, limit):
224 """Comment each candidate out, rebuild its object, keep only what still compiles."""
225 confirmed, rejected = [], []
226 tested = 0
227 for path, cands in sorted(results.items()):
228 if not path.endswith(('.c', '.cc', '.cpp')):
229 continue # headers have no object of their own; see roadmap
230 target = ninja_target_for(path)
231 if target is None:
232 continue
233 original = read(path)
234 for c in cands:
235 if tested >= limit:
236 break
237 tested += 1
238 patched = original.replace('#include "%s"' % c['include'],
239 '/* IWYU-TEST */ //#include "%s"' % c['include'], 1)
240 with open(path, 'w', encoding='utf-8') as fh:
241 fh.write(patched)
242 rc = subprocess.run(['ninja', '-C', BUILD, target],
243 capture_output=True, text=True).returncode
244 with open(path, 'w', encoding='utf-8') as fh:
245 fh.write(original)
246 (confirmed if rc == 0 else rejected).append((path, c['include']))
247 subprocess.run(['ninja', '-C', BUILD, target], capture_output=True, text=True)
248 if tested >= limit:
249 break
250 return confirmed, rejected
251
252
253def main():
254 only_h = '--headers' in sys.argv
255 only_c = '--sources' in sys.argv
256 results = analyse()
257
258 if '--file' in sys.argv:
259 want = sys.argv[sys.argv.index('--file') + 1]
260 for inc in results.get(want, []):
261 print('%s: %s%s' % (want, inc['include'],
262 ' [platform-guarded]' if inc['platform_guarded'] else ''))
263 if want not in results:
264 print('%s: no candidates' % want)
265 return 0
266
267 if only_h:
268 results = {k: v for k, v in results.items() if k.endswith(('.h', '.hpp'))}
269 if only_c:
270 results = {k: v for k, v in results.items() if k.endswith(('.c', '.cc', '.cpp'))}
271
272 if '--json' in sys.argv:
273 out = sys.argv[sys.argv.index('--json') + 1]
274 with open(out, 'w', encoding='utf-8') as fh:
275 json.dump(results, fh, indent=1, sort_keys=True)
276 print('wrote %s' % out)
277
278 if '--push-down' in sys.argv:
279 return push_down(results)
280
281 if '--apply' in sys.argv:
282 prefix = sys.argv[sys.argv.index('--prefix') + 1] if '--prefix' in sys.argv else 'src/'
283 return apply_mode(results, prefix, '--include-guarded' not in sys.argv)
284
285 if '--verify' in sys.argv:
286 limit = 20
287 if '--limit' in sys.argv:
288 limit = int(sys.argv[sys.argv.index('--limit') + 1])
289 confirmed, rejected = verify(results, limit)
290 print('\n=== VERIFIED REMOVABLE (compiles without it) ===')
291 for p, i in confirmed:
292 print(' %s: %s' % (p, i))
293 print('\n=== NEEDED AFTER ALL (transitive dependency) ===')
294 for p, i in rejected:
295 print(' %s: %s' % (p, i))
296 print('\n%d removable / %d tested' % (len(confirmed), len(confirmed) + len(rejected)))
297 return 0
298
299 total = sum(len(v) for v in results.values())
300 hdr = sum(len(v) for k, v in results.items() if k.endswith(('.h', '.hpp')))
301 print('candidate unneeded includes: %d in %d files (%d in headers, %d in sources)'
302 % (total, len(results), hdr, total - hdr))
303
304 per_dir = defaultdict(int)
305 for k, v in results.items():
306 per_dir[k.split(os.sep)[1]] += len(v)
307 print('\nby directory:')
308 for d, n in sorted(per_dir.items(), key=lambda kv: -kv[1]):
309 print(' %5d %s' % (n, d))
310
311 per_header = defaultdict(int)
312 for v in results.values():
313 for c in v:
314 per_header[c['include']] += 1
315 print('\nmost often included without being used:')
316 for h, n in sorted(per_header.items(), key=lambda kv: -kv[1])[:20]:
317 print(' %5d %s' % (n, h))
318
319 worst = sorted(results.items(), key=lambda kv: -len(kv[1]))[:15]
320 print('\nfiles with the most candidates:')
321 for p, v in worst:
322 print(' %5d %s' % (len(v), p))
323 return 0
324
325
326
327
328# ---------------------------------------------------------------------------
329# --apply: remove candidates and prove the result still builds.
330#
331# Doing one compile per candidate would be ~760 builds. Instead:
332# 1. strip every candidate in the tranche at once, then run ONE full build;
333# 2. whatever that build implicates, restore -- file by file -- and retry;
334# 3. for each restored file, retry alone (all of its candidates at once), and
335# only if THAT fails fall back to removing its candidates one at a time.
336# The happy path costs one build for a whole directory; the pathological path
337# degenerates to the naive per-include cost for the few files that need it.
338# ---------------------------------------------------------------------------
339
340def _strip(path, incs):
341 """Remove whole #include lines. Returns the ones actually removed.
342
343 Must match the LINE, not the exact string `#include "x"\n`: an include carrying a
344 trailing comment would silently fail to match, and the caller would then report a
345 removal that never happened -- which is how this tool once claimed 7 removals for a
346 3-line diff."""
347 s = read(path)
348 done = []
349 for inc in incs:
350 pat = re.compile(r'^[ \t]*#[ \t]*include[ \t]+"%s"[^\n]*\n' % re.escape(inc), re.M)
351 s, n = pat.subn('', s, count=1)
352 if n:
353 done.append(inc)
354 with open(path, 'w', encoding='utf-8') as fh:
355 fh.write(s)
356 return done
357
358
359def _build(targets=None):
360 cmd = ['ninja', '-C', BUILD, '-k', '0'] + (targets or [])
361 p = subprocess.run(cmd, capture_output=True, text=True)
362 return p.returncode, (p.stdout + p.stderr)
363
364
365def _implicated(log, owned):
366 """Source files in `owned` that the build log blames, directly or through a
367 generated introspection_*.c that textually includes them."""
368 hit = set()
369 for m in re.finditer(r'([\w./\\-]+\.(?:c|cc|cpp|h|hpp))[:(]', log):
370 f = m.group(1).replace('\\', '/')
371 for o in owned:
372 if f.endswith('/' + os.path.basename(o)) or f.endswith(o):
373 hit.add(o)
374 base = os.path.basename(f)
375 if base.startswith('introspection_'):
376 stem = base[len('introspection_'):]
377 for o in owned:
378 if os.path.basename(o) == stem:
379 hit.add(o)
380 return hit
381
382
383def _apply_only(subset, originals, targets):
384 """Restore every touched file, then re-strip just `subset`."""
385 for p, text in originals.items():
386 with open(p, 'w', encoding='utf-8') as fh:
387 fh.write(text)
388 for p in subset:
389 _strip(p, [c['include'] for c in targets[p]])
390
391
392def _bisect_set(files, originals, targets):
393 """Largest subset of `files` whose removals still build. Removals are independent
394 in practice, so recursive halving is sound; the caller re-verifies the union."""
395 if not files:
396 return []
397 _apply_only(files, originals, targets)
398 rc, _ = _build()
399 if rc == 0:
400 return list(files)
401 if len(files) == 1:
402 return []
403 mid = len(files) // 2
404 left = _bisect_set(files[:mid], originals, targets)
405 right = _bisect_set(files[mid:], originals, targets)
406 good = left + right
407 _apply_only(good, originals, targets)
408 rc, _ = _build()
409 if rc != 0: # halves interact: keep the larger half only
410 good = left if len(left) >= len(right) else right
411 _apply_only(good, originals, targets)
412 _build()
413 return good
414
415
416def apply_mode(results, prefix, skip_guarded):
417 targets = {p: v for p, v in results.items() if p.startswith(prefix)}
418 if skip_guarded:
419 targets = {p: v for p, v in targets.items()
420 if not any(c['platform_guarded'] for c in v)}
421 # A trailing comment on an include line is the author stating why it is there --
422 # usually a transitive need the static pass cannot see ("needed by dwt.h",
423 # "for dt_pthread_mutex_t"). Never remove those silently.
424 annotated = [(p, c['include']) for p, v in targets.items() for c in v if c.get('annotated')]
425 targets = {p: [c for c in v if not c.get('annotated')] for p, v in targets.items()}
426 targets = {p: v for p, v in targets.items() if v}
427 for p, i in annotated:
428 print(' skipping annotated include (documented intent): %s: %s' % (p, i))
429 if not targets:
430 print('nothing to do for prefix %r' % prefix)
431 return 0
432
433 originals = {p: read(p) for p in targets}
434 print('applying %d candidate removals across %d files (prefix %r)'
435 % (sum(len(v) for v in targets.values()), len(targets), prefix))
436 for p, cands in targets.items():
437 _strip(p, [c['include'] for c in cands])
438
439 removed = dict(targets)
440 for attempt in range(6):
441 rc, log = _build()
442 if rc == 0:
443 break
444 bad = _implicated(log, list(removed))
445 if not bad:
446 # Blame-mapping is impossible for headers: when foo.h stops including bar.h,
447 # the error lands in some baz.c that used bar.h's symbols through foo.h, and
448 # the log never names foo.h. Fall back to bisecting the FILE SET instead of
449 # giving up: O(bad x log n) builds rather than one per file.
450 print(' build fails and blames nothing we edited -- bisecting the file set')
451 keep = _bisect_set(sorted(removed), originals, targets)
452 removed = {p: targets[p] for p in keep}
453 break
454 print(' round %d: restoring %d implicated file(s)' % (attempt + 1, len(bad)))
455 for p in bad:
456 open(p, 'w', encoding='utf-8').write(originals[p])
457 removed.pop(p, None)
458 else:
459 print('did not converge; reverting all', file=sys.stderr)
460 for p, s in originals.items():
461 open(p, 'w', encoding='utf-8').write(s)
462 _build()
463 return 1
464
465 # Retry each restored file on its own, then per-include.
466 salvaged = 0
467 for p in [f for f in targets if f not in removed]:
468 cands = [c['include'] for c in targets[p]]
469 _strip(p, cands)
470 rc, _ = _build()
471 if rc == 0:
472 removed[p] = targets[p]
473 salvaged += len(cands)
474 continue
475 open(p, 'w', encoding='utf-8').write(originals[p])
476 kept = []
477 for inc in cands:
478 _strip(p, [inc])
479 rc, _ = _build()
480 if rc == 0:
481 kept.append(inc)
482 else:
483 open(p, 'w', encoding='utf-8').write(originals[p])
484 for k in kept:
485 _strip(p, [k])
486 if kept:
487 removed[p] = [{'include': i} for i in kept]
488 salvaged += len(kept)
489
490 rc, _ = _build()
491 total = sum(len(v) for v in removed.values())
492 print('\nREMOVED %d includes from %d files (%d salvaged by bisection); final build rc=%d'
493 % (total, len(removed), salvaged, rc))
494 return 0 if rc == 0 else 1
495
496
497
498
499# ---------------------------------------------------------------------------
500# --push-down: enforce "a header includes only what its own declarations need".
501#
502# tools/include_unused.py --apply keeps an include the header does not need when a
503# CONSUMER is reaching through the header to get it. That is the wrong reason to keep
504# it: the consumer should include it itself. This mode removes it from the header and
505# adds it to whichever translation units actually break.
506#
507# The exception is honoured, not overridden: if EVERY consumer of the header turns out
508# to need the include, pushing it down is pure boilerplate, so it stays in the header.
509# ---------------------------------------------------------------------------
510
512 """Reverse-map ninja object targets back to source files, via compdb."""
513 fmap = {}
514 r = subprocess.run(['ninja', '-C', BUILD, '-t', 'compdb'], capture_output=True, text=True)
515 if r.returncode != 0:
516 return fmap
517 for e in json.loads(r.stdout):
518 out = e.get('output') or ''
519 f = e.get('file') or ''
520 if not out or not f:
521 continue
522 base = os.path.basename(f)
523 if base.startswith('introspection_'):
524 stem = base[len('introspection_'):]
525 for d in ('src/iop/', 'src/libs/', 'src/imageio/format/', 'src/imageio/storage/'):
526 if os.path.exists(d + stem):
527 f = d + stem
528 break
529 fmap[out] = os.path.relpath(f, os.getcwd()) if os.path.isabs(f) else f
530 return fmap
531
532
533def _failing_sources(log, obj2src):
534 out = set()
535 for m in re.finditer(r'^FAILED: (?:\[[^\]]*\] )?(\S+)', log, re.M):
536 src = obj2src.get(m.group(1))
537 if src:
538 out.add(os.path.normpath(src))
539 return out
540
541
542def _consumers_of(header):
543 """Files that include `header` directly (either spelling)."""
544 rel = header[len(SRC) + 1:] if header.startswith(SRC + os.sep) else header
545 base = os.path.basename(header)
546 hits = set()
547 for root, dirs, names in os.walk(SRC):
548 dirs[:] = [d for d in dirs if d != 'external']
549 for n in names:
550 if not n.endswith(('.c', '.cc', '.cpp', '.h', '.hpp')):
551 continue
552 p = os.path.join(root, n)
553 t = read(p)
554 if '#include "%s"' % rel in t or '#include "%s"' % base in t:
555 hits.add(os.path.normpath(p))
556 return hits
557
558
559def push_down(results):
560 headers = {p: v for p, v in results.items() if p.endswith(('.h', '.hpp'))}
561 obj2src = _object_to_source()
562 moved = kept_universal = reverted = 0
563
564 for hdr, cands in sorted(headers.items()):
565 consumers = _consumers_of(hdr)
566 for c in cands:
567 inc = c['include']
568 original_hdr = read(hdr)
569 _strip(hdr, [inc])
570 rc, log = _build()
571 if rc == 0:
572 print(' %s: dropped %s (nobody needed it)' % (hdr, inc))
573 moved += 1
574 continue
575
576 needy = _failing_sources(log, obj2src)
577 c_consumers = {f for f in consumers if f.endswith(('.c', '.cc', '.cpp'))}
578 if needy and c_consumers and needy >= c_consumers:
579 # every consumer needs it -- pushing it down is pure boilerplate
580 open(hdr, 'w', encoding='utf-8').write(original_hdr)
581 _build()
582 print(' %s: KEPT %s (all %d consumers need it)' % (hdr, inc, len(c_consumers)))
583 kept_universal += 1
584 continue
585
586 patched = []
587 for f in sorted(needy):
588 t = read(f)
589 if '#include "%s"' % inc in t:
590 continue
591 lines = [l for l in t.splitlines() if l.startswith('#include "')]
592 if not lines:
593 continue
594 anchor = lines[0] + '\n'
595 t = t.replace(anchor, anchor + '#include "%s"\n' % inc, 1)
596 with open(f, 'w', encoding='utf-8') as fh:
597 fh.write(t)
598 patched.append(f)
599
600 rc2, _ = _build()
601 if rc2 == 0:
602 print(' %s: moved %s down into %d consumer(s)' % (hdr, inc, len(patched)))
603 moved += 1
604 else:
605 open(hdr, 'w', encoding='utf-8').write(original_hdr)
606 for f in patched:
607 t = read(f).replace('#include "%s"\n' % inc, '', 1)
608 with open(f, 'w', encoding='utf-8') as fh:
609 fh.write(t)
610 _build()
611 print(' %s: reverted %s (did not converge)' % (hdr, inc))
612 reverted += 1
613
614 rc, _ = _build()
615 print('\nmoved=%d kept-as-universal=%d reverted=%d final build rc=%d'
616 % (moved, kept_universal, reverted, rc))
617 return 0 if rc == 0 else 1
618
619
620if __name__ == '__main__':
621 sys.exit(main())
apply_mode(results, prefix, skip_guarded)
verify(results, limit)
_build(targets=None)
_bisect_set(files, originals, targets)
_implicated(log, owned)
_strip(path, incs)
strip_comments_and_strings(text)
_apply_only(subset, originals, targets)
_failing_sources(log, obj2src)
resolve(inc, from_path, known)