3838# /releases/latest/download/<asset> auto-redirects to the newest non-prerelease
3939# asset, so this URL doesn't need to be bumped per release.
4040GMTOOLKIT_RELEASE_URL = "https://github.com/JeodC/gmtoolkit/releases/latest/download"
41+ GMTOOLKIT_API_URL = "https://api.github.com/repos/JeodC/gmtoolkit/releases/latest"
4142
4243# ----------------------------------------------------------------------
4344# GitHub request
@@ -246,11 +247,28 @@ def _ensure_gmtoolkit_binary(self, port: Port) -> None:
246247
247248 bin_name = f"gmtoolkit.{ DEVICE_ARCH } "
248249 dest = Path (controlfolder ) / bin_name
249- if dest .exists ():
250- return
251-
252250 zip_name = f"gmtoolkit-{ DEVICE_ARCH } .zip"
253- url = f"{ GMTOOLKIT_RELEASE_URL } /{ zip_name } "
251+
252+ remote = self ._gmtoolkit_remote_asset (zip_name )
253+ local_meta = self ._load_gmtoolkit_meta ()
254+
255+ if dest .exists ():
256+ if not remote :
257+ # Can't reach the release API – keep whatever is on disk rather
258+ # than break an offline / rate-limited install.
259+ print ("[GMTOOLKIT] Present, version check skipped (release query failed)" )
260+ return
261+ if (local_meta .get ("id" ) == remote ["id" ]
262+ and local_meta .get ("size" ) == remote ["size" ]):
263+ print ("[GMTOOLKIT] Present and up to date" )
264+ return
265+ # No recorded version → treat as stale; otherwise a newer build exists.
266+ reason = "no version recorded" if not local_meta else "newer build available"
267+ print (f"[GMTOOLKIT] Updating ({ reason } )" )
268+
269+ # Prefer the API-provided asset URL; fall back to the latest/download
270+ # redirect when the API was unreachable but the binary is missing.
271+ url = remote ["url" ] if remote and remote .get ("url" ) else f"{ GMTOOLKIT_RELEASE_URL } /{ zip_name } "
254272 print (f"[GMTOOLKIT] Downloading { zip_name } " )
255273 self .progress_q .put ((0 , 1 , f"gmtoolkit: { zip_name } " , "download" ))
256274 zip_tmp = Path (controlfolder ) / f"{ zip_name } .tmp"
@@ -283,6 +301,9 @@ def _ensure_gmtoolkit_binary(self, port: Port) -> None:
283301
284302 with suppress (OSError ):
285303 os .chmod (dest , 0o755 )
304+ # Record the version we just installed so future runs detect staleness.
305+ if remote :
306+ self ._save_gmtoolkit_meta ({"id" : remote ["id" ], "size" : remote ["size" ]})
286307 self .progress_q .put ((1 , 1 , "gmtoolkit installed" , "download" ))
287308 print (f"[GMTOOLKIT] Installed { dest } " )
288309 except Exception as e :
@@ -291,6 +312,50 @@ def _ensure_gmtoolkit_binary(self, port: Port) -> None:
291312 with suppress (OSError ):
292313 zip_tmp .unlink ()
293314
315+ def _gmtoolkit_remote_asset (self , zip_name : str ) -> dict | None :
316+ """Return {id, size, url} for the latest gmtoolkit asset, or None on failure."""
317+ try :
318+ data , _ = _gh_request (GMTOOLKIT_API_URL )
319+ release = json .loads (data )
320+ except Exception as e :
321+ print (f"[GMTOOLKIT] Release query failed: { type (e ).__name__ } : { e } " )
322+ return None
323+ asset = next ((a for a in release .get ("assets" , []) if a ["name" ] == zip_name ), None )
324+ if not asset :
325+ print (f"[GMTOOLKIT] Asset { zip_name } not found in latest release" )
326+ return None
327+ return {
328+ "id" : asset .get ("id" ),
329+ "size" : asset .get ("size" , 0 ),
330+ "url" : asset .get ("browser_download_url" ),
331+ }
332+
333+ def _load_gmtoolkit_meta (self ) -> dict :
334+ if MANIFEST_PATH .exists ():
335+ with suppress (Exception ):
336+ with open (MANIFEST_PATH , "r" , encoding = "utf-8" ) as f :
337+ return json .load (f ).get ("gmtoolkit" , {})
338+ return {}
339+
340+ def _save_gmtoolkit_meta (self , meta : dict ) -> None :
341+ data = {}
342+ if MANIFEST_PATH .exists ():
343+ with suppress (Exception ):
344+ with open (MANIFEST_PATH , "r" , encoding = "utf-8" ) as f :
345+ data = json .load (f )
346+ data ["gmtoolkit" ] = meta
347+ tmp = MANIFEST_PATH .with_suffix (".tmp" )
348+ try :
349+ with open (tmp , "w" , encoding = "utf-8" ) as f :
350+ json .dump (data , f , indent = 2 )
351+ f .flush ()
352+ os .fsync (f .fileno ())
353+ os .replace (tmp , MANIFEST_PATH )
354+ except Exception as e :
355+ print (f"[ERROR] gmtoolkit manifest update failed: { e } " )
356+ with suppress (OSError ):
357+ tmp .unlink ()
358+
294359 def _load_runtime_manifest (self ) -> dict :
295360 if MANIFEST_PATH .exists ():
296361 with suppress (Exception ):
0 commit comments