2020#*
2121#****************************************************************************
2222import os
23+ import fnmatch
2324import httpx
2425import json
2526import re
@@ -89,20 +90,45 @@ def update(self, update_info):
8990 else :
9091 return self ._update_normal (update_info , pkg_dir , file_url , forced_ext )
9192
93+ def _repo_base_url (self ):
94+ """Return the base GitHub API URL for this package's repo."""
95+ github_com_idx = self .url .find ("github.com" )
96+ return "https://api.github.com/repos/" + self .url [github_com_idx + len ("github.com" ) + 1 :]
97+
98+ def _fetch_tags (self ):
99+ """Fetch tags from GitHub API and normalize them to release-like dicts."""
100+ tags_url = self ._repo_base_url () + "/tags"
101+ resp = httpx .get (tags_url , follow_redirects = True )
102+ if resp .status_code != 200 :
103+ return []
104+ tags = json .loads (resp .content )
105+ # Normalize tags to look like release dicts so existing version-selection
106+ # logic can be reused. Tags only provide source archives, no binary assets.
107+ normalized = []
108+ for t in tags :
109+ normalized .append ({
110+ "tag_name" : t ["name" ],
111+ "prerelease" : False ,
112+ "assets" : [],
113+ "tarball_url" : t .get ("tarball_url" ),
114+ "zipball_url" : t .get ("zipball_url" ),
115+ "_is_tag" : True ,
116+ })
117+ return normalized
118+
92119 def _resolve_release (self ):
93120 """Query GitHub API and resolve the release and asset to download.
94121
95122 Returns:
96123 Tuple of (rls_info, rls, file_url, forced_ext)
97124 """
98- github_com_idx = self .url .find ("github.com" )
99- url = "https://api.github.com/repos/" + self .url [github_com_idx + len ("github.com" ) + 1 :] + "/releases"
100- rls_info = httpx .get (url , follow_redirects = True )
125+ releases_url = self ._repo_base_url () + "/releases"
126+ rls_info_resp = httpx .get (releases_url , follow_redirects = True )
101127
102- if rls_info .status_code != 200 :
103- raise Exception ("Failed to fetch release info: %d" % rls_info .status_code )
128+ if rls_info_resp .status_code != 200 :
129+ raise Exception ("Failed to fetch release info: %d" % rls_info_resp .status_code )
104130
105- rls_info = json .loads (rls_info .content )
131+ rls_info = json .loads (rls_info_resp .content )
106132
107133 # Select release per version specification
108134 rls = None
@@ -113,19 +139,40 @@ def _resolve_release(self):
113139 rls = r
114140 break
115141 if rls is None :
116- raise Exception ("Failed to find latest release (prerelease=%s)" % self .prerelease )
142+ # No formal releases — fall back to most recent tag
143+ tags = self ._fetch_tags ()
144+ if tags :
145+ rls = tags [0 ]
146+ rls_info = tags
147+ else :
148+ raise Exception ("Failed to find latest release (prerelease=%s)" % self .prerelease )
117149 else :
118150 rls = self ._select_release_by_version (rls_info )
119151 if rls is None :
120- raise Exception (f"No release matches version spec '{ self .version } ' (prerelease={ self .prerelease } )" )
152+ # Not found in releases — try tags
153+ tags = self ._fetch_tags ()
154+ rls = self ._select_release_by_version (tags )
155+ if rls is None :
156+ raise Exception (f"No release or tag matches version spec '{ self .version } '" )
157+ rls_info = tags
121158
122159 # Determine file to download
123160 file_url = None
124161 forced_ext = None
125162
126163 assets = rls .get ("assets" , [])
127164 if self .file is not None :
128- raise NotImplementedError ("File specification not yet supported" )
165+ # Filter assets to those whose name starts with the basename or matches it as a pattern
166+ filtered = [
167+ a for a in assets
168+ if (lambda nm : nm .startswith (self .file ) or fnmatch .fnmatch (nm , self .file ))(
169+ a .get ("name" ) or os .path .basename (a .get ("browser_download_url" , "" ))
170+ )
171+ ]
172+ if not filtered :
173+ names = [a .get ("name" ) or os .path .basename (a .get ("browser_download_url" , "" )) for a in assets ]
174+ raise Exception (f"No assets matching basename '{ self .file } ' found in release. Available assets: { ', ' .join (names )} " )
175+ assets = filtered
129176
130177 # If source=true, skip binary detection and go straight to source
131178 has_binaries = self ._has_binary_assets (assets ) if not self .source else False
0 commit comments