Skip to content

Commit 406b34a

Browse files
committed
Fix timestamp checking and hashing
1 parent e5d58e0 commit 406b34a

File tree

2 files changed

+59
-16
lines changed

2 files changed

+59
-16
lines changed

tools/cpbuild/build_circuitpython.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,10 @@ async def preprocess_and_split_defs(compiler, source_file, build_path, flags):
5353

5454
async def collect_defs(mode, build_path):
5555
output_file = build_path / f"{mode}defs.collected"
56+
splitdir = build_path / "genhdr" / mode
5657
await cpbuild.run_command(
57-
[
58-
"python",
59-
srcdir / "py" / "makeqstrdefs.py",
60-
"cat",
61-
mode,
62-
"_",
63-
build_path / "genhdr" / mode,
64-
output_file,
65-
],
66-
srcdir,
58+
["cat", "-s", *splitdir.glob(f"**/*.{mode}"), ">", output_file],
59+
splitdir,
6760
)
6861
return output_file
6962

@@ -184,8 +177,18 @@ async def build_circuitpython():
184177

185178
genhdr = builddir / "genhdr"
186179
genhdr.mkdir(exist_ok=True, parents=True)
180+
version_header = genhdr / "mpversion.h"
187181
await cpbuild.run_command(
188-
["python", srcdir / "py" / "makeversionhdr.py", genhdr / "mpversion.h"], srcdir
182+
[
183+
"python",
184+
srcdir / "py" / "makeversionhdr.py",
185+
version_header,
186+
"&&",
187+
"touch",
188+
version_header,
189+
],
190+
srcdir,
191+
check_hash=[version_header],
189192
)
190193

191194
supervisor_source = [
@@ -310,6 +313,6 @@ async def main():
310313
handler = colorlog.StreamHandler()
311314
handler.setFormatter(colorlog.ColoredFormatter("%(log_color)s%(levelname)s:%(name)s:%(message)s"))
312315

313-
logging.basicConfig(level=logging.INFO, handlers=[handler])
316+
logging.basicConfig(level=logging.DEBUG, handlers=[handler])
314317

315318
asyncio.run(main())

tools/cpbuild/cpbuild.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,10 @@ async def __aenter__(self):
8080
future = loop.create_future()
8181
self.pending_futures.append(future)
8282
self.read_transport.resume_reading()
83-
print("aenter")
8483
self.tokens_in_use.append(await future)
8584

8685
async def __aexit__(self, exc_type, exc, tb):
8786
token = self.tokens_in_use.pop()
88-
print("aexit")
8987
self.new_token(token)
9088

9189

@@ -103,7 +101,21 @@ def _create_semaphore():
103101
max_track = 0
104102

105103

106-
async def run_command(command, working_directory, description=None):
104+
async def run_command(command, working_directory, description=None, check_hash=[]):
105+
"""
106+
Runs a command asynchronously. The command should ideally be a list of strings
107+
and pathlib.Path objects. If all of the paths haven't been modified since the last
108+
time the command was run, then it'll be skipped. (The last time a command was run
109+
is stored based on the hash of the command.)
110+
111+
The command is run from the working_directory and the paths are made relative to it.
112+
113+
Description is used for logging only. If None, the command itself is logged.
114+
115+
Paths in check_hash are hashed before and after the command. If the hash is
116+
the same, then the old mtimes are reset. This is helpful if a command may produce
117+
the same result and you don't want the rest of the build impacted.
118+
"""
107119
paths = []
108120
if isinstance(command, list):
109121
for i, part in enumerate(command):
@@ -129,10 +141,24 @@ async def run_command(command, working_directory, description=None):
129141
if command_hash in LAST_BUILD_TIMES and all((p.exists() for p in paths)):
130142
newest_file = max((p.stat().st_mtime_ns for p in paths))
131143
last_build_time = LAST_BUILD_TIMES[command_hash]
132-
if last_build_time < newest_file:
144+
if last_build_time <= newest_file:
133145
ALREADY_RUN[command_hash].set()
134146
return
135147

148+
else:
149+
newest_file = 0
150+
151+
file_hashes = {}
152+
for path in check_hash:
153+
if not path.exists():
154+
continue
155+
with path.open("rb") as f:
156+
digest = hashlib.file_digest(f, "sha256")
157+
stat = path.stat()
158+
mtimes = (stat.st_atime, stat.st_mtime)
159+
mtimes_ns = (stat.st_atime_ns, stat.st_mtime_ns)
160+
file_hashes[path] = (digest, mtimes, mtimes_ns)
161+
136162
cancellation = None
137163
async with shared_semaphore:
138164
global max_track
@@ -167,9 +193,19 @@ async def run_command(command, working_directory, description=None):
167193
tracks.append(track)
168194

169195
if process.returncode == 0:
196+
old_newest_file = newest_file
170197
newest_file = max((p.stat().st_mtime_ns for p in paths))
171198
LAST_BUILD_TIMES[command_hash] = newest_file
172199

200+
for path in check_hash:
201+
if path not in file_hashes:
202+
continue
203+
with path.open("rb") as f:
204+
digest = hashlib.file_digest(f, "sha256")
205+
old_digest, _, old_mtimes_ns = file_hashes[path]
206+
if old_digest.digest() == digest.digest():
207+
os.utime(path, ns=old_mtimes_ns)
208+
173209
# If something has failed and we've been canceled, hide our success so
174210
# the error is clear.
175211
if cancellation:
@@ -179,6 +215,9 @@ async def run_command(command, working_directory, description=None):
179215
logger.debug(command)
180216
else:
181217
logger.info(command)
218+
if old_newest_file == newest_file:
219+
logger.error("No files were modified by the command.")
220+
raise RuntimeError()
182221
ALREADY_RUN[command_hash].set()
183222
else:
184223
if command_hash in LAST_BUILD_TIMES:
@@ -268,6 +307,7 @@ async def preprocess(
268307
],
269308
description=f"Preprocess {source_file.relative_to(self.srcdir)} -> {output_file.relative_to(self.builddir)}",
270309
working_directory=self.srcdir,
310+
check_hash=[output_file],
271311
)
272312

273313
async def compile(

0 commit comments

Comments
 (0)