88import tempfile
99import sys
1010import argparse
11+ import subprocess
12+ import platform
13+ import re
14+ import stat
15+ from datetime import datetime , timedelta
1116from pathlib import Path
1217import tomlkit
1318from requests .exceptions import RequestException
@@ -133,6 +138,21 @@ def calculate_sha256(data):
133138 return hashlib .sha256 (data ).hexdigest ()
134139
135140
141+ def get_host_platform ():
142+ """Detects the current host platform in the format used by Hermes."""
143+ system = platform .system ().lower ()
144+ machine = platform .machine ().lower ()
145+
146+ if system == "linux" and machine == "x86_64" :
147+ return "linux-x86_64"
148+ elif system == "darwin" :
149+ if machine in ["arm64" , "aarch64" ]:
150+ return "macos-aarch64"
151+ elif machine == "x86_64" :
152+ return "macos-x86_64"
153+ return None
154+
155+
136156def get_asset_checksums (release , repo_name ):
137157 """Downloads all platform archives and calculates required checksums.
138158
@@ -143,8 +163,17 @@ def get_asset_checksums(release, repo_name):
143163
144164 The latter allows Hermes to detect and repair corruption of individual
145165 binaries in a local toolchain installation.
166+
167+ Also extracts the `charon` binary for the host platform to query the
168+ Rust toolchain version it was built against.
146169 """
147170 checksums = {}
171+ host_platform = get_host_platform ()
172+ if host_platform is None :
173+ print (f"ERROR: Unsupported host platform: { platform .system ()} { platform .machine ()} " )
174+ sys .exit (1 )
175+
176+ charon_rust_toolchain = None
148177
149178 with tempfile .TemporaryDirectory () as tmp_dir :
150179 tmp_path = Path (tmp_dir )
@@ -195,6 +224,49 @@ def get_asset_checksums(release, repo_name):
195224 )
196225 found_binaries .add (name )
197226
227+ # If this is charon on the host platform, extract it to disk to query its rustc version.
228+ if platform == host_platform and name in ["charon" , "charon-driver" ]:
229+ bin_path = tmp_path / name
230+ bin_path .write_bytes (binary_data )
231+ bin_path .chmod (bin_path .stat ().st_mode | stat .S_IEXEC )
232+
233+ # Now that all binaries are extracted for the host platform, run charon.
234+ if platform == host_platform :
235+ charon_bin_path = tmp_path / "charon"
236+ if charon_bin_path .exists ():
237+ try :
238+ print (" Querying charon rustc version..." )
239+ # charon internally calls charon-driver, so they must both be extracted.
240+ # Add the temp directory to PATH so charon can find charon-driver
241+ env = os .environ .copy ()
242+ env ["PATH" ] = f"{ tmp_path } :{ env .get ('PATH' , '' )} "
243+ result = subprocess .run (
244+ [str (charon_bin_path ), "rustc" , "--" , "--version" ],
245+ capture_output = True ,
246+ text = True ,
247+ env = env
248+ )
249+ if result .returncode != 0 :
250+ # It might fail because rustc is not actually installed correctly,
251+ # or because of some missing library, but charon might still print
252+ # its version to stderr or stdout before failing.
253+ pass
254+ version_output = result .stdout .strip () or result .stderr .strip ()
255+ match = re .search (r'rustc .* \(.* (\d{4}-\d{2}-\d{2})\)' , version_output )
256+ if not match :
257+ raise CompatibilityError (
258+ f"Could not parse rustc version from charon output: '{ version_output } '"
259+ )
260+ date_str = match .group (1 )
261+ date_obj = datetime .strptime (date_str , "%Y-%m-%d" )
262+ toolchain_date = date_obj + timedelta (days = 1 )
263+ charon_rust_toolchain = f"nightly-{ toolchain_date .strftime ('%Y-%m-%d' )} "
264+ print (f" Found Charon Rust toolchain: { charon_rust_toolchain } " )
265+ except subprocess .CalledProcessError as e :
266+ raise CompatibilityError (f"Failed to execute charon to get rustc version: { e } " )
267+ except FileNotFoundError as e :
268+ raise CompatibilityError (f"Failed to execute charon to get rustc version: { e } " )
269+
198270 # Verify that we found the expected binaries for this repo.
199271 expected = ["aeneas" , "charon" , "charon-driver" ]
200272 missing = [b for b in expected if b not in found_binaries ]
@@ -214,7 +286,12 @@ def get_asset_checksums(release, repo_name):
214286 except tarfile .TarError as e :
215287 raise CompatibilityError (f"Failed to extract archive { asset_name } : { e } " )
216288
217- return checksums
289+ if charon_rust_toolchain is None :
290+ raise CompatibilityError (
291+ f"Could not find charon binary for host platform { host_platform } in releases."
292+ )
293+
294+ return checksums , charon_rust_toolchain
218295
219296
220297def update_cargo_toml (aeneas_meta , charon_meta , dry_run = False ):
@@ -233,6 +310,7 @@ def update_cargo_toml(aeneas_meta, charon_meta, dry_run=False):
233310 print (f" Aeneas Tag: { aeneas_meta ['tag' ]} " )
234311 print (f" Lean Toolchain: { aeneas_meta ['lean_toolchain' ]} " )
235312 print (f" Charon Version: { charon_meta ['version' ]} " )
313+ print (f" Charon Rust Toolchain: { charon_meta ['rust_toolchain' ]} " )
236314 return
237315
238316 print (f"Updating { CARGO_TOML_PATH } ..." )
@@ -251,6 +329,7 @@ def update_cargo_toml(aeneas_meta, charon_meta, dry_run=False):
251329 metadata ["build_rs" ]["aeneas_rev" ] = aeneas_meta ["commit" ]
252330 metadata ["build_rs" ]["lean_toolchain" ] = aeneas_meta ["lean_toolchain" ]
253331 metadata ["build_rs" ]["charon_version" ] = charon_meta ["version" ]
332+ metadata ["build_rs" ]["charon_rust_toolchain" ] = charon_meta ["rust_toolchain" ]
254333
255334 # Update the hermes.dependencies section, which defines the download URLs
256335 # and checksums used by the 'cargo hermes setup' command.
@@ -296,6 +375,20 @@ def main():
296375 if args .aeneas_tag and args .charon_tag :
297376 print (f"Using forced tags: Aeneas={ args .aeneas_tag } , Charon={ args .charon_tag } " )
298377 target_aeneas_release = get_release_by_tag (AENEAS_REPO , args .aeneas_tag )
378+ a_commit = get_commit_from_tag (args .aeneas_tag )
379+ lean_toolchain = fetch_file_content (AENEAS_REPO , a_commit , "backends/lean/lean-toolchain" )
380+ if lean_toolchain :
381+ lean_toolchain = lean_toolchain .strip ()
382+ print (f" Fetching checksums from Aeneas artifact..." )
383+ a_checksums , charon_rust_toolchain = get_asset_checksums (target_aeneas_release , AENEAS_REPO )
384+ target_aeneas_meta = {
385+ "tag" : args .aeneas_tag ,
386+ "commit" : a_commit ,
387+ "lean_toolchain" : lean_toolchain ,
388+ "checksums" : a_checksums ,
389+ "version" : args .charon_tag ,
390+ "charon_rust_toolchain" : charon_rust_toolchain ,
391+ }
299392 else :
300393 print ("Fetching releases from GitHub..." )
301394 aeneas_releases = get_automated_releases (AENEAS_REPO )
@@ -340,7 +433,7 @@ def main():
340433
341434 print (f" Fetching checksums from Aeneas artifact..." )
342435 try :
343- a_checksums = get_asset_checksums (a_rel , AENEAS_REPO )
436+ a_checksums , charon_rust_toolchain = get_asset_checksums (a_rel , AENEAS_REPO )
344437 except CompatibilityError as e :
345438 print (f" Skipping { a_tag } : { e } " )
346439 continue
@@ -352,6 +445,7 @@ def main():
352445 "lean_toolchain" : lean_toolchain ,
353446 "checksums" : a_checksums ,
354447 "version" : charon_version ,
448+ "charon_rust_toolchain" : charon_rust_toolchain ,
355449 }
356450 break
357451
@@ -362,8 +456,12 @@ def main():
362456 print (f"\n Targeting:" )
363457 print (f" Aeneas Tag: { target_aeneas_meta ['tag' ]} " )
364458 print (f" Charon Version: { target_aeneas_meta ['version' ]} " )
459+ print (f" Charon Rust Toolchain: { target_aeneas_meta ['charon_rust_toolchain' ]} " )
365460
366- charon_meta = {"version" : target_aeneas_meta ["version" ]}
461+ charon_meta = {
462+ "version" : target_aeneas_meta ["version" ],
463+ "rust_toolchain" : target_aeneas_meta ["charon_rust_toolchain" ],
464+ }
367465 update_cargo_toml (target_aeneas_meta , charon_meta , dry_run = args .dry_run )
368466 if not args .dry_run :
369467 print ("\n Successfully rolled toolchain updates!" )
0 commit comments