[include-what-you-use] Initial tooling (#36508)

This PR introduces the basic tooling to have `include-what-you-use` run
against the brave codebase in maintainance mode. This manual regular
runs that cover more and more of the codebase.

Bug: https://github.com/brave/brave-browser/issues/42212
This commit is contained in:
cdesouza-chromium
2026-05-19 18:59:55 +01:00
committed by GitHub
parent 1587d2171a
commit c31f01cfc2
10 changed files with 1255 additions and 1 deletions
+24
View File
@@ -473,6 +473,30 @@ def CheckPlasterFiles(input_api, output_api):
])
def CheckJson5ParseErrors(input_api, output_api):
"""Check that .json5 files parse without syntax errors.
Upstream `CheckParseErrors` covers `.idl` and `.json` but not `.json5`.
`json5.loads` natively accepts `//` and `/* */` comments, trailing
commas, and unquoted keys, so no comment-stripping pre-pass is needed.
"""
import json5
def _is_json5(affected_file):
return affected_file.LocalPath().endswith('.json5')
results = []
for affected_file in input_api.AffectedFiles(file_filter=_is_json5,
include_deletes=False):
try:
json5.loads(input_api.ReadFile(affected_file))
except ValueError as e:
results.append(
output_api.PresubmitError(
f'{affected_file.LocalPath()} could not be parsed: {e}'))
return results
# DON'T ADD NEW BRAVE CHECKS AFTER THIS LINE.
#
# This call inlines Chromium checks into current scope from src/PRESUBMIT.py. We
+60
View File
@@ -92,5 +92,65 @@ class CheckTypeScriptSuppressionsHaveReasonsTest(unittest.TestCase):
self.assertEqual(0, len(errors))
class CheckJson5ParseErrorsTest(unittest.TestCase):
def testAcceptsJson5WithCommentsAndTrailingCommas(self):
input_api = MockInputApi()
input_api.files = [
MockAffectedFile('brave/build/mappings.json5', [
'// libc++ private headers -> public facades',
'{',
' "include": [',
' [ "<__algorithm/sort.h>", "private",',
' "<algorithm>", "public" ], /* trailing comma OK */',
' ],',
'}',
]),
]
errors = PRESUBMIT.CheckJson5ParseErrors(input_api, MockOutputApi())
self.assertEqual([], errors)
def testReportsInvalidJson5(self):
input_api = MockInputApi()
input_api.files = [
MockAffectedFile('brave/build/broken.json5', [
'{',
' "key": "value"',
' "missing_comma": true',
'}',
]),
]
errors = PRESUBMIT.CheckJson5ParseErrors(input_api, MockOutputApi())
self.assertEqual(1, len(errors))
self.assertIn('brave/build/broken.json5 could not be parsed',
errors[0].message)
def testIgnoresNonJson5Files(self):
input_api = MockInputApi()
input_api.files = [
MockAffectedFile('brave/build/mappings.json',
['{ "not-json5": true,, }']),
MockAffectedFile('brave/foo.ts', ['const x = 1;']),
]
errors = PRESUBMIT.CheckJson5ParseErrors(input_api, MockOutputApi())
self.assertEqual([], errors)
def testIgnoresDeletedFiles(self):
input_api = MockInputApi()
input_api.files = [
MockAffectedFile('brave/build/deleted.json5', [], action='D'),
]
errors = PRESUBMIT.CheckJson5ParseErrors(input_api, MockOutputApi())
self.assertEqual([], errors)
if __name__ == '__main__':
unittest.main()
+354
View File
@@ -0,0 +1,354 @@
// Copyright (c) 2026 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
// IWYU mapping file consumed by brave/tools/cr/iwyu/run_iwyu.py.
//
// Chromium ships libc++, whose standard headers (`<algorithm>`, `<atomic>`,
// ...) are split into many private detail headers under
// `third_party/libc++/src/include/__<name>/`. Without these mappings, IWYU
// would suggest:
//
// #include "__algorithm/ranges_sort.h"
// #include "__atomic/atomic.h"
// #include "__fwd/sstream.h"
//
// instead of the public-facing:
//
// #include <algorithm>
// #include <atomic>
// #include <sstream>
//
// Each rule uses the `@` wildcard prefix so a single regex pattern remaps
// every private header under a `__<name>/` subtree to its public facade.
// `__fwd/<x>.h` is the libc++ forward-decl shim for `<x>` and must be
// enumerated per-header.
//
// IMPORTANT: the `@` regex matches the include string *including* its
// delimiters. Brave's compile commands produce both angle-bracket
// (`<__algorithm/...>`) and quoted (`"__algorithm/..."`) forms depending
// on platform / build config, so every rule has both variants.
//
// See `tools/cr/iwyu/README.md` and IWYU's IWYUMappings.md for the file format.
[
// === Private detail subdirectories -- angle-bracket form ===
{ 'include': ['@<__algorithm/.*>', 'private', '<algorithm>', 'public'] },
{ 'include': ['@<__atomic/.*>', 'private', '<atomic>', 'public'] },
{ 'include': ['@<__bit/.*>', 'private', '<bit>', 'public'] },
{ 'include': ['@<__charconv/.*>', 'private', '<charconv>', 'public'] },
{ 'include': ['@<__chrono/.*>', 'private', '<chrono>', 'public'] },
{ 'include': ['@<__compare/.*>', 'private', '<compare>', 'public'] },
{ 'include': ['@<__concepts/.*>', 'private', '<concepts>', 'public'] },
{
'include': [
'@<__condition_variable/.*>',
'private',
'<condition_variable>',
'public',
],
},
{ 'include': ['@<__coroutine/.*>', 'private', '<coroutine>', 'public'] },
{ 'include': ['@<__exception/.*>', 'private', '<exception>', 'public'] },
{ 'include': ['@<__expected/.*>', 'private', '<expected>', 'public'] },
{ 'include': ['@<__filesystem/.*>', 'private', '<filesystem>', 'public'] },
{ 'include': ['@<__format/.*>', 'private', '<format>', 'public'] },
{ 'include': ['@<__functional/.*>', 'private', '<functional>', 'public'] },
{ 'include': ['@<__ios/.*>', 'private', '<ios>', 'public'] },
{ 'include': ['@<__iterator/.*>', 'private', '<iterator>', 'public'] },
{ 'include': ['@<__locale/.*>', 'private', '<locale>', 'public'] },
{ 'include': ['@<__math/.*>', 'private', '<cmath>', 'public'] },
{ 'include': ['@<__memory/.*>', 'private', '<memory>', 'public'] },
{
'include': [
'@<__memory_resource/.*>',
'private',
'<memory_resource>',
'public',
],
},
{ 'include': ['@<__mutex/.*>', 'private', '<mutex>', 'public'] },
{ 'include': ['@<__new/.*>', 'private', '<new>', 'public'] },
{ 'include': ['@<__numeric/.*>', 'private', '<numeric>', 'public'] },
{ 'include': ['@<__ostream/.*>', 'private', '<ostream>', 'public'] },
{ 'include': ['@<__random/.*>', 'private', '<random>', 'public'] },
{ 'include': ['@<__ranges/.*>', 'private', '<ranges>', 'public'] },
{ 'include': ['@<__stop_token/.*>', 'private', '<stop_token>', 'public'] },
{ 'include': ['@<__string/.*>', 'private', '<string>', 'public'] },
{
'include': ['@<__system_error/.*>', 'private', '<system_error>', 'public'],
},
{ 'include': ['@<__thread/.*>', 'private', '<thread>', 'public'] },
{ 'include': ['@<__tuple/.*>', 'private', '<tuple>', 'public'] },
{ 'include': ['@<__type_traits/.*>', 'private', '<type_traits>', 'public'] },
{ 'include': ['@<__utility/.*>', 'private', '<utility>', 'public'] },
{ 'include': ['@<__variant/.*>', 'private', '<variant>', 'public'] },
{ 'include': ['@<__vector/.*>', 'private', '<vector>', 'public'] },
// === Private detail subdirectories -- quoted form ===
{ 'include': ['@"__algorithm/.*"', 'private', '<algorithm>', 'public'] },
{ 'include': ['@"__atomic/.*"', 'private', '<atomic>', 'public'] },
{ 'include': ['@"__bit/.*"', 'private', '<bit>', 'public'] },
{ 'include': ['@"__charconv/.*"', 'private', '<charconv>', 'public'] },
{ 'include': ['@"__chrono/.*"', 'private', '<chrono>', 'public'] },
{ 'include': ['@"__compare/.*"', 'private', '<compare>', 'public'] },
{ 'include': ['@"__concepts/.*"', 'private', '<concepts>', 'public'] },
{
'include': [
'@"__condition_variable/.*"',
'private',
'<condition_variable>',
'public',
],
},
{ 'include': ['@"__coroutine/.*"', 'private', '<coroutine>', 'public'] },
{ 'include': ['@"__exception/.*"', 'private', '<exception>', 'public'] },
{ 'include': ['@"__expected/.*"', 'private', '<expected>', 'public'] },
{ 'include': ['@"__filesystem/.*"', 'private', '<filesystem>', 'public'] },
{ 'include': ['@"__format/.*"', 'private', '<format>', 'public'] },
{ 'include': ['@"__functional/.*"', 'private', '<functional>', 'public'] },
{ 'include': ['@"__ios/.*"', 'private', '<ios>', 'public'] },
{ 'include': ['@"__iterator/.*"', 'private', '<iterator>', 'public'] },
{ 'include': ['@"__locale/.*"', 'private', '<locale>', 'public'] },
{ 'include': ['@"__math/.*"', 'private', '<cmath>', 'public'] },
{ 'include': ['@"__memory/.*"', 'private', '<memory>', 'public'] },
{
'include': [
'@"__memory_resource/.*"',
'private',
'<memory_resource>',
'public',
],
},
{ 'include': ['@"__mutex/.*"', 'private', '<mutex>', 'public'] },
{ 'include': ['@"__new/.*"', 'private', '<new>', 'public'] },
{ 'include': ['@"__numeric/.*"', 'private', '<numeric>', 'public'] },
{ 'include': ['@"__ostream/.*"', 'private', '<ostream>', 'public'] },
{ 'include': ['@"__random/.*"', 'private', '<random>', 'public'] },
{ 'include': ['@"__ranges/.*"', 'private', '<ranges>', 'public'] },
{ 'include': ['@"__stop_token/.*"', 'private', '<stop_token>', 'public'] },
{ 'include': ['@"__string/.*"', 'private', '<string>', 'public'] },
{
'include': ['@"__system_error/.*"', 'private', '<system_error>', 'public'],
},
{ 'include': ['@"__thread/.*"', 'private', '<thread>', 'public'] },
{ 'include': ['@"__tuple/.*"', 'private', '<tuple>', 'public'] },
{
'include': ['@"__type_traits/.*"', 'private', '<type_traits>', 'public'],
},
{ 'include': ['@"__utility/.*"', 'private', '<utility>', 'public'] },
{ 'include': ['@"__variant/.*"', 'private', '<variant>', 'public'] },
{ 'include': ['@"__vector/.*"', 'private', '<vector>', 'public'] },
// === Forward-decl shims -- angle-bracket form ===
// `__fwd/<x>.h` is libc++'s forward-decl-only header for `<x>`.
{ 'include': ['<__fwd/array.h>', 'private', '<array>', 'public'] },
{ 'include': ['<__fwd/byte.h>', 'private', '<cstddef>', 'public'] },
{ 'include': ['<__fwd/complex.h>', 'private', '<complex>', 'public'] },
{ 'include': ['<__fwd/deque.h>', 'private', '<deque>', 'public'] },
{ 'include': ['<__fwd/format.h>', 'private', '<format>', 'public'] },
{ 'include': ['<__fwd/fstream.h>', 'private', '<fstream>', 'public'] },
{ 'include': ['<__fwd/functional.h>', 'private', '<functional>', 'public'] },
{ 'include': ['<__fwd/ios.h>', 'private', '<ios>', 'public'] },
{ 'include': ['<__fwd/istream.h>', 'private', '<istream>', 'public'] },
{ 'include': ['<__fwd/mdspan.h>', 'private', '<mdspan>', 'public'] },
{ 'include': ['<__fwd/memory.h>', 'private', '<memory>', 'public'] },
{
'include': [
'<__fwd/memory_resource.h>',
'private',
'<memory_resource>',
'public',
],
},
{ 'include': ['<__fwd/ostream.h>', 'private', '<ostream>', 'public'] },
{ 'include': ['<__fwd/pair.h>', 'private', '<utility>', 'public'] },
{ 'include': ['<__fwd/queue.h>', 'private', '<queue>', 'public'] },
{ 'include': ['<__fwd/span.h>', 'private', '<span>', 'public'] },
{ 'include': ['<__fwd/sstream.h>', 'private', '<sstream>', 'public'] },
{ 'include': ['<__fwd/stack.h>', 'private', '<stack>', 'public'] },
{ 'include': ['<__fwd/streambuf.h>', 'private', '<streambuf>', 'public'] },
{ 'include': ['<__fwd/string.h>', 'private', '<string>', 'public'] },
{
'include': ['<__fwd/string_view.h>', 'private', '<string_view>', 'public'],
},
{ 'include': ['<__fwd/subrange.h>', 'private', '<ranges>', 'public'] },
{ 'include': ['<__fwd/tuple.h>', 'private', '<tuple>', 'public'] },
{ 'include': ['<__fwd/variant.h>', 'private', '<variant>', 'public'] },
{ 'include': ['<__fwd/vector.h>', 'private', '<vector>', 'public'] },
// === Forward-decl shims -- quoted form ===
{ 'include': ['"__fwd/array.h"', 'private', '<array>', 'public'] },
{ 'include': ['"__fwd/byte.h"', 'private', '<cstddef>', 'public'] },
{ 'include': ['"__fwd/complex.h"', 'private', '<complex>', 'public'] },
{ 'include': ['"__fwd/deque.h"', 'private', '<deque>', 'public'] },
{ 'include': ['"__fwd/format.h"', 'private', '<format>', 'public'] },
{ 'include': ['"__fwd/fstream.h"', 'private', '<fstream>', 'public'] },
{
'include': ['"__fwd/functional.h"', 'private', '<functional>', 'public'],
},
{ 'include': ['"__fwd/ios.h"', 'private', '<ios>', 'public'] },
{ 'include': ['"__fwd/istream.h"', 'private', '<istream>', 'public'] },
{ 'include': ['"__fwd/mdspan.h"', 'private', '<mdspan>', 'public'] },
{ 'include': ['"__fwd/memory.h"', 'private', '<memory>', 'public'] },
{
'include': [
'"__fwd/memory_resource.h"',
'private',
'<memory_resource>',
'public',
],
},
{ 'include': ['"__fwd/ostream.h"', 'private', '<ostream>', 'public'] },
{ 'include': ['"__fwd/pair.h"', 'private', '<utility>', 'public'] },
{ 'include': ['"__fwd/queue.h"', 'private', '<queue>', 'public'] },
{ 'include': ['"__fwd/span.h"', 'private', '<span>', 'public'] },
{ 'include': ['"__fwd/sstream.h"', 'private', '<sstream>', 'public'] },
{ 'include': ['"__fwd/stack.h"', 'private', '<stack>', 'public'] },
{ 'include': ['"__fwd/streambuf.h"', 'private', '<streambuf>', 'public'] },
{ 'include': ['"__fwd/string.h"', 'private', '<string>', 'public'] },
{
'include': ['"__fwd/string_view.h"', 'private', '<string_view>', 'public'],
},
{ 'include': ['"__fwd/subrange.h"', 'private', '<ranges>', 'public'] },
{ 'include': ['"__fwd/tuple.h"', 'private', '<tuple>', 'public'] },
{ 'include': ['"__fwd/variant.h"', 'private', '<variant>', 'public'] },
{ 'include': ['"__fwd/vector.h"', 'private', '<vector>', 'public'] },
// `third_party/jni_zero/jni_zero.h` is Chromium's wrapper over `<jni.h>`
// and pulls it in transitively. Anywhere IWYU would suggest the raw
// `<jni.h>`, route through jni_zero so files that already include the
// wrapper don't pick up a redundant `<jni.h>`.
{
'include': [
'<jni.h>',
'private',
'"third_party/jni_zero/jni_zero.h"',
'public',
],
},
// We use a short version for rust's cxx headers
{
'include': [
'"third_party/rust/chromium_crates_io/vendor/cxx-v1/include/cxx.h"',
'private',
'"third_party/rust/cxx/v1/cxx.h"',
'public',
],
},
// This one seems to be IWYU doing something weird, as I can get both the
// forwarding header and span.h pulled in some cases.
{
'include': [
'"base/containers/span_forward_internal.h"',
'private',
'"base/containers/span.h"',
'public',
],
},
// BoringSSL: the project convention is to spell these as
// `"openssl/<name>.h"` since `third_party/boringssl:external_config` adds
// `src/include` to every consumer's `-I` path (see boringssl/BUILD.gn).
// Map the full physical path back to that short form.
{
'include': [
'"third_party/boringssl/src/include/openssl/base.h"',
'private',
'"openssl/base.h"',
'public',
],
},
{
'include': [
'"third_party/boringssl/src/include/openssl/digest.h"',
'private',
'"openssl/digest.h"',
'public',
],
},
// Pin the basic integer and size typedefs to their canonical C headers.
// `openssl/base.h` re-exports `<stddef.h>` and `<stdint.h>` via
// `IWYU pragma: export`, which would otherwise make it a valid provider
// for these types and cause IWYU to keep a vestigial `openssl/base.h`
// include just to satisfy a `size_t` / `uint8_t` use.
{ 'symbol': ['size_t', 'private', '<stddef.h>', 'public'] },
{ 'symbol': ['ptrdiff_t', 'private', '<stddef.h>', 'public'] },
{ 'symbol': ['int8_t', 'private', '<stdint.h>', 'public'] },
{ 'symbol': ['int16_t', 'private', '<stdint.h>', 'public'] },
{ 'symbol': ['int32_t', 'private', '<stdint.h>', 'public'] },
{ 'symbol': ['int64_t', 'private', '<stdint.h>', 'public'] },
{ 'symbol': ['uint8_t', 'private', '<stdint.h>', 'public'] },
{ 'symbol': ['uint16_t', 'private', '<stdint.h>', 'public'] },
{ 'symbol': ['uint32_t', 'private', '<stdint.h>', 'public'] },
{ 'symbol': ['uint64_t', 'private', '<stdint.h>', 'public'] },
{
'include': [
'"ethash/hash_types.h"',
'private',
'"brave/third_party/ethash/src/include/ethash/hash_types.h"',
'public',
],
},
// mappings that require further investigation.
{
'include': [
'"gtest/gtest.h"',
'private',
'"testing/gtest/include/gtest/gtest.h"',
'public',
],
},
{
'include': [
'"gmock/gmock.h"',
'private',
'"testing/gmock/include/gmock/gmock.h"',
'public',
],
},
{
'include': ['"span.h"', 'private', '"base/containers/span.h"', 'public'],
},
// this seems to occur only on windows build
{ 'include': ['"map"', 'private', '<map>', 'public'] },
{ 'include': ['"tuple"', 'private', '<tuple>', 'public'] },
{ 'include': ['"string_view"', 'private', '<string_view>', 'public'] },
// Using <new> as an include blackhole
//
// These headers are being blackholed because they are not desired to ever be
// shown in our code. They are usually cases where we could add an export
// pragma in upstream code, or where include-what-you-use is confused about
// things.
{ 'include': ['<vcruntime_new.h>', 'private', '<new>', 'public'] },
{ 'include': ['<vcruntime_string.h>', 'private', '<new>', 'public'] },
{
'include': [
'"mojo/public/cpp/bindings/struct_ptr.h"',
'private',
'<new>',
'public',
],
},
{
'include': [
'"base/containers/checked_iterators.h"',
'private',
'<new>',
'public',
],
},
{ 'include': ['"base/macros/if.h"', 'private', '<new>', 'public'] },
{ 'include': ['"base/macros/is_empty.h"', 'private', '<new>', 'public'] },
]
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) 2026 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
# All paths listes are relative to `src/`. Use + prefix for the paths you wnat
# to include, and - prefix for paths you want to exclude in the inclusions.
# For how this file is used, see `tools/cr/iwyu/README.md`.
+brave/components/brave_ads/browser
+brave/components/brave_wallet/common
+102
View File
@@ -0,0 +1,102 @@
# Brave IWYU tooling
Two scripts that drive [include-what-you-use](https://include-what-you-use.org/)
over Brave sources:
* `build_iwyu.py` — builds the IWYU binary against Chromium's pinned Clang.
* `run_iwyu.py` — runs IWYU against an existing Brave build dir and applies
the suggested include fixes in place.
IWYU is **not** part of the normal Brave build; run these scripts on demand.
## One-time setup: build IWYU
```sh
vpython3 tools/cr/iwyu/build_iwyu.py
```
Clones LLVM at Chromium's pinned `CLANG_REVISION` and IWYU at the pinned
`IWYU_REVISION` into `out/iwyu/`, then builds:
```
out/iwyu/tools/clang/third_party/llvm/build/bin/include-what-you-use
```
Re-run after Chromium's Clang pin moves, or bump `IWYU_REVISION` in
`build_iwyu.py` to pick up an upstream IWYU fix.
## Enable the paths you want IWYU to scan
Edit `brave/build/include_what_you_use_paths.cfg`. Each non-comment line is:
```
+path/to/enable
-path/to/disable
```
Paths are relative to `src/`. Longest-matching prefix wins, so you can
enable a directory and carve exceptions out of it:
```
+brave/components/brave_ads/browser
-brave/components/brave_ads/browser/third_party
```
## Run IWYU
IWYU needs an existing Brave build in some `out/<config>` to pick up its
compile flags and generated headers, **and** several GN args have to be
flipped — without them IWYU crashes during analysis:
```sh
npm run build -- Static \
--target_os=android \
--ignore_compile_failure \
--target=brave:all \
--gn=clang_use_chrome_plugins:false \
--gn=force_enable_raw_ptr_exclusion:true \
--gn=enable_precompiled_headers:false \
--gn=treat_warnings_as_errors:false \
--gn=clang_warning_suppression_file:"" \
--gn=symbol_level:0
```
Required (IWYU crashes without these):
* `clang_use_chrome_plugins:false`.
* `force_enable_raw_ptr_exclusion:true`
* `enable_precompiled_headers:false`
* `treat_warnings_as_errors:false`
Then finally run `include-what-you-use` with `run_iwyu.py`, bearing in mind that `--out` is relative to `src/`, rather than the current directory.
```sh
vpython3 tools/cr/iwyu/run_iwyu.py --out out/Static --verbose
```
This:
1. Generates a compile database from `out/<config>` and filters it to
sources enabled by `include_what_you_use_paths.cfg`.
2. Runs `iwyu_tool.py` with our libc++ mapping file
(`brave/build/include_what_you_use_mappings.json5`).
3. Applies suggestions in place via `fix_includes.py`.
4. Strips blackholed includes (e.g. `<new>`) from modified files.
5. Runs `npm run format` so the resulting diff matches Brave's style.
Artifacts written next to the build:
* `out/<config>/iwyu_compile_commands.json` — filtered compile DB.
* `out/<config>/iwyu_suggestions.txt` — raw IWYU output, handy when a
rewrite looks wrong.
## Defending an include from IWYU
If IWYU keeps removing an include you need, mark it:
```c++
#include "foo/bar.h" // IWYU pragma: keep
```
`run_iwyu.py` leaves any line containing `IWYU pragma:` untouched, including
during the blackhole-includes pass.
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) 2026 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
"""Import this to add the parent tools/cr to sys.path."""
import sys
from pathlib import Path
_cr = str(Path(__file__).parent.parent)
if _cr not in sys.path:
sys.path.insert(0, _cr)
+216
View File
@@ -0,0 +1,216 @@
#!/usr/bin/env vpython3
# Copyright (c) 2026 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
"""Build `include-what-you-use` against Chromium's pinned LLVM revision.
Keep this script standalone, with no dependencies on other local python sources
in tools/cr, as we may want to run the build in CI, in the future, using only
a vanilla Chromium checkout.
What this script does:
1. Clones the LLVM monorepo into `out/iwyu/tools/clang/third_party/llvm`,
pinned to `CLANG_REVISION` from `tools/clang/scripts/update.py`. This
matches the revision Chromium's bundled Clang is built from, so the
resulting IWYU binary speaks the same Clang AST as the rest of the
toolchain.
2. Clones include-what-you-use into
`out/iwyu/tools/clang/third_party/iwyu`, alongside the LLVM checkout
(pinned to the `IWYU_REVISION` constant below).
3. Configures a CMake build that pulls IWYU in as an LLVM external project
(`LLVM_EXTERNAL_PROJECTS=iwyu` +
`LLVM_EXTERNAL_IWYU_SOURCE_DIR=…/iwyu`). Building IWYU as part of the
LLVM build avoids the standalone-build resource-dir gymnastics
(see IWYU README's "How to install" section).
4. Invokes `ninja include-what-you-use` inside the LLVM build tree.
The resulting binary lands at:
out/iwyu/tools/clang/third_party/llvm/build/bin/include-what-you-use
"""
from __future__ import annotations
import argparse
import logging
import platform
import shutil
import subprocess
import sys
from pathlib import Path, PurePath
CHROMIUM_SRC: Path = Path(__file__).resolve().parents[4]
# Importing clang toolchain helper from Chromium.
sys.path.append(str(CHROMIUM_SRC / 'tools' / 'clang' / 'scripts'))
# Static analysers can't see through the sys.path mutation above.
# pylint: disable=wrong-import-position
from build import AddCMakeToPath, CheckoutGitRepo, LLVM_GIT_URL # type: ignore # noqa: E402
from update import CLANG_REVISION # type: ignore # noqa: E402
# pylint: enable=wrong-import-position
# The repo to clone from for the build.
IWYU_GIT_URL = ('https://github.com/include-what-you-use/'
'include-what-you-use.git')
# Pinned IWYU revision. IWYU master tracks LLVM main; we pin to a specific
# commit that compiles against the Clang revision Chromium has currently
# pinned (`update.CLANG_REVISION`). Bump when bumping Chromium's Clang.
#
# `ece9edb` is the commit immediately before
# c36eacb "[clang compat] Handle new HLSL builtin trait", which references
# `clang::TypeTrait::UTT_IsConstantBufferElementCompatible` -- a symbol that
# doesn't exist in `llvmorg-23-init-5669-g8a0be0bc` (our current pin).
IWYU_REVISION = 'ece9edb'
# Constants for the the directory layout of where these tools live under.
THIRD_PARTY_REL = PurePath('tools') / 'clang' / 'third_party'
LLVM_REL = THIRD_PARTY_REL / 'llvm'
IWYU_REL = THIRD_PARTY_REL / 'iwyu'
BUILD_REL = LLVM_REL / 'build'
# We always build it at `out/iwyu` in Chromium.
OUT_DIR: Path = CHROMIUM_SRC / 'out' / 'iwyu'
def _check_call(*command, cwd=None):
"""Run *command* as a subprocess, logging the invocation.
Stdout and stderr are inherited from this process so the user sees ninja
progress and compiler diagnostics live -- important for long-running
cmake/ninja invocations where errors otherwise scroll past or get lost.
"""
logging.info(' >>>> %s', ' '.join(str(a) for a in command))
if platform.system() == 'Windows':
# Resolve to an absolute path so .bat shims (e.g. `cmake.bat`) are
# found without `shell=True`.
resolved = shutil.which(command[0])
if resolved is None:
raise RuntimeError(f'Command not found: {command[0]}')
if resolved != command[0]:
command = [resolved] + list(command[1:])
subprocess.run(command, cwd=cwd, check=True)
class IwyuBuilder:
"""Clone LLVM + IWYU and build the IWYU binary against pinned Clang.
Three phases:
1. **Checkout** (`_checkout_llvm`, `_checkout_iwyu`): clones (or updates)
LLVM and IWYU at the requested revisions into the layout described
in the module docstring. Uses `build.CheckoutGitRepo` from the
Chromium clang scripts, which idempotently fetches/updates an
existing clone or re-clones from scratch if the working tree is
broken.
2. **Configure** (`_configure`): runs `cmake -GNinja` against
`<llvm-clone>/llvm/CMakeLists.txt` with IWYU wired in as an LLVM
external project. Build dir lives at `<llvm-clone>/build/`, matching
`build_clang_tools_extra.py`.
3. **Build** (`_build`): `ninja <targets>` (default:
`include-what-you-use`).
"""
def __init__(self):
self.llvm_dir: Path = OUT_DIR / LLVM_REL
self.iwyu_dir: Path = OUT_DIR / IWYU_REL
self.build_dir: Path = OUT_DIR / BUILD_REL
def _checkout_llvm(self):
"""Clone or update LLVM at the pinned Chromium Clang revision."""
self.llvm_dir.parent.mkdir(parents=True, exist_ok=True)
CheckoutGitRepo('LLVM monorepo', LLVM_GIT_URL, CLANG_REVISION,
str(self.llvm_dir))
def _checkout_iwyu(self):
"""Clone or update IWYU at the pinned revision."""
self.iwyu_dir.parent.mkdir(parents=True, exist_ok=True)
CheckoutGitRepo('include-what-you-use', IWYU_GIT_URL, IWYU_REVISION,
str(self.iwyu_dir))
def _configure(self):
"""Configure the LLVM build with IWYU added as an external project.
The LLVM monorepo's CMake root is `<llvm-clone>/llvm`, not the
repo root. IWYU is pulled in via the standard LLVM external-project
knobs, so the build picks up the in-tree Clang's resource directory
without any of the `IWYU_RESOURCE_RELATIVE_TO` gymnastics needed for
a standalone IWYU build.
"""
# Download Chromium's pinned cmake into third_party/llvm-build-tools/
# and prepend it to PATH. Sidesteps "wrong cmake on PATH" failures --
# most often on macOS where Homebrew cmake versions skew vs. the
# llvm-project requirements.
AddCMakeToPath()
self.build_dir.mkdir(parents=True, exist_ok=True)
llvm_cmake_root = self.llvm_dir / 'llvm'
cmake_args = [
'cmake',
'-GNinja',
'-DLLVM_ENABLE_PROJECTS=clang',
'-DLLVM_EXTERNAL_PROJECTS=iwyu',
f'-DLLVM_EXTERNAL_IWYU_SOURCE_DIR={self.iwyu_dir}',
'-DCMAKE_BUILD_TYPE=Release',
'-DLLVM_ENABLE_ASSERTIONS=On',
# IWYU is a Clang frontend tool and needs no codegen backends.
# Restricting to the host backend skips AMDGPU/MIPS/PowerPC/etc.
# which would otherwise take the bulk of the build time (and have
# caused AMDGPU compile failures on some hosts).
'-DLLVM_TARGETS_TO_BUILD=host',
# Trim other optional bits that are irrelevant for a tool build.
'-DLLVM_INCLUDE_TESTS=OFF',
'-DLLVM_INCLUDE_EXAMPLES=OFF',
'-DLLVM_INCLUDE_BENCHMARKS=OFF',
'-DLLVM_INCLUDE_DOCS=OFF',
str(llvm_cmake_root),
]
_check_call(*cmake_args, cwd=self.build_dir)
def _build(self):
"""Build the IWYU binary."""
_check_call('ninja', 'include-what-you-use', cwd=self.build_dir)
def run(self):
"""Execute the full checkout / configure / build pipeline."""
OUT_DIR.mkdir(parents=True, exist_ok=True)
logging.info('LLVM revision: %s', CLANG_REVISION)
logging.info('IWYU revision: %s', IWYU_REVISION)
logging.info('LLVM checkout: %s', self.llvm_dir)
logging.info('IWYU checkout: %s', self.iwyu_dir)
logging.info('Build dir: %s', self.build_dir)
self._checkout_llvm()
self._checkout_iwyu()
self._configure()
self._build()
produced = self.build_dir / 'bin' / 'include-what-you-use'
logging.info('Build complete. Expected binary at: %s', produced)
def main():
parser = argparse.ArgumentParser(
description='Build include-what-you-use against Chromium-pinned LLVM.')
parser.add_argument('--verbose',
action='store_true',
help='Enable verbose (debug) logging.')
args = parser.parse_args()
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
IwyuBuilder().run()
return 0
if __name__ == '__main__':
sys.exit(main())
+304
View File
@@ -0,0 +1,304 @@
#!/usr/bin/env vpython3
# Copyright (c) 2026 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
"""Run `include-what-you-use` across the Brave paths enabled in
`brave/build/include_what_you_use_paths.cfg`.
What this script does:
1. Generates a compile database for the build dir given via `--out`, using
the helpers behind Chromium's `tools/clang/scripts/generate_compdb.py`
(`tools/clang/pylib/clang/compile_db.py`). Building must already be
done in that directory.
2. Reads `brave/build/include_what_you_use_paths.cfg` and filters the
compile DB down to entries whose source files fall under enabled paths.
3. Writes the filtered DB to `<out>/iwyu_compile_commands.json` and invokes
`iwyu_tool.py` against it with `IWYU_BINARY` pointed at the IWYU binary
produced by `build_iwyu.py`.
Prerequisites:
* `brave/tools/cr/iwyu/build_iwyu.py` has been run, producing the IWYU
binary at `<src>/out/iwyu/tools/clang/third_party/llvm/build/bin/`.
* The Brave build directory passed via `--out` has been built (so that
`build.ninja` and any generated headers exist).
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import re
import sys
from pathlib import Path
import _boot # noqa: F401 -- adds parent tools/cr to sys.path
import repository # noqa: E402 -- after _boot
# Make Chromium helpers importable. Same trick
# `tools/clang/scripts/generate_compdb.py` itself uses. Derived from
# __file__ rather than `repository.chromium.root` so the sys.path entries are
# stable regardless of the cwd `repository` was initialised against.
_SRC_ROOT: Path = Path(__file__).resolve().parents[4]
sys.path.append(str(_SRC_ROOT / 'tools' / 'clang' / 'pylib'))
sys.path.append(str(_SRC_ROOT / 'tools' / 'json_comment_eater'))
# pylint: disable=wrong-import-position
import json_comment_eater # type: ignore # noqa: E402
from clang import compile_db # type: ignore # noqa: E402
from terminal import terminal # type: ignore # noqa: E402
# pylint: enable=wrong-import-position
# IWYU artefacts produced by `build_iwyu.py`. Keeping the paths in sync with
# that script is intentional -- run_iwyu.py is a no-op without it.
_IWYU_OUT_DIR: Path = (repository.chromium.root / 'out' / 'iwyu' / 'tools' /
'clang' / 'third_party')
IWYU_BINARY: Path = (_IWYU_OUT_DIR / 'llvm' / 'build' / 'bin' /
'include-what-you-use')
IWYU_TOOL: Path = _IWYU_OUT_DIR / 'iwyu' / 'iwyu_tool.py'
# Applies the textual suggestions produced by iwyu_tool to source files in
# place. Lives alongside iwyu_tool.py in the same IWYU clone.
FIX_INCLUDES: Path = _IWYU_OUT_DIR / 'iwyu' / 'fix_includes.py'
# Brave-managed path filter file. See module docstring.
PATHS_FILE: Path = (repository.brave.root / 'build' /
'include_what_you_use_paths.cfg')
# IWYU mapping file: remaps libc++ private detail headers (e.g.
# `__algorithm/ranges_sort.h`) to their public facades (`<algorithm>`).
# Passed to IWYU via `-Xiwyu --mapping_file=<absolute path>` so the lookup
# is independent of each compile DB entry's cwd. A comment-stripped copy
# is written under `--out` at run time and fed to iwyu_tool (see main).
MAPPINGS_FILE: Path = (repository.brave.root / 'build' /
'include_what_you_use_mappings.json5')
# Headers we never want to see in Brave's source after IWYU.
BLACKHOLE_INCLUDES: frozenset[str] = frozenset([
'<new>',
])
# Matches `#include <hdr>` or `#include "hdr"`, capturing the delimited
# token (with its brackets/quotes intact) for comparison against
# BLACKHOLE_INCLUDES.
_INCLUDE_RE = re.compile(r'^\s*#\s*include\s+([<"][^<>"]+[>"])')
def parse_paths_file(path: Path) -> list[tuple[str, str]]:
"""Parse the path-rule file into an ordered list of (sign, path) rules.
Each non-blank, non-comment line is either `+<path>/` (enable) or
`-<path>/` (disable). Trailing `#` comments are stripped. Paths are
normalised to end with `/` so prefix matching is unambiguous (e.g.
`brave/browser/` does not match `brave/browser_other/foo.cc`).
"""
rules: list[tuple[str, str]] = []
text = path.read_bytes().decode('utf-8')
for line_no, raw_line in enumerate(text.splitlines(), 1):
# Strip trailing comments.
comment_idx = raw_line.find('#')
if comment_idx != -1:
raw_line = raw_line[:comment_idx]
line = raw_line.strip()
if not line:
continue
if line[0] not in ('+', '-'):
raise ValueError(
f'{path}:{line_no}: rule lines must start with `+` or `-`, '
f'got: {line!r}')
sign = line[0]
rule_path = line[1:].strip()
if not rule_path:
raise ValueError(f'{path}:{line_no}: empty path after `{sign}`')
if not rule_path.endswith('/'):
rule_path += '/'
rules.append((sign, rule_path))
return rules
def is_path_enabled(source_rel: str, rules: list[tuple[str, str]]) -> bool:
"""Return whether `source_rel` (relative to src/) is enabled by `rules`.
Longest-matching prefix wins. If no rule matches, the path is disabled.
"""
best_len = -1
best_sign = '-'
for sign, rule_path in rules:
if source_rel.startswith(rule_path) and len(rule_path) > best_len:
best_len = len(rule_path)
best_sign = sign
return best_sign == '+'
def blackhole_unwanted_includes() -> None:
"""Remove BLACKHOLE_INCLUDES from any file modified in the working tree.
Runs after `npm run format` to clean up includes IWYU/fix_includes
re-added that we never want. Files to scan are taken from
`git diff --name-only` against brave-core's HEAD, so only the files
the pipeline actually touched get rewritten.
Lines containing `IWYU pragma:` are preserved verbatim so callers can
defend a specific include with `// IWYU pragma: keep`.
"""
diff = terminal.run_git('-C', str(repository.brave.root), 'diff',
'--name-only')
files = diff.splitlines() if diff else []
for rel in files:
target = repository.brave.root / rel
if not target.is_file():
continue
text = target.read_bytes().decode('utf-8')
kept: list[str] = []
removed_any = False
for line in text.splitlines(keepends=True):
if 'IWYU pragma:' in line:
kept.append(line)
continue
match = _INCLUDE_RE.match(line)
if match and match.group(1) in BLACKHOLE_INCLUDES:
removed_any = True
continue
kept.append(line)
if removed_any:
target.write_text(''.join(kept), encoding='utf-8', newline='')
logging.info('Blackholed unwanted includes from %s', rel)
def main():
parser = argparse.ArgumentParser(
description='Run IWYU on enabled Brave paths against an existing '
'Brave build directory.')
parser.add_argument(
'--out',
required=True,
help='Brave build directory, relative to Chromium\'s src/ '
'(e.g. `out/Component`).')
parser.add_argument('--verbose',
action='store_true',
help='Enable verbose (debug) logging.')
args = parser.parse_args()
out_dir = repository.chromium.root / args.out
if not (out_dir / 'build.ninja').exists():
raise RuntimeError(
f'--out does not look like a build dir (no build.ninja): '
f'{out_dir}')
if not IWYU_BINARY.exists():
raise RuntimeError(f'IWYU binary not found at {IWYU_BINARY}. '
f'Run brave/tools/cr/iwyu/build_iwyu.py first.')
if not IWYU_TOOL.exists():
raise RuntimeError(f'iwyu_tool.py not found at {IWYU_TOOL}. '
f'Run brave/tools/cr/iwyu/build_iwyu.py first.')
if not FIX_INCLUDES.exists():
raise RuntimeError(f'fix_includes.py not found at {FIX_INCLUDES}. '
f'Run brave/tools/cr/iwyu/build_iwyu.py first.')
if not MAPPINGS_FILE.exists():
raise RuntimeError(f'IWYU mapping file not found at {MAPPINGS_FILE}.')
rules = parse_paths_file(PATHS_FILE)
logging.info('Loaded %d path rule(s) from %s', len(rules), PATHS_FILE)
logging.info('Generating compile database for %s', out_dir)
raw_db = compile_db.GenerateWithNinja(str(out_dir))
full_db = compile_db.ProcessCompileDatabase(raw_db, filtered_args=None)
logging.info('Compile DB has %d entries', len(full_db))
filtered_db = []
for entry in full_db:
# `entry['file']` may be relative to `entry['directory']` (the build
# dir) or already absolute; Path / handles both.
source_path = Path(entry['directory']) / entry['file']
try:
source_rel = repository.chromium.to_repo_relative(source_path)
except ValueError:
# Source outside the Chromium tree -- not something we own.
continue
if is_path_enabled(source_rel.as_posix(), rules):
filtered_db.append(entry)
logging.info('Running IWYU on %d source file(s)', len(filtered_db))
if not filtered_db:
logging.warning('No source files enabled. Edit %s to enable paths.',
PATHS_FILE)
return 0
# Write the filtered DB next to the build outputs, distinct from the
# build's own compile_commands.json (if any).
filtered_db_path = out_dir / 'iwyu_compile_commands.json'
filtered_db_path.write_text(json.dumps(filtered_db, indent=2),
encoding='utf-8',
newline='')
logging.info('Wrote filtered compile DB to %s', filtered_db_path)
env = os.environ.copy()
env['IWYU_BINARY'] = str(IWYU_BINARY)
# Strip `//` and `/* */` comments from the mapping file into a copy
# under `--out` so strict JSON parsers (presubmit, json.load) can
# consume it; IWYU itself is permissive but downstream tooling isn't.
# Living under `--out` keeps this artefact out of the source tree
# without needing a .gitignore entry.
normalised_mappings_path = out_dir / 'iwyu_normalised_mappings.json5'
normalised_mappings_path.write_text(json_comment_eater.Nom(
MAPPINGS_FILE.read_bytes().decode('utf-8')),
encoding='utf-8',
newline='')
logging.info('Wrote normalised mapping file to %s',
normalised_mappings_path)
cpu_count = os.cpu_count() or 1
# Step 1: run iwyu_tool to produce textual fix suggestions on stdout.
# We capture stdout (rather than `interactive=True`) so we can pipe it
# into fix_includes.py in step 2.
#
# Args after `--` are forwarded to each IWYU subprocess by iwyu_tool.
# `-Xiwyu --mapping_file=...` tells IWYU to use our libc++ mapping
# file; the path must be absolute since IWYU runs each unit cd'd to
# the compile DB entry's `directory`.
logging.info('Running iwyu_tool.py')
iwyu_result = terminal.run([
sys.executable, IWYU_TOOL, '-p', filtered_db_path, '-j', cpu_count,
'--', '-Xiwyu', f'--mapping_file={normalised_mappings_path.resolve()}'
],
env=env)
# Persist the raw suggestions next to the filtered DB for inspection /
# post-mortem diffing if a fix goes wrong.
suggestions_path = out_dir / 'iwyu_suggestions.txt'
suggestions_path.write_text(iwyu_result.stdout,
encoding='utf-8',
newline='')
logging.info('Wrote IWYU suggestions to %s', suggestions_path)
# Step 2: apply the suggestions in place. fix_includes.py reads the
# iwyu_tool report on stdin and rewrites the affected source files.
#
# The filenames in the report are relative to each compile DB entry's
# `directory` field, which for a Chromium-style build is the build dir
# (e.g. `../../brave/common/importer/foo.cc` rooted at `out/<config>`).
# Run fix_includes.py with cwd=<build dir> so those paths resolve.
logging.info('Applying suggestions via fix_includes.py')
# FIX_INCLUDES is cwd-relative (via repository.chromium.root); absolutise
# it before changing cwd or the lookup will fail.
terminal.run([sys.executable, FIX_INCLUDES.resolve()],
stdin=iwyu_result.stdout,
cwd=out_dir)
# Step 3: strip BLACKHOLE_INCLUDES from any file the pipeline modified.
logging.info('Stripping blackholed includes')
blackhole_unwanted_includes()
# Step 4: re-format the rewritten files so the resulting diff matches
logging.info('Running npm run format')
terminal.run_npm_command('format')
return 0
if __name__ == '__main__':
sys.exit(main())
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env vpython3
# Copyright (c) 2026 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from run_iwyu import is_path_enabled, parse_paths_file
class ParsePathsFileTest(unittest.TestCase):
def setUp(self):
self._tmp_dir = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp_dir.cleanup)
self.cfg = Path(self._tmp_dir.name) / 'paths.cfg'
def _write(self, content: str) -> Path:
self.cfg.write_text(content, encoding='utf-8', newline='')
return self.cfg
def test_empty_file(self):
self.assertEqual(parse_paths_file(self._write('')), [])
def test_blank_and_comment_lines_only(self):
self._write('\n \n# a comment\n\t# indented comment\n')
self.assertEqual(parse_paths_file(self.cfg), [])
def test_enable_and_disable_rules(self):
self._write('+brave/browser/\n-brave/browser/internal/\n')
self.assertEqual(parse_paths_file(self.cfg), [
('+', 'brave/browser/'),
('-', 'brave/browser/internal/'),
])
def test_trailing_slash_is_added_when_missing(self):
self._write('+brave/browser\n-brave/browser/internal\n')
self.assertEqual(parse_paths_file(self.cfg), [
('+', 'brave/browser/'),
('-', 'brave/browser/internal/'),
])
def test_trailing_comment_is_stripped(self):
self._write('+brave/browser/ # enable browser\n'
'-brave/browser/internal/# nested disable\n')
self.assertEqual(parse_paths_file(self.cfg), [
('+', 'brave/browser/'),
('-', 'brave/browser/internal/'),
])
def test_line_that_is_only_a_comment_is_skipped(self):
self._write('+brave/browser/\n'
'# -brave/browser/internal/\n'
'-brave/components/\n')
self.assertEqual(parse_paths_file(self.cfg), [
('+', 'brave/browser/'),
('-', 'brave/components/'),
])
def test_surrounding_whitespace_is_tolerated(self):
self._write(' +brave/browser/ \n\t-brave/components/\t\n')
self.assertEqual(parse_paths_file(self.cfg), [
('+', 'brave/browser/'),
('-', 'brave/components/'),
])
def test_rules_preserve_file_order(self):
self._write('+a/\n-b/\n+c/\n-d/\n')
self.assertEqual(parse_paths_file(self.cfg), [
('+', 'a/'),
('-', 'b/'),
('+', 'c/'),
('-', 'd/'),
])
def test_invalid_prefix_raises(self):
self._write('brave/browser/\n')
with self.assertRaisesRegex(ValueError,
'must start with `\\+` or `-`'):
parse_paths_file(self.cfg)
def test_empty_path_after_sign_raises(self):
self._write('+\n')
with self.assertRaisesRegex(ValueError, 'empty path after `\\+`'):
parse_paths_file(self.cfg)
def test_error_line_number_matches_offending_line(self):
self._write('+brave/browser/\n'
'\n'
'# a comment\n'
'oops\n')
with self.assertRaisesRegex(ValueError, ':4:'):
parse_paths_file(self.cfg)
class IsPathEnabledTest(unittest.TestCase):
def test_no_rules_means_disabled(self):
self.assertFalse(is_path_enabled('brave/browser/foo.cc', []))
def test_unmatched_path_is_disabled(self):
rules = [('+', 'brave/browser/')]
self.assertFalse(is_path_enabled('brave/components/foo.cc', rules))
def test_single_enable_rule(self):
rules = [('+', 'brave/browser/')]
self.assertTrue(is_path_enabled('brave/browser/foo.cc', rules))
def test_single_disable_rule(self):
rules = [('-', 'brave/browser/')]
self.assertFalse(is_path_enabled('brave/browser/foo.cc', rules))
def test_longest_prefix_wins_disable_overrides_enable(self):
rules = [
('+', 'brave/browser/'),
('-', 'brave/browser/internal/'),
]
self.assertTrue(is_path_enabled('brave/browser/foo.cc', rules))
self.assertFalse(
is_path_enabled('brave/browser/internal/foo.cc', rules))
def test_longest_prefix_wins_enable_overrides_disable(self):
rules = [
('-', 'brave/browser/'),
('+', 'brave/browser/public/'),
]
self.assertFalse(is_path_enabled('brave/browser/foo.cc', rules))
self.assertTrue(is_path_enabled('brave/browser/public/foo.cc', rules))
def test_rule_order_does_not_matter(self):
forward = [
('+', 'brave/browser/'),
('-', 'brave/browser/internal/'),
]
reverse = list(reversed(forward))
path = 'brave/browser/internal/foo.cc'
self.assertEqual(is_path_enabled(path, forward),
is_path_enabled(path, reverse))
def test_prefix_must_align_to_directory_boundary(self):
# The trailing `/` is what prevents `brave/browser/` matching
# `brave/browser_other/...`.
rules = [('+', 'brave/browser/')]
self.assertFalse(is_path_enabled('brave/browser_other/foo.cc', rules))
def test_exact_directory_match_is_enabled(self):
# Source exactly under the enabled prefix.
rules = [('+', 'brave/browser/')]
self.assertTrue(is_path_enabled('brave/browser/a/b/c.cc', rules))
if __name__ == '__main__':
unittest.main()
+15 -1
View File
@@ -3,6 +3,8 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
from __future__ import annotations
from contextlib import contextmanager
import logging
import platform
@@ -188,7 +190,8 @@ class Terminal:
*,
env: dict[str, str] | None = None,
cwd=None,
interactive: bool = False):
interactive: bool = False,
stdin: str | None = None):
"""Runs a command on the terminal.
When `interactive=True`, the subprocess inherits the parent's
@@ -201,6 +204,11 @@ class Terminal:
the parent's environment, a dict fully replaces it. If you want to
add a few keys on top of `os.environ`, copy it first
(`env={**os.environ, ...}`).
Pass `stdin=` to feed data into the subprocess's stdin. Captured
(non-interactive) mode encodes it with utf-8 to match the
captured streams; interactive mode rejects `stdin=` because the
subprocess owns the tty.
"""
# Convert all arguments to strings, to avoid issues with `PurePath`
# being passed arguments
@@ -245,11 +253,17 @@ class Terminal:
text=True,
encoding='utf-8')
if interactive and stdin is not None:
raise ValueError(
'terminal.run(): `stdin=` is not supported with '
'`interactive=True` (the subprocess owns the tty).')
try:
result = subprocess.run(cmd,
check=True,
env=env,
cwd=cwd,
input=stdin,
**capture_kwargs)
except subprocess.CalledProcessError as e:
if e.stderr: