@@ -75,12 +75,14 @@ class ProgramRegistryDiscoveryError(Exception):
7575 pass
7676
7777
78- class ProgramBinary (BaseModel ):
79- """Platform -specific binary information."""
78+ class ProgramDistribution (BaseModel ):
79+ """Distribution -specific information."""
8080
81+ name : str = Field (
82+ ..., description = "Distribution name (e.g., linux, mac, macarm, win64, win64ext)"
83+ )
8184 asset : str = Field (..., description = "Release asset filename" )
8285 hash : str | None = Field (None , description = "SHA256 hash" )
83- exe : str = Field (..., description = "Executable path within archive" )
8486
8587 model_config = {"arbitrary_types_allowed" : True }
8688
@@ -92,8 +94,9 @@ class ProgramMetadata(BaseModel):
9294 description : str | None = Field (None , description = "Program description" )
9395 repo : str = Field (..., description = "Source repository (owner/name)" )
9496 license : str | None = Field (None , description = "License identifier" )
95- binaries : dict [str , ProgramBinary ] = Field (
96- default_factory = dict , description = "Platform-specific binaries (linux/mac/win64)"
97+ exe : str = Field (..., description = "Executable path within archive (e.g., bin/mf6)" )
98+ dists : list [ProgramDistribution ] = Field (
99+ default_factory = list , description = "Available distributions"
97100 )
98101
99102 model_config = {"arbitrary_types_allowed" : True }
@@ -1239,15 +1242,19 @@ def install(
12391242 if verbose :
12401243 print (f"Detected platform: { platform } " )
12411244
1242- # 4. Get binary metadata
1243- if platform not in program_meta .binaries :
1244- available = ", " .join (program_meta .binaries .keys ())
1245+ # 4. Get distribution metadata
1246+ dist_meta = None
1247+ for dist in program_meta .dists :
1248+ if dist .name == platform :
1249+ dist_meta = dist
1250+ break
1251+
1252+ if dist_meta is None :
1253+ available = ", " .join (d .name for d in program_meta .dists )
12451254 raise ProgramInstallationError (
1246- f"Binary not available for platform '{ platform } '. Available platforms : { available } "
1255+ f"Distribution not available for platform '{ platform } '. Available: { available } "
12471256 )
12481257
1249- binary_meta = program_meta .binaries [platform ]
1250-
12511258 # 5. Determine bindir
12521259 if bindir is None :
12531260 bindir_options = get_bindir_options (program )
@@ -1270,28 +1277,35 @@ def install(
12701277 if verbose :
12711278 print (f"{ program } { version } is already installed in { bindir } " )
12721279 # Return paths to existing executables
1273- exe_name = Path (binary_meta .exe ).name
1280+ exe_name = Path (program_meta .exe ).name
1281+ # Add .exe extension on Windows
1282+ if platform .startswith ("win" ) and not exe_name .endswith (".exe" ):
1283+ exe_name += ".exe"
12741284 return [bindir / exe_name ]
12751285
12761286 # 7. Download archive (if not cached)
1277- asset_url = f"https://github.com/{ program_meta .repo } /releases/download/{ found_ref } /{ binary_meta .asset } "
1287+ asset_url = f"https://github.com/{ program_meta .repo } /releases/download/{ found_ref } /{ dist_meta .asset } "
12781288 archive_dir = self .cache .get_archive_dir (program , version , platform )
1279- archive_path = archive_dir / binary_meta .asset
1289+ archive_path = archive_dir / dist_meta .asset
12801290
12811291 if verbose :
12821292 print (f"Downloading archive from { asset_url } ..." )
12831293
12841294 download_archive (
12851295 url = asset_url ,
12861296 dest = archive_path ,
1287- expected_hash = binary_meta .hash ,
1297+ expected_hash = dist_meta .hash ,
12881298 force = force ,
12891299 verbose = verbose ,
12901300 )
12911301
12921302 # 8. Extract to binaries cache (if not already extracted)
12931303 binary_dir = self .cache .get_binary_dir (program , version , platform )
1294- exe_in_cache = binary_dir / binary_meta .exe
1304+ exe_path = program_meta .exe
1305+ # Add .exe extension on Windows for extraction path
1306+ if platform .startswith ("win" ) and not exe_path .endswith (".exe" ):
1307+ exe_path += ".exe"
1308+ exe_in_cache = binary_dir / exe_path
12951309
12961310 if not exe_in_cache .exists () or force :
12971311 if verbose :
@@ -1300,12 +1314,15 @@ def install(
13001314 extract_executables (
13011315 archive = archive_path ,
13021316 dest_dir = binary_dir ,
1303- exe_path = binary_meta . exe ,
1317+ exe_path = exe_path ,
13041318 verbose = verbose ,
13051319 )
13061320
13071321 # 9. Copy executables to bindir
1308- exe_name = Path (binary_meta .exe ).name
1322+ exe_name = Path (program_meta .exe ).name
1323+ # Add .exe extension on Windows
1324+ if platform .startswith ("win" ) and not exe_name .endswith (".exe" ):
1325+ exe_name += ".exe"
13091326 dest_exe = bindir / exe_name
13101327
13111328 if verbose :
@@ -1326,7 +1343,7 @@ def install(
13261343 "repo" : program_meta .repo ,
13271344 "tag" : found_ref ,
13281345 "asset_url" : asset_url ,
1329- "hash" : binary_meta .hash or "" ,
1346+ "hash" : dist_meta .hash or "" ,
13301347 }
13311348 installation = ProgramInstallation (
13321349 version = version ,
@@ -1842,7 +1859,11 @@ def list_installed(program: str | None = None) -> dict[str, list[ProgramInstalla
18421859
18431860
18441861def _try_best_effort_sync ():
1845- """Attempt to sync registries on first import (best-effort, fails silently)."""
1862+ """
1863+ Attempt to sync registries (best-effort, fails silently).
1864+
1865+ Called by consumer commands before accessing program registries.
1866+ """
18461867 global _SYNC_ATTEMPTED
18471868
18481869 if _SYNC_ATTEMPTED :
@@ -1861,10 +1882,6 @@ def _try_best_effort_sync():
18611882_SYNC_ATTEMPTED = False
18621883"""Track whether auto-sync has been attempted"""
18631884
1864- # Attempt best-effort sync on import (unless disabled)
1865- if not os .environ .get ("MODFLOW_DEVTOOLS_NO_AUTO_SYNC" ):
1866- _try_best_effort_sync ()
1867-
18681885
18691886# ============================================================================
18701887# Public API
@@ -1875,8 +1892,8 @@ def _try_best_effort_sync():
18751892 "_DEFAULT_MANAGER" ,
18761893 "DiscoveredProgramRegistry" ,
18771894 "InstallationMetadata" ,
1878- "ProgramBinary" ,
18791895 "ProgramCache" ,
1896+ "ProgramDistribution" ,
18801897 "ProgramInstallation" ,
18811898 "ProgramInstallationError" ,
18821899 "ProgramManager" ,
@@ -1885,6 +1902,7 @@ def _try_best_effort_sync():
18851902 "ProgramRegistryDiscoveryError" ,
18861903 "ProgramSourceConfig" ,
18871904 "ProgramSourceRepo" ,
1905+ "_try_best_effort_sync" ,
18881906 "download_archive" ,
18891907 "extract_executables" ,
18901908 "get_bindir_options" ,
0 commit comments