Skip to content

Commit e26c23b

Browse files
committed
Initial commit
0 parents  commit e26c23b

33 files changed

Lines changed: 3408 additions & 0 deletions

.clang-format

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright (c) 2026 Agustin Berge
2+
#
3+
# Distributed under the Boost Software License, Version 1.0.
4+
# See accompanying file LICENSE.txt or copy at
5+
# http://www.boost.org/LICENSE_1_0.txt
6+
---
7+
Language: Cpp
8+
Standard: Latest
9+
10+
# -- Indentation --------------------------------------------------------------
11+
IndentWidth: 4
12+
TabWidth: 4
13+
UseTab: Never
14+
ContinuationIndentWidth: 4
15+
IndentCaseLabels: true
16+
IndentPPDirectives: AfterHash # `# if`, `# define` inside nested ifdefs
17+
NamespaceIndentation: None # namespace bodies are NOT indented
18+
19+
# -- Column limit -------------------------------------------------------------
20+
ColumnLimit: 80 # classic; fits terminal & paper
21+
22+
# -- Brace style --------------------------------------------------------------
23+
BreakBeforeBraces: Mozilla # wraps after functions and classes/structs
24+
25+
# -- Spaces -------------------------------------------------------------------
26+
SpaceBeforeParens: ControlStatements
27+
SpaceInEmptyParentheses: false
28+
SpacesInParentheses: false
29+
SpacesInSquareBrackets: false
30+
SpacesInAngles: false # `vector<int>` not `vector< int >`
31+
SpaceAfterTemplateKeyword: true # `template <typename T>` (space after kw)
32+
SpaceBeforeAssignmentOperators: true
33+
SpaceBeforeCpp11BracedList: false
34+
35+
# -- Pointer / reference alignment --------------------------------------------
36+
# East-const style: `T const&`, `T*` -> pointer/ref binds to the type, not name
37+
PointerAlignment: Left # `T* p` / `T& r` - type-side binding
38+
ReferenceAlignment: Left
39+
40+
# -- Template / requires ------------------------------------------------------
41+
AlwaysBreakTemplateDeclarations: Yes # `template <...>` always on its own line
42+
# before the declaration
43+
44+
# -- Function / call formatting -----------------------------------------------
45+
AllowShortFunctionsOnASingleLine: Inline # only genuinely trivial accessors inline
46+
AllowShortIfStatementsOnASingleLine: true
47+
AllowShortLoopsOnASingleLine: true
48+
AllowShortLambdasOnASingleLine: Inline
49+
AlignAfterOpenBracket: BlockIndent # always breaks, places closing
50+
# brackets on new lines
51+
52+
BinPackArguments: true # all args same line
53+
BinPackParameters: true # same for parameters
54+
55+
# -- Return type --------------------------------------------------------------
56+
AlwaysBreakAfterReturnType: None # return type stays on same line as name
57+
# unless forced by column limit
58+
59+
# -- Constructor initialiser lists --------------------------------------------
60+
BreakConstructorInitializers: BeforeColon # `: base(x)\n, member_(y)` style
61+
ConstructorInitializerIndentWidth: 4
62+
PackConstructorInitializers: Never
63+
64+
# -- Inheritance --------------------------------------------------------------
65+
BreakInheritanceList: BeforeColon
66+
67+
# -- Include sorting ----------------------------------------------------------
68+
SortIncludes: CaseSensitive
69+
IncludeBlocks: Regroup
70+
IncludeCategories:
71+
# 1. The library's own headers <eggs/...>
72+
- Regex: '^<eggs/'
73+
Priority: 1
74+
# 2. Standard library
75+
- Regex: '^<[a-z_]+>$'
76+
Priority: 2
77+
# 3. Local/quoted headers "..."
78+
- Regex: '^"'
79+
Priority: 3
80+
# 4. Everything else (config, platform)
81+
- Regex: '.*'
82+
Priority: 4
83+
84+
# -- Comments -----------------------------------------------------------------
85+
ReflowComments: false # do NOT reflow // section banners
86+
# (e.g. `// namespace detail`)
87+
SpacesBeforeTrailingComments: 1
88+
89+
# -- Alignment ----------------------------------------------------------------
90+
AlignConsecutiveAssignments: false # no forced vertical alignment of `=`
91+
AlignConsecutiveDeclarations: false
92+
AlignTrailingComments: true # trailing `//` comments align in a block
93+
AlignEscapedNewlines: Left
94+
95+
# -- Misc ---------------------------------------------------------------------
96+
Cpp11BracedListStyle: true
97+
FixNamespaceComments: true # auto-add `// namespace foo` at closing }
98+
CompactNamespaces: false
99+
MaxEmptyLinesToKeep: 1
100+
KeepEmptyLinesAtTheStartOfBlocks: false
101+
DerivePointerAlignment: false
102+
SeparateDefinitionBlocks: Always # blank line between top-level definitions
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env python3
2+
# Eggs.Stacktrace
3+
#
4+
# Copyright (c) 2026 Agustin Berge
5+
#
6+
# Distributed under the Boost Software License, Version 1.0.
7+
# See accompanying file LICENSE.txt or copy at
8+
# http://www.boost.org/LICENSE_1_0.txt
9+
10+
# Fail if a tracked header/source file is not referenced by any CMake target.
11+
12+
import json
13+
import os
14+
import pathlib
15+
import subprocess
16+
import sys
17+
18+
SOURCE_EXTENSIONS = {".h", ".hpp", ".c", ".cpp"}
19+
20+
EXCLUDED_DIRS = (
21+
"test/cmake-fetch_content/",
22+
"test/cmake-find_package/",
23+
)
24+
25+
26+
def gh_escape_property(value: str) -> str:
27+
return (
28+
value.replace("%", "%25")
29+
.replace("\r", "%0D")
30+
.replace("\n", "%0A")
31+
.replace(":", "%3A")
32+
.replace(",", "%2C")
33+
)
34+
35+
36+
def tracked_source_files(repo_root: pathlib.Path) -> set[str]:
37+
out = subprocess.run(
38+
["git", "ls-files", "-z"],
39+
cwd=repo_root,
40+
check=True,
41+
capture_output=True,
42+
text=True,
43+
).stdout
44+
45+
files = set()
46+
for line in out.split("\0"):
47+
if not line:
48+
continue
49+
if pathlib.PurePosixPath(line).suffix not in SOURCE_EXTENSIONS:
50+
continue
51+
if line.startswith(EXCLUDED_DIRS):
52+
continue
53+
files.add(line)
54+
return files
55+
56+
57+
def referenced_source_files(build_dir: pathlib.Path) -> set[str]:
58+
reply_dir = build_dir / ".cmake" / "api" / "v1" / "reply"
59+
index_files = sorted(reply_dir.glob("index-*.json"))
60+
if not index_files:
61+
sys.exit(f"error: no CMake File API reply found in {reply_dir}")
62+
63+
index = json.loads(index_files[-1].read_text(encoding="utf-8"))
64+
codemodel_file = reply_dir / index["reply"]["codemodel-v2"]["jsonFile"]
65+
codemodel = json.loads(codemodel_file.read_text(encoding="utf-8"))
66+
67+
referenced = set()
68+
for target_ref in codemodel["configurations"][0]["targets"]:
69+
target = json.loads(
70+
(reply_dir / target_ref["jsonFile"]).read_text(encoding="utf-8")
71+
)
72+
for source in target.get("sources", []):
73+
if not source.get("isGenerated", False):
74+
referenced.add(source["path"])
75+
return referenced
76+
77+
78+
def main() -> int:
79+
if len(sys.argv) != 2:
80+
sys.exit(f"usage: {sys.argv[0]} <build-dir>")
81+
82+
build_dir = pathlib.Path(sys.argv[1]).resolve()
83+
repo_root = pathlib.Path(
84+
subprocess.run(
85+
["git", "rev-parse", "--show-toplevel"],
86+
check=True,
87+
capture_output=True,
88+
text=True,
89+
).stdout.strip()
90+
)
91+
92+
tracked = tracked_source_files(repo_root)
93+
referenced = referenced_source_files(build_dir)
94+
95+
orphans = sorted(tracked - referenced)
96+
if orphans:
97+
print("error: files not referenced by any CMake target:")
98+
in_ci = os.environ.get("GITHUB_ACTIONS") == "true"
99+
for path in orphans:
100+
print(f" {path}")
101+
if in_ci:
102+
print(
103+
f"::error file={gh_escape_property(path)}::"
104+
"not referenced by any CMake target"
105+
)
106+
return 1
107+
108+
print(
109+
f"OK: all {len(tracked)} tracked header/source files "
110+
"are referenced by a CMake target."
111+
)
112+
return 0
113+
114+
115+
if __name__ == "__main__":
116+
sys.exit(main())
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env python3
2+
# Eggs.Stacktrace
3+
#
4+
# Copyright (c) 2026 Agustin Berge
5+
#
6+
# Distributed under the Boost Software License, Version 1.0.
7+
# See accompanying file LICENSE.txt or copy at
8+
# http://www.boost.org/LICENSE_1_0.txt
9+
10+
"""Annotate any -Weverything warning in a merged SARIF report that falls on a
11+
line changed relative to a base ref (e.g. origin/main).
12+
"""
13+
14+
import json
15+
import re
16+
import subprocess
17+
import sys
18+
19+
HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(?P<start>\d+)(?:,(?P<count>\d+))? @@")
20+
21+
22+
def changed_lines(base_ref: str) -> dict[str, set[int]]:
23+
diff = subprocess.run(
24+
["git", "diff", "--unified=0", f"{base_ref}...HEAD"],
25+
capture_output=True,
26+
text=True,
27+
check=True,
28+
).stdout
29+
30+
changed: dict[str, set[int]] = {}
31+
current_file = None
32+
for line in diff.splitlines():
33+
if line.startswith("+++ "):
34+
path = line[4:]
35+
current_file = None if path == "/dev/null" else path.removeprefix("b/")
36+
elif line.startswith("@@") and current_file is not None:
37+
match = HUNK_RE.match(line)
38+
if not match:
39+
continue
40+
start = int(match["start"])
41+
count = int(match["count"]) if match["count"] is not None else 1
42+
if count == 0:
43+
continue # pure deletion, nothing added at this location
44+
changed.setdefault(current_file, set()).update(range(start, start + count))
45+
return changed
46+
47+
48+
def main() -> int:
49+
if len(sys.argv) != 3:
50+
sys.exit(f"usage: {sys.argv[0]} <sarif-path> <base-ref>")
51+
52+
sarif_path, base_ref = sys.argv[1], sys.argv[2]
53+
with open(sarif_path, encoding="utf-8") as f:
54+
results = json.load(f)["runs"][0]["results"]
55+
56+
changed = changed_lines(base_ref)
57+
58+
seen = set()
59+
flagged = []
60+
for result in results:
61+
location = result["locations"][0]["physicalLocation"]
62+
uri = location["artifactLocation"]["uri"]
63+
region = location["region"]
64+
if region["startLine"] not in changed.get(uri, set()):
65+
continue
66+
key = (uri, region["startLine"], result["ruleId"])
67+
if key in seen:
68+
continue
69+
seen.add(key)
70+
flagged.append((uri, region, result["ruleId"], result["message"]["text"]))
71+
72+
for uri, region, rule_id, message in flagged:
73+
print(
74+
f"::error file={uri},line={region['startLine']},col={region['startColumn']}::[{rule_id}] {message}"
75+
)
76+
77+
if flagged:
78+
print(
79+
f"{len(flagged)} -Weverything warning(s) on lines changed relative to {base_ref}.",
80+
file=sys.stderr,
81+
)
82+
83+
return 0
84+
85+
86+
if __name__ == "__main__":
87+
sys.exit(main())
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python3
2+
# Eggs.Stacktrace
3+
#
4+
# Copyright (c) 2026 Agustin Berge
5+
#
6+
# Distributed under the Boost Software License, Version 1.0.
7+
# See accompanying file LICENSE.txt or copy at
8+
# http://www.boost.org/LICENSE_1_0.txt
9+
10+
"""CMAKE_CXX_COMPILER_LAUNCHER wrapper that captures per-TU -Weverything
11+
diagnostics as a SARIF result fragment, without altering the real build.
12+
"""
13+
14+
import json
15+
import os
16+
import re
17+
import subprocess
18+
import sys
19+
20+
DIAGNOSTIC_RE = re.compile(
21+
r"^(?P<file>[^:\n]+):(?P<line>\d+):(?P<column>\d+): warning: (?P<message>.*)$"
22+
)
23+
FLAG_RE = re.compile(r"^(?P<text>.*) \[-(?P<flag>W[\w-]+)\]$")
24+
25+
26+
def find_output(args: list[str]) -> str | None:
27+
for prev, arg in zip(args, args[1:]):
28+
if prev == "-o":
29+
return arg
30+
return None
31+
32+
33+
def parse_diagnostics(stderr: str) -> list[dict]:
34+
source_dir = os.environ.get("EGGS_STACKTRACE_SOURCE_DIR")
35+
results = []
36+
for line in stderr.splitlines():
37+
match = DIAGNOSTIC_RE.match(line)
38+
if not match:
39+
continue
40+
41+
file = match["file"]
42+
if source_dir:
43+
try:
44+
file = os.path.relpath(file, source_dir)
45+
except ValueError:
46+
pass
47+
48+
flag_match = FLAG_RE.match(match["message"])
49+
rule_id = f"-{flag_match['flag']}" if flag_match else "clang-diagnostic"
50+
text = flag_match["text"] if flag_match else match["message"]
51+
52+
results.append(
53+
{
54+
"ruleId": rule_id,
55+
"level": "warning",
56+
"message": {"text": text},
57+
"locations": [
58+
{
59+
"physicalLocation": {
60+
"artifactLocation": {"uri": file.replace(os.sep, "/")},
61+
"region": {
62+
"startLine": int(match["line"]),
63+
"startColumn": int(match["column"]),
64+
},
65+
},
66+
}
67+
],
68+
}
69+
)
70+
return results
71+
72+
73+
def main() -> int:
74+
compiler, *args = sys.argv[1:]
75+
proc = subprocess.run([compiler, *args], capture_output=True, text=True)
76+
sys.stdout.write(proc.stdout)
77+
sys.stderr.write(proc.stderr)
78+
79+
output = find_output(args)
80+
if output is not None:
81+
results = parse_diagnostics(proc.stderr)
82+
sidecar = output + ".sarif.json"
83+
os.makedirs(os.path.dirname(sidecar) or ".", exist_ok=True)
84+
with open(sidecar, "w", encoding="utf-8") as f:
85+
json.dump(results, f)
86+
87+
return proc.returncode
88+
89+
90+
if __name__ == "__main__":
91+
sys.exit(main())

0 commit comments

Comments
 (0)