Skip to content

Commit 25aa9b7

Browse files
feat: force clean build after warnings or dependency changes (#555)
Co-authored-by: Maximilian Sören Pollak <maximilian.pollak@qorix.com>
1 parent 4e83e56 commit 25aa9b7

5 files changed

Lines changed: 179 additions & 2 deletions

File tree

src/BUILD

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
# *******************************************************************************
1313

1414
load("@aspect_rules_py//py:defs.bzl", "py_library")
15+
load("@docs_as_code_hub_env//:requirements.bzl", "all_requirements")
1516
load("@rules_java//java:java_binary.bzl", "java_binary")
1617
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
18+
load("//:score_pytest.bzl", "score_pytest")
1719

1820
# These are only exported because they're passed as files to the //docs.bzl
1921
# macros, and thus must be visible to other packages. They should only be
@@ -30,6 +32,16 @@ exports_files(
3032
visibility = ["//visibility:public"],
3133
)
3234

35+
score_pytest(
36+
name = "incremental_dirty_build_test",
37+
srcs = [
38+
"incremental_dirty_build_test.py",
39+
"incremental.py",
40+
],
41+
deps = all_requirements,
42+
pytest_config = "//:pyproject.toml",
43+
)
44+
3345
filegroup(
3446
name = "all_sources",
3547
srcs = glob(

src/incremental.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
# *******************************************************************************
1313

1414
import argparse
15+
import hashlib
1516
import logging
1617
import os
18+
import shutil
1719
import sys
1820
import time
1921
from pathlib import Path
@@ -26,6 +28,8 @@
2628

2729
logger = logging.getLogger(__name__)
2830

31+
_MODULE_HASH_FILE = ".module_bazel_hash"
32+
2933

3034
def get_env(name: str) -> str:
3135
val = os.environ.get(name, None)
@@ -35,6 +39,38 @@ def get_env(name: str) -> str:
3539
return val
3640

3741

42+
def _compute_hash(files: list[Path]) -> str:
43+
h = hashlib.sha256()
44+
for f in sorted(files, key=str):
45+
h.update(f.read_bytes())
46+
return h.hexdigest()
47+
48+
49+
def clean_builddir_if_stale(build_dir: Path, sentinel_files: list[Path]) -> None:
50+
"""Delete build_dir if the previous build had warnings or any sentinel file changed."""
51+
if not build_dir.exists():
52+
return
53+
54+
warnings_txt = build_dir / "warnings.txt"
55+
has_warnings = warnings_txt.exists() and warnings_txt.stat().st_size > 0
56+
57+
hash_file = build_dir / _MODULE_HASH_FILE
58+
hash_changed = (
59+
not hash_file.exists()
60+
or hash_file.read_text().strip() != _compute_hash(sentinel_files)
61+
)
62+
63+
if has_warnings or hash_changed:
64+
print(
65+
"Previous build had warnings or the hash changed. Removing _build to ensure a clean build."
66+
)
67+
shutil.rmtree(build_dir)
68+
69+
70+
def update_module_hash(build_dir: Path, sentinel_files: list[Path]) -> None:
71+
(build_dir / _MODULE_HASH_FILE).write_text(_compute_hash(sentinel_files))
72+
73+
3874
if __name__ == "__main__":
3975
parser = argparse.ArgumentParser()
4076
# Add debuging functionality
@@ -73,9 +109,21 @@ def get_env(name: str) -> str:
73109
else:
74110
workspace = ""
75111

112+
build_dir = Path(workspace + "_build")
113+
sentinel_files = [
114+
Path(workspace + "MODULE.bazel"),
115+
Path(workspace + "MODULE.bazel.lock"),
116+
Path(workspace + "BUILD"),
117+
]
118+
clean_builddir_if_stale(build_dir, sentinel_files)
119+
120+
warning_file = Path(workspace + "_build/warnings.txt")
121+
76122
base_arguments = [
77123
workspace + get_env("SOURCE_DIRECTORY"),
78124
workspace + "_build",
125+
"--warning-file",
126+
str(warning_file),
79127
"-W", # treat warning as errors
80128
"--keep-going", # do not abort after one error
81129
"-T", # show details in case of errors in extensions
@@ -134,7 +182,13 @@ def get_env(name: str) -> str:
134182
start_time = time.perf_counter()
135183
exit_code = sphinx_main(base_arguments)
136184
end_time = time.perf_counter()
137-
duration = end_time - start_time
138-
print(f"docs ({action}) finished in {duration:.1f} seconds")
185+
print(f"docs ({action}) finished in {end_time - start_time:.1f} seconds")
186+
187+
if exit_code == 0:
188+
update_module_hash(build_dir, sentinel_files)
189+
else:
190+
with warning_file.open("a", encoding="utf-8") as f:
191+
f.write("-" * 80 + "\n")
192+
f.write(f"Build failed with exit code {exit_code}\n")
139193

140194
sys.exit(exit_code)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
14+
# Unit Tests of incremental.py
15+
16+
from pathlib import Path
17+
18+
from incremental import clean_builddir_if_stale, update_module_hash
19+
20+
_BUILD = Path("/build")
21+
_MODULE = Path("/MODULE.bazel")
22+
_LOCK = Path("/MODULE.bazel.lock")
23+
24+
25+
def _simulate_old_state(fs, warnings: str | None) -> None:
26+
"""Helper function to set up a build directory with an old hash and warnings."""
27+
28+
fs.create_dir(_BUILD)
29+
fs.create_file(_MODULE, contents="stable")
30+
fs.create_file(_LOCK, contents="old lock")
31+
update_module_hash(_BUILD, [_MODULE, _LOCK])
32+
if warnings is not None:
33+
fs.create_file(_BUILD / "warnings.txt", contents=warnings)
34+
35+
36+
def test_clean_removes_build_dir_when_previous_build_had_warnings(fs) -> None:
37+
"""If warnings.txt exists and is not empty, the build dir is removed."""
38+
39+
_simulate_old_state(fs, warnings="WARNING: something went wrong")
40+
41+
clean_builddir_if_stale(_BUILD, [_MODULE])
42+
43+
assert not _BUILD.exists()
44+
45+
46+
def test_clean_keeps_build_dir_when_warnings_txt_is_empty(fs) -> None:
47+
"""If warnings.txt exists and is empty, the build dir is kept."""
48+
49+
_simulate_old_state(fs, warnings="")
50+
51+
clean_builddir_if_stale(_BUILD, [_MODULE, _LOCK])
52+
53+
assert _BUILD.exists()
54+
55+
56+
def test_clean_is_noop_when_warnings_txt_is_absent(fs) -> None:
57+
"""If warnings.txt does not exist, the build dir is kept (no error)."""
58+
59+
_simulate_old_state(fs, warnings=None)
60+
61+
clean_builddir_if_stale(_BUILD, [_MODULE, _LOCK])
62+
63+
assert _BUILD.exists()
64+
65+
66+
def test_clean_is_noop_when_build_dir_is_absent(fs) -> None:
67+
fs.create_file(_MODULE, contents="stable")
68+
69+
clean_builddir_if_stale(_BUILD, [_MODULE])
70+
71+
72+
def test_module_changed_removes_build_dir_when_one_sentinel_file_changed(fs) -> None:
73+
_simulate_old_state(fs, warnings=None)
74+
75+
_LOCK.write_bytes(b"new lock")
76+
clean_builddir_if_stale(_BUILD, [_MODULE, _LOCK])
77+
78+
assert not _BUILD.exists()
79+
80+
81+
def test_module_changed_keeps_build_dir_when_all_sentinel_files_unchanged(fs) -> None:
82+
_simulate_old_state(fs, warnings=None)
83+
84+
clean_builddir_if_stale(_BUILD, [_MODULE, _LOCK])
85+
86+
assert _BUILD.exists()
87+
88+
89+
def test_module_change_after_successful_build_forces_clean(fs) -> None:
90+
_simulate_old_state(fs, warnings=None)
91+
92+
_MODULE.write_bytes(b"version 2")
93+
clean_builddir_if_stale(_BUILD, [_MODULE])
94+
95+
assert not _BUILD.exists()
96+
97+
98+
def test_missing_hash_file_triggers_clean(fs) -> None:
99+
"""If _build/ exists but hash file is absent, treat as stale (e.g. upgrade from old version)."""
100+
fs.create_dir(_BUILD)
101+
fs.create_file(_MODULE, contents="stable")
102+
# No hash file written
103+
104+
clean_builddir_if_stale(_BUILD, [_MODULE])
105+
106+
assert not _BUILD.exists()

src/requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ bazel-runfiles
3535

3636
# Local development
3737
pytest
38+
pyfakefs
3839
basedpyright

src/requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,10 @@ pydata-sphinx-theme==0.17.0 \
10621062
--hash=sha256:529c5631582cb3328cf4814fb9eb80611d1704c854406d282a75c9c86e3a1955 \
10631063
--hash=sha256:cec5c92f41f4a11541b6df8210c446b4aa9c3badb7fcf2db7893405b786d5c99
10641064
# via -r requirements.in
1065+
pyfakefs==6.2.0 \
1066+
--hash=sha256:0968a49db692694ffed420e54a9f1cbae4636637b880e8ab09c8ccc0f11bd7ae \
1067+
--hash=sha256:e59a36db447bf509ce9c97ab3d1510c08cc51895c5311325a560a5e5b5dc1940
1068+
# via -r requirements.in
10651069
pygithub==2.9.0 \
10661070
--hash=sha256:5e2b260ce327bffce9b00f447b65953ef7078ffe93e5a5425624a3075483927c \
10671071
--hash=sha256:a26abda1222febba31238682634cad11d8b966137ed6cc3c5e445b29a11cb0a4

0 commit comments

Comments
 (0)