1010from builder .actions .script import Script
1111
1212import os
13+ import re
1314import stat
1415import tempfile
16+ import urllib .request
1517
1618# this is a copy of https://apt.llvm.org/llvm.sh modified to add support back in
1719# for older versions of clang < 8, and removed the need for clangd, lldb
101103"""
102104
103105
106+ def _get_codename ():
107+ """
108+ Get the distribution codename (e.g., 'noble', 'bookworm').
109+ Works with Ubuntu and Debian.
110+ We should probably only care about Ubuntu but may as well support both and older codename detection is possible.
111+ Returns None if codename cannot be determined.
112+ """
113+ import subprocess
114+
115+ # Try lsb_release (works on Ubuntu, Debian)
116+ try :
117+ result = subprocess .run (['lsb_release' , '-cs' ], capture_output = True , text = True )
118+ if result .returncode == 0 and result .stdout .strip ():
119+ return result .stdout .strip ()
120+ except (FileNotFoundError , Exception ):
121+ pass
122+
123+ # Try /etc/os-release
124+ try :
125+ with open ('/etc/os-release' , 'r' ) as f :
126+ os_release = {}
127+ for line in f :
128+ if '=' in line :
129+ key , value = line .strip ().split ('=' , 1 )
130+ os_release [key ] = value .strip ('"' )
131+
132+ # VERSION_CODENAME is the standard field
133+ if 'VERSION_CODENAME' in os_release and os_release ['VERSION_CODENAME' ]:
134+ return os_release ['VERSION_CODENAME' ]
135+
136+ # For Ubuntu, we can also check UBUNTU_CODENAME
137+ if 'UBUNTU_CODENAME' in os_release and os_release ['UBUNTU_CODENAME' ]:
138+ return os_release ['UBUNTU_CODENAME' ]
139+ except (FileNotFoundError , Exception ):
140+ pass
141+
142+ # Try /etc/lsb-release (older Ubuntu systems)
143+ try :
144+ with open ('/etc/lsb-release' , 'r' ) as f :
145+ for line in f :
146+ if line .startswith ('DISTRIB_CODENAME=' ):
147+ codename = line .strip ().split ('=' , 1 )[1 ].strip ('"' )
148+ if codename :
149+ return codename
150+ except (FileNotFoundError , Exception ):
151+ pass
152+
153+ return None
154+
155+
156+ def _fetch_url_content (url ):
157+ """
158+ Fetch content from a URL.
159+ We use curl as a subprocess for compression handling.
160+ Falls back to urllib if curl is not available.
161+ """
162+ import subprocess
163+
164+ # Use curl with --compressed flag (handles all compression types)
165+ try :
166+ result = subprocess .run (
167+ ['curl' , '-s' , '--compressed' , url ],
168+ capture_output = True ,
169+ text = True ,
170+ timeout = 60
171+ )
172+ if result .returncode == 0 and result .stdout :
173+ return result .stdout
174+ except (FileNotFoundError , subprocess .TimeoutExpired , Exception ) as e :
175+ print ('curl not available or failed, falling back to urllib: {}' .format (e ))
176+
177+ # Fall back to urllib with gzip decompression.
178+ # apt.llvm.org uses gzip encoding for the main page, no compression for llvm.sh
179+ import gzip
180+ try :
181+ req = urllib .request .Request (url , headers = {
182+ 'Accept-Encoding' : 'gzip, identity' ,
183+ 'User-Agent' : 'aws-crt-builder'
184+ })
185+ with urllib .request .urlopen (req , timeout = 30 ) as response :
186+ data = response .read ()
187+ encoding = response .info ().get ('Content-Encoding' , '' )
188+
189+ if encoding == 'gzip' :
190+ return gzip .decompress (data ).decode ('utf-8' )
191+ else :
192+ # No compression
193+ return data .decode ('utf-8' )
194+
195+ except Exception as e :
196+ print ('urllib fetch failed: {}' .format (e ))
197+
198+ return None
199+
200+
201+ def get_latest_llvm_version ():
202+ """
203+ Detect the latest available LLVM/Clang version from apt.llvm.org.
204+ Prefers the qualification/RC branch (stable + 1), falls back to stable if not available.
205+
206+ Supported distributions:
207+ - Ubuntu: bionic (18.04), focal (20.04), jammy (22.04), noble (24.04), plucky (25.04), questing (25.10)
208+ - Debian: bullseye (11), bookworm (12), trixie (13)
209+
210+ Returns the version number as a string, or None if detection fails.
211+ """
212+ try :
213+ # Download the llvm.sh script to get the stable version
214+ llvm_sh_content = _fetch_url_content ('https://apt.llvm.org/llvm.sh' )
215+ if not llvm_sh_content :
216+ print ('Warning: Could not download llvm.sh' )
217+ return None
218+
219+ # Extract CURRENT_LLVM_STABLE from the script
220+ stable_match = re .search (r'CURRENT_LLVM_STABLE=(\d+)' , llvm_sh_content )
221+ if not stable_match :
222+ print ('Warning: Could not parse CURRENT_LLVM_STABLE from llvm.sh' )
223+ return None
224+
225+ stable_version = int (stable_match .group (1 ))
226+ qualification_version = stable_version + 1
227+
228+ # Get the codename (works for Ubuntu and Debian)
229+ codename = _get_codename ()
230+
231+ if codename :
232+ print ('Detected distribution codename: {}' .format (codename ))
233+
234+ # Parse the LLVM apt page to find available versions for this codename
235+ apt_page = _fetch_url_content ('https://apt.llvm.org/' )
236+ if apt_page :
237+ # Look for llvm-toolchain-<codename>-<version> patterns
238+ pattern = r'llvm-toolchain-{}-(\d+)' .format (re .escape (codename ))
239+ available_versions = set (re .findall (pattern , apt_page ))
240+
241+ if available_versions :
242+ print ('Available LLVM versions for {}: {}' .format (
243+ codename , ', ' .join (sorted (available_versions , key = int ))))
244+
245+ if str (qualification_version ) in available_versions :
246+ print ('Latest LLVM: Using qualification/RC branch: clang-{}' .format (qualification_version ))
247+ return str (qualification_version )
248+ elif str (stable_version ) in available_versions :
249+ print ('Latest LLVM: Qualification branch {} not available for {}, using stable: clang-{}' .format (
250+ qualification_version , codename , stable_version ))
251+ return str (stable_version )
252+ else :
253+ # Use the highest available version for this codename
254+ highest = max (available_versions , key = int )
255+ print ('Latest LLVM: Neither qualification ({}) nor stable ({}) available for {}, using highest available: clang-{}' .format (
256+ qualification_version , stable_version , codename , highest ))
257+ return highest
258+ else :
259+ print ('Warning: No LLVM versions found for codename {} on apt.llvm.org' .format (codename ))
260+ print ('This distribution may not be supported by apt.llvm.org' )
261+ else :
262+ print ('Warning: Could not fetch apt.llvm.org page' )
263+ else :
264+ print ('Warning: Could not determine distribution codename' )
265+
266+ # Fall back to stable version if we couldn't check availability
267+ print ('Latest LLVM: Falling back to stable version: clang-{}' .format (stable_version ))
268+ return str (stable_version )
269+
270+ except Exception as e :
271+ print ('Warning: Could not detect latest LLVM version: {}' .format (e ))
272+ return None
273+
274+
104275class LLVM (Import ):
276+ # Cache for the resolved latest version
277+ _latest_version_cache = None
278+
105279 def __init__ (self , ** kwargs ):
106280 super ().__init__ (
107281 compiler = True ,
@@ -114,6 +288,15 @@ def __init__(self, **kwargs):
114288 def resolved (self ):
115289 return True
116290
291+ @staticmethod
292+ def resolve_latest_version ():
293+ """
294+ Resolve 'latest' to an actual version number. We cache the result to avoid repeating this step.
295+ """
296+ if LLVM ._latest_version_cache is None :
297+ LLVM ._latest_version_cache = get_latest_llvm_version ()
298+ return LLVM ._latest_version_cache
299+
117300 def install (self , env ):
118301 if self .installed :
119302 return
@@ -126,8 +309,18 @@ def install(self, env):
126309 Script ([InstallPackages (packages )],
127310 name = 'Install compiler prereqs' ).run (env )
128311
312+ # Handle 'latest' version
313+ version = env .toolchain .compiler_version
314+ if version == 'latest' :
315+ version = LLVM .resolve_latest_version ()
316+ if version is None :
317+ raise Exception ("Could not determine latest LLVM version" )
318+ print ('Resolved clang-latest to clang-{}' .format (version ))
319+ env .toolchain .compiler_version = version
320+ env .spec .compiler_version = version
321+
129322 installed_path , installed_version = Toolchain .find_compiler (
130- env .spec .compiler , env . spec . compiler_version )
323+ env .spec .compiler , version )
131324 if installed_path :
132325 print ('Compiler {} {} already exists at {}' .format (
133326 env .spec .compiler , installed_version , installed_path ))
@@ -138,7 +331,7 @@ def install(self, env):
138331 sudo = ['sudo' ] if sudo else []
139332
140333 # Strip minor version info
141- version = env . toolchain . compiler_version .replace (r'\..+' , '' )
334+ version = version .replace (r'\..+' , '' )
142335
143336 script = tempfile .NamedTemporaryFile (delete = False )
144337 script_path = script .name
0 commit comments