mirror of
https://github.com/apache/doris.git
synced 2026-11-04 07:31:17 +00:00
### What problem does this PR solve? Related PR: #66510 (the BE build-time series, #66615..#66826, that this PR locks in) Problem Summary: The build-time series cut BE cold builds by ~49% and took `olap_common.h` / `uid_util.h` off the everything-rebuilds line. Every one of those wins can regress **silently**: a stray include, a lost `extern template`, a header added to the PCH — the code still compiles, only every build afterwards is slower, and nothing reports it. `#66052` already showed the failure mode: the layering rules in `check-header-deps.py` only worked because the author happened to maintain them by hand; nothing in CI runs them. This PR turns each known regression vector into a **fail-loud, seconds-level gate that runs at configure time**, so a violation is a build error on every path that builds the BE (CI, `build.sh`, the UT build) with zero extra CI wiring — the same mounting pattern the FE side already uses for its architecture gates (`exec-maven-plugin` running `check-fe-core-metadata-funnel.sh` at `validate`). **Checks** (pure text scans, ~1.3s combined, zero build dependency, zero false positives on current master): | Gate | Regression it stops | |---|---| | Header layering rules (30, +6 new) | a hub header (e.g. `exec_env.h`, `function.h`, `pch.h`) re-growing an include edge into a subsystem that was deliberately cut | | Third-party ban table (new) | `fmt`/`boost`/`concurrentqueue.h`/`<ranges>` returning to headers whose bodies were moved out of line to contain template-instantiation cost (`<ranges>` in a src header also breaks the `-fno-access-control` UT build on libc++) | | Two-axis budgets (new) | the edges nobody names: light hubs must stay light (include-closure budget, zero slack), heavy payloads must not spread (transitive-reach baseline +10%); `--closure` / `--reach` subcommands rank the offending edges for the fix | | pch whitelist (new) | any quoted include added to `pch/pch.h` — the single most leveraged regression in the repo (touch one such header and the whole backend plus the PCH rebuild) | | `check-extern-template-pairing.py` (new) | the one **silent** extern-template failure: a `.cpp` keeps the explicit instantiation but the header loses the `extern` declaration, so every TU quietly re-instantiates; also the loud direction (declaration with no definition), reported at once instead of at link | | `check-unity-skip-coverage.py` (new) | a test `#include`-ing a src `.cpp` that is not opted out of unity batching — reported at configure with the exact skip entry to add, instead of as duplicate symbols at the end of the BE UT link | `build-support/check-build-hygiene.sh` runs all three scripts; `be/CMakeLists.txt` executes it at configure time. Escape hatches, by design: - `-DENABLE_BUILD_HYGIENE=OFF` (default ON; a missing `python3` is a loud FATAL naming this switch, never a silent skip); - every budget/whitelist/rule table lives in the scripts themselves, so the legal way past a gate is a **one-line reviewed diff in the same PR** — the gate is never a dead end. Every failure message carries the mechanism (why this edge is expensive) and the concrete fix path. **Found by the new gates on day one, fixed here:** `core/types.h` has carried five `extern template struct fmt::formatter<Decimal*>;` declarations with **no matching definition anywhere** since #53483 — every member is defined in-class, so the linker never noticed; deleted. Also made the FE funnel self-test's `sed -i` invocation portable to BSD/macOS. Blind spot, by design: instantiations expanded from macros (e.g. `DECLARE_OPERATOR` in `operator.cpp`) are invisible to the pairing scan on both sides — macro bodies are skipped; a comment at the site says so. `be/README.md` (new) documents the gates, what to do when each fires, and the `*_fwd.h` convention as the sanctioned way through a layering barrier. The same rules are encoded for AI-assisted development and review: `be/AGENTS.md` (new — include discipline while writing BE code + review checkpoints), `build-support/AGENTS.md` (new — maintenance discipline for the gate scripts themselves), an include-src-cpp checkpoint in `be/test/AGENTS.md`, and a Header Hygiene entry in the root `AGENTS.md`, so coding agents and the review pipeline apply them automatically. ### Release note None ### Check List (For Author) - Test - [x] Manual test (add detailed scripts or steps below) - `build-support/check-build-hygiene.sh` on master: all green, ~1.3s, zero false positives. - Self-tests `bash build-support/tests/run.sh` (7/7 PASS): red/green injection per gate family — each seeded violation turns red with the mechanism + fix in the message, restoring the file turns it green again. - Real configure on macOS arm64: green path passes the gate section; with an injected violation, configure dies at the gate in under a second with the full message; `-DENABLE_BUILD_HYGIENE=OFF` bypasses; a missing `python3` is a loud exit-2 FATAL naming the switch. - This PR's own Linux CI exercises the configure mount on the second platform. - Behavior changed: - [x] Yes. BE configure now fails fast (with mechanism + fix in the message) on header-hygiene violations; emergency bypass via `-DENABLE_BUILD_HYGIENE=OFF`. No runtime behavior change — the only source-code changes are comments and the deletion of five never-defined `extern template` declarations. - Does this need documentation? - [x] No. Developer-facing docs are included in-repo (`be/README.md`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01EyZL631YTeuNXa9WLjvk7s --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
227 lines
9.1 KiB
Python
Executable File
227 lines
9.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# 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.
|
|
|
|
"""Unity-skip coverage for test-included source files.
|
|
|
|
A few tests `#include` a be/src .cpp directly (to reach file-static helpers or
|
|
to instantiate a template with test types). That only links because the source
|
|
file is opted out of unity batching: if it stays in a batch, the batch object is
|
|
pulled into the doris_be_test link for a *sibling's* symbol and its copy of the
|
|
file's definitions collides with the test's inlined copy -- duplicate strong
|
|
symbols, reported at the end of the BE UT build, an hour after the mistake.
|
|
This script reports the mistake at configure time instead, with the exact skip
|
|
entry to add.
|
|
|
|
It is the mirror of the fail-loud check inside doris_skip_unity_inclusion()
|
|
(be/CMakeLists.txt): that one catches skip entries pointing at files that no
|
|
longer exist; this one catches files that need an entry and do not have one.
|
|
|
|
A source file counts as covered when its owning CMakeLists (nearest ancestor
|
|
with a CMakeLists.txt, else any CMakeLists naming it) does one of:
|
|
|
|
literal entry the file's path appears in a list fed to
|
|
doris_skip_unity_inclusion(), via set()/list(APPEND)/direct
|
|
arguments;
|
|
whole-target the skip list is initialized from the target's own source
|
|
opt-out list (`set(<VAR> ${SRC_FILES})`), minus any
|
|
`list(FILTER <VAR> EXCLUDE REGEX ...)` the file matches;
|
|
no unity the owning CMakeLists never sets UNITY_BUILD, so there are
|
|
no batches to collide with (archive link rules protect
|
|
plain object files).
|
|
|
|
Any other CMake idiom is unknown to this script on purpose: it fails loudly so
|
|
the idiom is either replaced with a literal entry or taught here, instead of
|
|
being silently guessed wrong.
|
|
|
|
Usage:
|
|
build-support/check-unity-skip-coverage.py
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
TEST_ROOT = "be/test"
|
|
SRC_ROOT = "be/src"
|
|
|
|
TEST_INCLUDE = re.compile(r'^\s*#\s*include\s+"([^"]+\.(?:cpp|cc))"')
|
|
SKIP_CALL = re.compile(r"doris_skip_unity_inclusion\s*\(([^)]*)\)", re.S)
|
|
SET_BLOCK = re.compile(r"\bset\s*\(\s*(\w+)([^)]*)\)", re.S)
|
|
APPEND_BLOCK = re.compile(r"\blist\s*\(\s*APPEND\s+(\w+)([^)]*)\)", re.S)
|
|
FILTER_BLOCK = re.compile(
|
|
r'\blist\s*\(\s*FILTER\s+(\w+)\s+EXCLUDE\s+REGEX\s+"([^"]*)"\s*\)'
|
|
)
|
|
VAR_REF = re.compile(r"\$\{(\w+)\}")
|
|
|
|
|
|
def strip_comments(text):
|
|
return "\n".join(line.split("#", 1)[0] for line in text.splitlines())
|
|
|
|
|
|
def cpp_tokens(blob):
|
|
"""Path-shaped tokens in a CMake argument blob, ${...} prefixes stripped."""
|
|
tokens = []
|
|
for token in blob.split():
|
|
if not token.endswith((".cpp", ".cc")):
|
|
continue
|
|
tokens.append(re.sub(r"\$\{\w+\}", "", token).lstrip("/"))
|
|
return tokens
|
|
|
|
|
|
class CMakeFile:
|
|
def __init__(self, path):
|
|
self.path = path
|
|
self.directory = os.path.dirname(path)
|
|
with open(path, encoding="utf-8", errors="ignore") as handle:
|
|
text = strip_comments(handle.read())
|
|
self.sets_unity = "UNITY_BUILD" in text
|
|
|
|
sets = {}
|
|
for name, blob in SET_BLOCK.findall(text):
|
|
sets.setdefault(name, []).append(blob)
|
|
for name, blob in APPEND_BLOCK.findall(text):
|
|
sets.setdefault(name, []).append(blob)
|
|
filters = {}
|
|
for name, regex in FILTER_BLOCK.findall(text):
|
|
filters.setdefault(name, []).append(regex)
|
|
|
|
# Literal skip entries and whether the skip list is seeded from the
|
|
# target's whole source list.
|
|
self.entries = []
|
|
self.whole_target = False
|
|
self.whole_target_excludes = []
|
|
for blob in SKIP_CALL.findall(text):
|
|
self.entries.extend(cpp_tokens(blob))
|
|
for var in VAR_REF.findall(blob):
|
|
for var_blob in sets.get(var, ()):
|
|
self.entries.extend(cpp_tokens(var_blob))
|
|
if re.search(r"\$\{\w*SRC_FILES\}", var_blob):
|
|
self.whole_target = True
|
|
self.whole_target_excludes.extend(filters.get(var, ()))
|
|
|
|
def resolved_entries(self):
|
|
"""Skip entries as repo-relative paths (handles ../ hops)."""
|
|
for entry in self.entries:
|
|
yield os.path.normpath(os.path.join(self.directory, entry))
|
|
|
|
def covers(self, repo_path):
|
|
"""Does this CMakeLists skip-list `repo_path` (repo-relative .cpp)?"""
|
|
for resolved in self.resolved_entries():
|
|
if resolved == repo_path or resolved.endswith("/" + repo_path):
|
|
return True
|
|
if self.whole_target and repo_path.startswith(self.directory + "/"):
|
|
# cmake applies FILTER regexes to absolute paths; approximate with
|
|
# the repo-relative one, which shares every path component that the
|
|
# tree's regexes (".*/format_v2/.*") anchor on.
|
|
return not any(
|
|
re.search(regex, repo_path)
|
|
for regex in self.whole_target_excludes
|
|
)
|
|
return False
|
|
|
|
|
|
def owning_cmakelists(repo_path, cmake_files):
|
|
directory = os.path.dirname(repo_path)
|
|
while directory.startswith(SRC_ROOT):
|
|
candidate = os.path.join(directory, "CMakeLists.txt")
|
|
if candidate in cmake_files:
|
|
return cmake_files[candidate]
|
|
directory = os.path.dirname(directory)
|
|
return None
|
|
|
|
|
|
def test_included_sources():
|
|
"""(test file, line, be/src-relative include) for every src .cpp a test
|
|
includes."""
|
|
found = []
|
|
for directory, _, names in os.walk(TEST_ROOT):
|
|
for name in names:
|
|
if not name.endswith((".cpp", ".cc", ".h", ".hpp")):
|
|
continue
|
|
path = os.path.join(directory, name)
|
|
with open(path, encoding="utf-8", errors="ignore") as handle:
|
|
for lineno, line in enumerate(handle, 1):
|
|
match = TEST_INCLUDE.match(line)
|
|
if not match:
|
|
continue
|
|
include = match.group(1)
|
|
if os.path.exists(os.path.join(SRC_ROOT, include)):
|
|
found.append((path, lineno, include))
|
|
return found
|
|
|
|
|
|
def main():
|
|
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
os.chdir(root)
|
|
|
|
cmake_files = {}
|
|
for directory, _, names in os.walk(SRC_ROOT):
|
|
if "CMakeLists.txt" in names:
|
|
path = os.path.join(directory, "CMakeLists.txt")
|
|
cmake_files[path] = CMakeFile(path)
|
|
|
|
failures = 0
|
|
checked = 0
|
|
for test_path, lineno, include in test_included_sources():
|
|
checked += 1
|
|
repo_path = os.path.join(SRC_ROOT, include)
|
|
owner = owning_cmakelists(repo_path, cmake_files)
|
|
if owner is not None and not owner.sets_unity:
|
|
continue # no unity batches in this target, nothing to collide with
|
|
if owner is not None and owner.covers(repo_path):
|
|
continue
|
|
if any(f.covers(repo_path) for f in cmake_files.values()):
|
|
continue # cross-directory entry (e.g. appended into a sibling's list)
|
|
failures += 1
|
|
print(
|
|
f"error: {test_path}:{lineno} includes {repo_path}, which is not "
|
|
"opted out of unity batching",
|
|
file=sys.stderr,
|
|
)
|
|
print(
|
|
" reason: the unity batch object containing this file gets pulled "
|
|
"into the doris_be_test link for a sibling's symbol, and its "
|
|
"definitions collide with the test's inlined copy -- duplicate "
|
|
"strong symbols at the end of the BE UT build",
|
|
file=sys.stderr,
|
|
)
|
|
if owner is not None:
|
|
entry = os.path.relpath(repo_path, owner.directory)
|
|
print(
|
|
f" fix: add ${{CMAKE_CURRENT_SOURCE_DIR}}/{entry} to the "
|
|
f"doris_skip_unity_inclusion list in {owner.path}",
|
|
file=sys.stderr,
|
|
)
|
|
else:
|
|
print(
|
|
" fix: add the file to the doris_skip_unity_inclusion "
|
|
"list of the CMakeLists whose source glob compiles it; for "
|
|
"the cross-directory entry form, see the adbc_reader.cpp "
|
|
"entry in be/src/format/CMakeLists.txt",
|
|
file=sys.stderr,
|
|
)
|
|
if failures:
|
|
print(f"\n{failures} unity-skip coverage violation(s)", file=sys.stderr)
|
|
return 1
|
|
print(f"unity-skip coverage: {checked} test-included source file(s) all covered")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|