66import platform
77import re
88import shutil
9+ import subprocess
10+ import sys
911import tarfile
1012import tempfile
1113import urllib .request
1214import zipfile
1315from typing import Dict , List , Optional , Tuple
1416
17+
1518logger = logging .getLogger (__name__ )
1619logger .addHandler (logging .NullHandler ())
1720
@@ -34,68 +37,81 @@ def is_linux_x86() -> bool:
3437 )
3538
3639
37- import subprocess
40+ #########################
41+ # Cache directory helper
42+ #########################
3843
39- MINIMUM_LIBC_VERSION = 2.29
44+ APP_NAMESPACE = [ "executorch" , "qnn" ]
4045
41- REQUIRED_LIBC_LIBS = [
42- "/lib/x86_64-linux-gnu/libc.so.6" ,
43- "/lib64/libc.so.6" ,
44- "/lib/libc.so.6" ,
45- ]
4646
47+ def _get_staging_dir (* parts : str ) -> pathlib .Path :
48+ r"""
49+ Return a cross-platform staging directory for staging SDKs/libraries.
50+
51+ - On Linux:
52+ ~/.cache/executorch/qnn/<parts...>
53+ (falls back to $HOME/.cache if $XDG_CACHE_HOME is unset)
4754
48- def check_glibc_exist_and_validate () -> bool :
55+ - On Windows (not supported yet, but as placeholder):
56+ %LOCALAPPDATA%\executorch\qnn\<parts...>
57+ (falls back to $HOME/AppData/Local if %LOCALAPPDATA% is unset)
58+
59+ - Override:
60+ If QNN_STAGING_DIR is set in the environment, that path is used instead.
61+
62+ Args:
63+ parts (str): Subdirectories to append under the root staging dir.
64+
65+ Returns:
66+ pathlib.Path: Fully qualified staging path.
4967 """
50- Check if users have glibc installed.
68+ # Environment override wins
69+ base = os .environ .get ("QNN_STAGING_DIR" )
70+ if base :
71+ return pathlib .Path (base ).joinpath (* parts )
72+
73+ system = platform .system ().lower ()
74+ if system == "windows" :
75+ # On Windows, prefer %LOCALAPPDATA%, fallback to ~/AppData/Local
76+ base = pathlib .Path (
77+ os .environ .get ("LOCALAPPDATA" , pathlib .Path .home () / "AppData" / "Local" )
78+ )
79+ elif is_linux_x86 ():
80+ # On Linux/Unix, prefer $XDG_CACHE_HOME, fallback to ~/.cache
81+ base = pathlib .Path (
82+ os .environ .get ("XDG_CACHE_HOME" , pathlib .Path .home () / ".cache" )
83+ )
84+ else :
85+ raise ValueError (f"Unsupported platform: { system } " )
86+
87+ return base .joinpath (* APP_NAMESPACE , * parts )
88+
89+
90+ def _atomic_download (url : str , dest : pathlib .Path ):
5191 """
52- exists = False
53- for path in REQUIRED_LIBC_LIBS :
54- try :
55- output = subprocess .check_output (
56- [path , "--version" ], stderr = subprocess .STDOUT
57- )
58- output = output .decode ().split ("\n " )[0 ]
59- logger .debug (f"[QNN] glibc version for path { path } is: { output } " )
60- match = re .search (r"version (\d+\.\d+)" , output )
61- if match :
62- version = match .group (1 )
63- if float (version ) >= MINIMUM_LIBC_VERSION :
64- logger .debug (f"[QNN] glibc version is { version } ." )
65- exists = True
66- return True
67- else :
68- logger .error (
69- f"[QNN] glibc version is too low. The minimum libc version is { MINIMUM_LIBC_VERSION } Please install glibc following the commands below."
70- )
71- else :
72- logger .error ("[QNN] glibc version not found." )
92+ Download URL into dest atomically:
93+ - Write to a temp file in the same dir
94+ - Move into place if successful
95+ """
96+ dest .parent .mkdir (parents = True , exist_ok = True )
7397
74- except Exception :
75- continue
98+ # Temp file in same dir (guarantees atomic rename)
99+ with tempfile .NamedTemporaryFile (dir = dest .parent , delete = False ) as tmp :
100+ tmp_path = pathlib .Path (tmp .name )
76101
77- if not exists :
78- logger .error (
79- r""""
80- [QNN] glibc not found or the version is too low. Please install glibc following the commands below.
81- Ubuntu/Debian:
82- sudo apt update
83- sudo apt install libc6
84-
85- Fedora/Red Hat:
86- sudo dnf install glibc
87-
88- Arch Linux:
89- sudo pacman -S glibc
90-
91- Also please make sure the glibc version is >= MINIMUM_LIBC_VERSION. You can verify the glibc version by running the following command:
92- Option 1:
93- ldd --version
94- Option 2:
95- /path/to/libc.so.6 --version
96- """
97- )
98- return exists
102+ try :
103+ urllib .request .urlretrieve (url , tmp_path )
104+ tmp_path .replace (dest ) # atomic rename
105+ except Exception :
106+ # Clean up partial file on failure
107+ if tmp_path .exists ():
108+ tmp_path .unlink (missing_ok = True )
109+ raise
110+
111+
112+ ####################
113+ # qnn sdk download management
114+ ####################
99115
100116
101117def _download_archive (url : str , archive_path : pathlib .Path ) -> bool :
@@ -178,9 +194,6 @@ def _download_qnn_sdk(dst_folder=SDK_DIR) -> Optional[pathlib.Path]:
178194 if not is_linux_x86 ():
179195 logger .info ("[QNN] Skipping Qualcomm SDK (only supported on Linux x86)." )
180196 return None
181- elif not check_glibc_exist_and_validate ():
182- logger .info ("[QNN] Skipping Qualcomm SDK (glibc not found or version too old)." )
183- return None
184197 else :
185198 logger .info ("[QNN] Downloading Qualcomm SDK for Linux x86" )
186199
@@ -241,6 +254,136 @@ def _extract_tar(archive_path: pathlib.Path, prefix: str, target_dir: pathlib.Pa
241254 dst .write (src .read ())
242255
243256
257+ ####################
258+ # libc management
259+ ####################
260+
261+ GLIBC_VERSION = "2.34"
262+ GLIBC_REEXEC_GUARD = "QNN_GLIBC_REEXEC"
263+ MINIMUM_LIBC_VERSION = GLIBC_VERSION
264+
265+
266+ def _get_glibc_libdir () -> pathlib .Path :
267+ glibc_root = _get_staging_dir (f"glibc-{ GLIBC_VERSION } " )
268+ return glibc_root / "lib"
269+
270+
271+ def _parse_version (v : str ) -> tuple [int , int ]:
272+ """Turn '2.34' → (2,34) so it can be compared."""
273+ parts = v .split ("." )
274+ return int (parts [0 ]), int (parts [1 ]) if len (parts ) > 1 else 0
275+
276+
277+ def _current_glibc_version () -> str :
278+ """Return system glibc version string (via ctypes)."""
279+ try :
280+ libc = ctypes .CDLL ("libc.so.6" )
281+ func = libc .gnu_get_libc_version
282+ func .restype = ctypes .c_char_p
283+ return func ().decode ()
284+ except Exception as e :
285+ return f"error:{ e } "
286+
287+
288+ def _resolve_glibc_loader () -> pathlib .Path | None :
289+ """Return staged ld.so path if available."""
290+ for p in [
291+ _get_glibc_libdir () / f"ld-{ GLIBC_VERSION } .so" ,
292+ _get_glibc_libdir () / "ld-linux-x86-64.so.2" ,
293+ ]:
294+ if p .exists ():
295+ return p
296+ return None
297+
298+
299+ def _stage_prebuilt_glibc ():
300+ """Download + extract Fedora 35 glibc RPM into /tmp."""
301+ logger .info (">>> Staging prebuilt glibc-%s from Fedora 35 RPM" , GLIBC_VERSION )
302+ _get_glibc_libdir ().mkdir (parents = True , exist_ok = True )
303+ rpm_path = _get_staging_dir ("glibc" ) / "glibc.rpm"
304+ work_dir = _get_staging_dir ("glibc" ) / "extracted"
305+ rpm_url = (
306+ "https://archives.fedoraproject.org/pub/archive/fedora/linux/releases/35/"
307+ "Everything/x86_64/os/Packages/g/glibc-2.34-7.fc35.x86_64.rpm"
308+ )
309+
310+ rpm_path .parent .mkdir (parents = True , exist_ok = True )
311+ logger .info ("[glibc] Downloading %s -> %s" , rpm_url , rpm_path )
312+ try :
313+ urllib .request .urlretrieve (rpm_url , rpm_path )
314+ except Exception as e :
315+ logger .error ("[glibc] Failed to download %s: %s" , rpm_url , e )
316+ raise
317+
318+ # Extract
319+ if work_dir .exists ():
320+ shutil .rmtree (work_dir )
321+ work_dir .mkdir (parents = True )
322+ subprocess .check_call (["bsdtar" , "-C" , str (work_dir ), "-xf" , str (rpm_path )])
323+
324+ # Copy runtime libs
325+ staged = [
326+ "ld-linux-x86-64.so.2" ,
327+ "libc.so.6" ,
328+ "libdl.so.2" ,
329+ "libpthread.so.0" ,
330+ "librt.so.1" ,
331+ "libm.so.6" ,
332+ "libutil.so.1" ,
333+ ]
334+ for lib in staged :
335+ src = work_dir / "lib64" / lib
336+ if src .exists ():
337+ shutil .copy2 (src , _get_glibc_libdir () / lib )
338+ logger .info ("[glibc] Staged %s" , lib )
339+ else :
340+ logger .warning ("[glibc] Missing %s in RPM" , lib )
341+
342+
343+ def ensure_glibc_minimum (min_version : str = GLIBC_VERSION ):
344+ """
345+ Ensure process runs under glibc >= min_version.
346+ - If system glibc is new enough → skip.
347+ - Else → stage Fedora RPM and re-exec under staged loader.
348+ """
349+ current = _current_glibc_version ()
350+ logger .info ("[glibc] Current loaded glibc: %s" , current )
351+
352+ # If system glibc already sufficient → skip everything
353+ m = re .match (r"(\d+\.\d+)" , current )
354+ if m and _parse_version (m .group (1 )) >= _parse_version (min_version ):
355+ logger .info ("[glibc] System glibc >= %s, no staging needed." , min_version )
356+ return
357+
358+ # Avoid infinite loop
359+ if os .environ .get (GLIBC_REEXEC_GUARD ) == "1" :
360+ logger .info ("[glibc] Already re-exec'd once, continuing." )
361+ return
362+
363+ # Stage prebuilt if not already staged
364+ if not (_get_glibc_libdir () / "libc.so.6" ).exists ():
365+ _stage_prebuilt_glibc ()
366+
367+ loader = _resolve_glibc_loader ()
368+ if not loader :
369+ logger .error ("[glibc] Loader not found in %s" , _get_glibc_libdir ())
370+ return
371+
372+ logger .info (
373+ "[glibc] Re-execing under loader %s with libdir %s" , loader , _get_glibc_libdir ()
374+ )
375+ os .environ [GLIBC_REEXEC_GUARD ] = "1"
376+ os .execv (
377+ str (loader ),
378+ [str (loader ), "--library-path" , str (_get_glibc_libdir ()), sys .executable ]
379+ + sys .argv ,
380+ )
381+
382+
383+ ####################
384+ # libc++ management
385+ ####################
386+
244387LLVM_VERSION = "14.0.0"
245388LIBCXX_BASE_NAME = f"clang+llvm-{ LLVM_VERSION } -x86_64-linux-gnu-ubuntu-18.04"
246389LLVM_URL = f"https://github.com/llvm/llvm-project/releases/download/llvmorg-{ LLVM_VERSION } /{ LIBCXX_BASE_NAME } .tar.xz"
@@ -258,12 +401,17 @@ def _stage_libcxx(target_dir: pathlib.Path):
258401 logger .info ("[libcxx] Already staged at %s, skipping download" , target_dir )
259402 return
260403
261- temp_tar = pathlib .Path ("/tmp" ) / f"{ LIBCXX_BASE_NAME } .tar.xz"
262- temp_extract = pathlib .Path ("/tmp" ) / LIBCXX_BASE_NAME
404+ libcxx_stage = _get_staging_dir (f"libcxx-{ LLVM_VERSION } " )
405+ temp_tar = libcxx_stage / f"{ LIBCXX_BASE_NAME } .tar.xz"
406+ temp_extract = libcxx_stage / LIBCXX_BASE_NAME
263407
264408 if not temp_tar .exists ():
265409 logger .info ("[libcxx] Downloading %s" , LLVM_URL )
266- urllib .request .urlretrieve (LLVM_URL , temp_tar )
410+ _atomic_download (LLVM_URL , temp_tar )
411+
412+ # Sanity check before extracting
413+ if not temp_tar .exists () or temp_tar .stat ().st_size == 0 :
414+ raise FileNotFoundError (f"[libcxx] Tarball missing or empty: { temp_tar } " )
267415
268416 logger .info ("[libcxx] Extracting %s" , temp_tar )
269417 with tarfile .open (temp_tar , "r:xz" ) as tar :
@@ -437,8 +585,10 @@ def install_qnn_sdk() -> bool:
437585 Returns:
438586 True if both steps succeeded (or were already satisfied), else False.
439587 """
440- if check_glibc_exist_and_validate ():
441- if _ensure_libcxx_stack ():
442- if _ensure_qnn_sdk_lib ():
443- return True
444- return False
588+ logger .info ("[QNN] Starting SDK installation" )
589+
590+ # Make sure we’re running under >= 2.34
591+ ensure_glibc_minimum (GLIBC_VERSION )
592+
593+ # libc++ and QNN SDK setup
594+ return _ensure_libcxx_stack () and _ensure_qnn_sdk_lib ()
0 commit comments