Skip to content

Commit 9b2ebe9

Browse files
committed
handle latest-clang to get latest qualification version to build with
1 parent f3881da commit 9b2ebe9

3 files changed

Lines changed: 223 additions & 6 deletions

File tree

builder/core/data.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,7 @@ class PKG_TOOLS(Enum):
513513

514514
'versions': {
515515
'default': {},
516+
'latest': {}, # Latest stable/qualification version
516517
'3': {
517518
'c': "clang-3.9",
518519
'cxx': "clang++-3.9",

builder/core/toolchain.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ def _compiler_version(cc):
3636

3737

3838
def _find_compiler_tool(name, versions):
39+
# Filter out 'latest' from versions list. It will be handled separately.
40+
versions = [v for v in versions if v != 'latest']
41+
3942
# look for the default tool, and see if the version is in the search set
4043
path = util.where(name, resolve_symlinks=False)
4144
if path:
@@ -180,8 +183,20 @@ def find_gcc_tool(name, version=None):
180183
@staticmethod
181184
def find_llvm_tool(name, version=None):
182185
""" Finds clang, clang-tidy, lld, etc at a specific version, or the
183-
latest one available """
184-
versions = [version] if version else _clang_versions()
186+
latest one available. If version is 'latest', resolves it dynamically. """
187+
if version == 'latest':
188+
# Import here to avoid circular dependency
189+
from builder.imports.llvm import LLVM
190+
resolved_version = LLVM.resolve_latest_version()
191+
if resolved_version:
192+
versions = [resolved_version]
193+
else:
194+
# Fall back to searching all known versions
195+
versions = _clang_versions()
196+
elif version:
197+
versions = [version]
198+
else:
199+
versions = _clang_versions()
185200
return _find_compiler_tool(name, versions)
186201

187202
@staticmethod
@@ -228,7 +243,8 @@ def _find_msvc(version, install_vswhere=True):
228243

229244
@staticmethod
230245
def find_compiler(compiler, version=None):
231-
""" Returns path, found_version for the requested compiler if it is installed """
246+
""" Returns path, found_version for the requested compiler if it is installed.
247+
If version is 'latest' for clang, it will be resolved dynamically. """
232248
if compiler == 'clang':
233249
if current_os() == "macos":
234250
return Toolchain.find_apple_llvm_compiler(compiler, version)
@@ -329,7 +345,14 @@ def _find_compiler():
329345

330346
@staticmethod
331347
def is_compiler_installed(compiler, version):
332-
""" Returns True if the specified compiler is already installed, False otherwise """
348+
""" Returns True if the specified compiler is already installed, False otherwise.
349+
For 'latest' version of 'clang' compiler, this will resolve the actual version first. """
350+
if version == 'latest' and compiler == 'clang':
351+
# Import here to avoid circular dependency
352+
from builder.imports.llvm import LLVM
353+
resolved_version = LLVM.resolve_latest_version()
354+
if resolved_version:
355+
version = resolved_version
333356
compiler_path, found_version = Toolchain.find_compiler(
334357
compiler, version)
335358
return compiler_path != None

builder/imports/llvm.py

Lines changed: 195 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
from builder.actions.script import Script
1111

1212
import os
13+
import re
1314
import stat
1415
import 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
@@ -101,7 +103,179 @@
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+
104275
class 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

Comments
 (0)