@@ -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():
103101max_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