6
6
import platform
7
7
import re
8
8
import shutil
9
+ import subprocess
10
+ import sys
9
11
import tarfile
10
12
import tempfile
11
13
import urllib .request
12
14
import zipfile
13
15
from typing import Dict , List , Optional , Tuple
14
16
17
+
15
18
logger = logging .getLogger (__name__ )
16
19
logger .addHandler (logging .NullHandler ())
17
20
@@ -34,68 +37,81 @@ def is_linux_x86() -> bool:
34
37
)
35
38
36
39
37
- import subprocess
40
+ #########################
41
+ # Cache directory helper
42
+ #########################
38
43
39
- MINIMUM_LIBC_VERSION = 2.29
44
+ APP_NAMESPACE = [ "executorch" , "qnn" ]
40
45
41
- REQUIRED_LIBC_LIBS = [
42
- "/lib/x86_64-linux-gnu/libc.so.6" ,
43
- "/lib64/libc.so.6" ,
44
- "/lib/libc.so.6" ,
45
- ]
46
46
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)
47
54
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.
49
67
"""
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 ):
51
91
"""
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 )
73
97
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 )
76
101
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
+ ####################
99
115
100
116
101
117
def _download_archive (url : str , archive_path : pathlib .Path ) -> bool :
@@ -178,9 +194,6 @@ def _download_qnn_sdk(dst_folder=SDK_DIR) -> Optional[pathlib.Path]:
178
194
if not is_linux_x86 ():
179
195
logger .info ("[QNN] Skipping Qualcomm SDK (only supported on Linux x86)." )
180
196
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
184
197
else :
185
198
logger .info ("[QNN] Downloading Qualcomm SDK for Linux x86" )
186
199
@@ -241,6 +254,136 @@ def _extract_tar(archive_path: pathlib.Path, prefix: str, target_dir: pathlib.Pa
241
254
dst .write (src .read ())
242
255
243
256
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
+
244
387
LLVM_VERSION = "14.0.0"
245
388
LIBCXX_BASE_NAME = f"clang+llvm-{ LLVM_VERSION } -x86_64-linux-gnu-ubuntu-18.04"
246
389
LLVM_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):
258
401
logger .info ("[libcxx] Already staged at %s, skipping download" , target_dir )
259
402
return
260
403
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
263
407
264
408
if not temp_tar .exists ():
265
409
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 } " )
267
415
268
416
logger .info ("[libcxx] Extracting %s" , temp_tar )
269
417
with tarfile .open (temp_tar , "r:xz" ) as tar :
@@ -437,8 +585,10 @@ def install_qnn_sdk() -> bool:
437
585
Returns:
438
586
True if both steps succeeded (or were already satisfied), else False.
439
587
"""
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