|
| 1 | +# Add these imports for additional logging |
| 2 | +import ctypes |
| 3 | +import os |
1 | 4 | import pathlib
|
2 | 5 | import platform
|
3 |
| -import sys |
| 6 | +import shutil |
4 | 7 | import tarfile
|
5 | 8 | import tempfile
|
6 | 9 | import urllib.request
|
7 |
| -import zipfile |
| 10 | +from typing import Optional |
| 11 | + |
| 12 | +SDK_DIR = pathlib.Path(__file__).parent.parent / "sdk" / "qnn" |
| 13 | +PKG_ROOT = pathlib.Path(__file__).parent |
8 | 14 |
|
9 | 15 |
|
10 | 16 | def is_linux_x86() -> bool:
|
11 | 17 | """
|
12 |
| - Check if the current system is Linux running on an x86 architecture. |
| 18 | + Check if the current platform is Linux x86_64. |
13 | 19 |
|
14 | 20 | Returns:
|
15 |
| - bool: True if the system is Linux and the architecture is one of |
16 |
| - x86_64, i386, or i686. False otherwise. |
| 21 | + bool: True if the system is Linux x86_64, False otherwise. |
17 | 22 | """
|
18 |
| - return sys.platform.startswith("linux") and platform.machine() in { |
19 |
| - "x86_64", |
20 |
| - "i386", |
21 |
| - "i686", |
22 |
| - } |
23 |
| - |
| 23 | + system = platform.system().lower() |
| 24 | + machine = platform.machine().lower() |
24 | 25 |
|
25 |
| -SDK_DIR = pathlib.Path(__file__).parent / "sdk" |
| 26 | + return system == "linux" and machine in ("x86_64", "amd64", "i386", "i686") |
26 | 27 |
|
27 | 28 |
|
28 |
| -def _download_qnn_sdk() -> pathlib.Path: |
| 29 | +def _download_qnn_sdk() -> Optional[pathlib.Path]: |
29 | 30 | """
|
30 |
| - Download and extract the Qualcomm SDK from the given URL into target_dir. |
31 |
| -
|
32 |
| - Args: |
33 |
| - url (str): The URL of the archive (.zip, .tar.gz, or .tgz). |
34 |
| - prefix_to_strip (str): Top-level directory inside the archive to strip |
35 |
| - from extracted file paths. |
36 |
| - target_dir (pathlib.Path): Directory to extract the SDK into. |
| 31 | + Download and extract the Qualcomm SDK into SDK_DIR. |
37 | 32 |
|
38 | 33 | Notes:
|
39 | 34 | - Only runs on Linux x86 platforms. Skips otherwise.
|
40 |
| - - Creates the target_dir if it does not exist. |
41 | 35 | """
|
42 |
| - # Default path where the Qualcomm SDK will be installed (under the script directory). |
43 |
| - |
44 |
| - # URL to download the Qualcomm AI Runtime SDK archive. |
| 36 | + print("Downloading Qualcomm SDK...") |
45 | 37 | qairt_url = (
|
46 | 38 | "https://softwarecenter.qualcomm.com/api/download/software/sdks/"
|
47 | 39 | "Qualcomm_AI_Runtime_Community/All/2.34.0.250424/v2.34.0.250424.zip"
|
48 | 40 | )
|
49 |
| - |
50 |
| - # Top-level directory inside the SDK archive to extract. |
51 | 41 | qairt_content_dir = "qairt/2.34.0.250424"
|
52 | 42 |
|
53 | 43 | if not is_linux_x86():
|
54 | 44 | print("Skipping Qualcomm SDK (only supported on Linux x86).")
|
55 |
| - return |
| 45 | + return None |
56 | 46 |
|
57 | 47 | SDK_DIR.mkdir(parents=True, exist_ok=True)
|
| 48 | + print(f"SDK_DIR is {SDK_DIR}, exists: {SDK_DIR.exists()}") |
| 49 | + print(f"Current working directory: {os.getcwd()}") |
58 | 50 |
|
59 | 51 | with tempfile.TemporaryDirectory() as tmpdir:
|
60 | 52 | archive_path = pathlib.Path(tmpdir) / pathlib.Path(qairt_url).name
|
| 53 | + print(f"Temporary directory: {tmpdir}") |
| 54 | + print(f"Archive will be saved to: {archive_path}") |
61 | 55 |
|
62 | 56 | print(f"Downloading Qualcomm SDK from {qairt_url}...")
|
63 |
| - urllib.request.urlretrieve(qairt_url, archive_path) |
| 57 | + try: |
| 58 | + # Use urlretrieve with a reporthook to show progress |
| 59 | + def report_progress(block_num, block_size, total_size): |
| 60 | + downloaded = block_num * block_size |
| 61 | + percent = downloaded / total_size * 100 |
| 62 | + print( |
| 63 | + f"Downloaded: {downloaded}/{total_size} bytes ({percent:.2f}%)", |
| 64 | + end="\r", |
| 65 | + ) |
| 66 | + |
| 67 | + urllib.request.urlretrieve(qairt_url, archive_path, report_progress) |
| 68 | + print("\nDownload completed!") |
| 69 | + |
| 70 | + # Check if file was downloaded successfully |
| 71 | + if archive_path.exists(): |
| 72 | + file_size = archive_path.stat().st_size |
| 73 | + print(f"Downloaded file size: {file_size} bytes") |
| 74 | + if file_size == 0: |
| 75 | + print("WARNING: Downloaded file is empty!") |
| 76 | + else: |
| 77 | + print("ERROR: File was not downloaded!") |
| 78 | + return None |
64 | 79 |
|
| 80 | + except Exception as e: |
| 81 | + print(f"Error during download: {e}") |
| 82 | + return None |
| 83 | + |
| 84 | + # Check extraction method |
65 | 85 | if qairt_url.endswith(".zip"):
|
| 86 | + print("Extracting ZIP archive...") |
66 | 87 | _extract_zip(archive_path, qairt_content_dir, SDK_DIR)
|
67 | 88 | elif qairt_url.endswith((".tar.gz", ".tgz")):
|
| 89 | + print("Extracting TAR archive...") |
68 | 90 | _extract_tar(archive_path, qairt_content_dir, SDK_DIR)
|
69 | 91 | else:
|
70 | 92 | raise ValueError(f"Unsupported archive format: {qairt_url}")
|
71 | 93 |
|
| 94 | + # Verify extraction |
| 95 | + print(f"Verifying extraction to {SDK_DIR}") |
| 96 | + if SDK_DIR.exists(): |
| 97 | + print(f"SDK directory exists. Contents:") |
| 98 | + for item in SDK_DIR.iterdir(): |
| 99 | + print(f" {item.name}") |
| 100 | + else: |
| 101 | + print("ERROR: SDK directory was not created!") |
| 102 | + |
72 | 103 | print(f"Qualcomm SDK extracted to {SDK_DIR}")
|
73 | 104 |
|
74 | 105 | return SDK_DIR
|
75 | 106 |
|
76 | 107 |
|
77 |
| -def _extract_zip(archive_path: pathlib.Path, prefix: str, target_dir: pathlib.Path): |
| 108 | +# You might also want to add detailed logging to your extraction functions |
| 109 | +def _extract_zip(archive_path, content_dir, target_dir): |
| 110 | + print(f"Extracting {archive_path} to {target_dir}") |
| 111 | + print(f"Looking for content in subdirectory: {content_dir}") |
| 112 | + |
| 113 | + # Add your zip extraction code here, with additional logging |
| 114 | + # For example: |
| 115 | + import zipfile |
| 116 | + |
| 117 | + with zipfile.ZipFile(archive_path, "r") as zip_ref: |
| 118 | + # List all files in the archive |
| 119 | + print("Files in archive:") |
| 120 | + for file in zip_ref.namelist(): |
| 121 | + print(f" {file}") |
| 122 | + |
| 123 | + # Extract only the specific content directory |
| 124 | + for file in zip_ref.namelist(): |
| 125 | + if file.startswith(content_dir): |
| 126 | + # Extract with path relative to content_dir |
| 127 | + relative_path = os.path.relpath(file, content_dir) |
| 128 | + if relative_path == ".": |
| 129 | + continue # Skip the directory entry itself |
| 130 | + target_path = target_dir / relative_path |
| 131 | + if file.endswith("/"): |
| 132 | + # Create directory |
| 133 | + target_path.mkdir(parents=True, exist_ok=True) |
| 134 | + else: |
| 135 | + # Extract file |
| 136 | + with zip_ref.open(file) as source, open( |
| 137 | + target_path, "wb" |
| 138 | + ) as target: |
| 139 | + shutil.copyfileobj(source, target) |
| 140 | + print(f"Extracted: {relative_path}") |
| 141 | + |
| 142 | + |
| 143 | +LLVM_VERSION = "14.0.0" |
| 144 | +LIBCXX_LIB_DIR = ( |
| 145 | + PKG_ROOT / "executorch" / "backends" / "qualcomm" / "sdk" / f"libcxx-{LLVM_VERSION}" |
| 146 | +) |
| 147 | +LIBCXX_BASE_NAME = f"clang+llvm-{LLVM_VERSION}-x86_64-linux-gnu-ubuntu-20.04" |
| 148 | + |
| 149 | + |
| 150 | +def stage_libcxx(target_dir: pathlib.Path): |
78 | 151 | """
|
79 |
| - Extract files from a zip archive into target_dir, stripping a prefix. |
80 |
| -
|
81 |
| - Args: |
82 |
| - archive_path (pathlib.Path): Path to the .zip archive. |
83 |
| - prefix (str): Prefix folder inside the archive to strip. |
84 |
| - target_dir (pathlib.Path): Destination directory. |
| 152 | + Download (if needed) and stage libc++ shared libraries into the wheel package. |
| 153 | + - target_dir: destination folder in the wheel, e.g. |
| 154 | + executorch/backends/qualcomm/sdk/libcxx-14.0.0 |
85 | 155 | """
|
86 |
| - with zipfile.ZipFile(archive_path, "r") as zf: |
87 |
| - for member in zf.infolist(): |
88 |
| - if not member.filename.startswith(prefix + "/"): |
89 |
| - continue |
90 |
| - relpath = pathlib.Path(member.filename).relative_to(prefix) |
91 |
| - if not relpath.parts or relpath.parts[0] == "..": |
92 |
| - continue |
93 |
| - out_path = target_dir / relpath |
94 |
| - if member.is_dir(): |
95 |
| - out_path.mkdir(parents=True, exist_ok=True) |
96 |
| - else: |
97 |
| - out_path.parent.mkdir(parents=True, exist_ok=True) |
98 |
| - with zf.open(member) as src, open(out_path, "wb") as dst: |
99 |
| - dst.write(src.read()) |
| 156 | + target_dir.mkdir(parents=True, exist_ok=True) |
100 | 157 |
|
| 158 | + # Check if already staged |
| 159 | + existing_files = list(target_dir.glob("*")) |
| 160 | + if existing_files: |
| 161 | + print(f"[libcxx] Already staged at {target_dir}, skipping") |
| 162 | + return |
101 | 163 |
|
102 |
| -def _extract_tar(archive_path: pathlib.Path, prefix: str, target_dir: pathlib.Path): |
| 164 | + # URL to a tarball with prebuilt libc++ shared libraries |
| 165 | + LLVM_URL = f"https://github.com/llvm/llvm-project/releases/download/llvmorg-{LLVM_VERSION}/{LIBCXX_BASE_NAME}.tar.xz" |
| 166 | + temp_tar = pathlib.Path("/tmp") / f"{LIBCXX_BASE_NAME}.tar.xz" |
| 167 | + temp_extract = pathlib.Path("/tmp") / f"{LIBCXX_BASE_NAME}" |
| 168 | + |
| 169 | + # Download if not already exists |
| 170 | + if not temp_tar.exists(): |
| 171 | + print(f"[libcxx] Downloading {LLVM_URL}") |
| 172 | + urllib.request.urlretrieve(LLVM_URL, temp_tar) |
| 173 | + |
| 174 | + # Extract |
| 175 | + print(f"[libcxx] Extracting {temp_tar}") |
| 176 | + with tarfile.open(temp_tar, "r:xz") as tar: |
| 177 | + tar.extractall(temp_extract.parent) |
| 178 | + |
| 179 | + # Copy only the required .so files |
| 180 | + lib_src = temp_extract / "lib" |
| 181 | + required_files = [ |
| 182 | + "libc++.so.1.0", |
| 183 | + "libc++abi.so.1.0", |
| 184 | + "libunwind.so.1", |
| 185 | + "libm.so.6", |
| 186 | + "libpython3.10.so.1.0", |
| 187 | + ] |
| 188 | + for fname in required_files: |
| 189 | + src_path = lib_src / fname |
| 190 | + if not src_path.exists(): |
| 191 | + raise FileNotFoundError(f"{fname} not found in extracted LLVM") |
| 192 | + shutil.copy(src_path, target_dir / fname) |
| 193 | + |
| 194 | + # Create symlinks |
| 195 | + os.symlink("libc++.so.1.0", target_dir / "libc++.so.1") |
| 196 | + os.symlink("libc++.so.1", target_dir / "libc++.so") |
| 197 | + os.symlink("libc++abi.so.1.0", target_dir / "libc++abi.so.1") |
| 198 | + os.symlink("libc++abi.so.1", target_dir / "libc++abi.so") |
| 199 | + |
| 200 | + print(f"[libcxx] Staged libc++ to {target_dir}") |
| 201 | + |
| 202 | + |
| 203 | +def _load_libcxx_libs(lib_path): |
103 | 204 | """
|
104 |
| - Extract files from a tar.gz archive into target_dir, stripping a prefix. |
| 205 | + Load libc++ shared libraries from the given directory. |
105 | 206 |
|
106 |
| - Args: |
107 |
| - archive_path (pathlib.Path): Path to the .tar.gz or .tgz archive. |
108 |
| - prefix (str): Prefix folder inside the archive to strip. |
109 |
| - target_dir (pathlib.Path): Destination directory. |
| 207 | + Ensures libc++abi is loaded first, then libc++, then any other .so files. |
110 | 208 | """
|
111 |
| - with tarfile.open(archive_path, "r:gz") as tf: |
112 |
| - for m in tf.getmembers(): |
113 |
| - if not m.name.startswith(prefix + "/"): |
114 |
| - continue |
115 |
| - relpath = pathlib.Path(m.name).relative_to(prefix) |
116 |
| - if not relpath.parts or relpath.parts[0] == "..": |
117 |
| - continue |
118 |
| - |
119 |
| - out_path = target_dir / relpath |
120 |
| - if m.isdir(): |
121 |
| - out_path.mkdir(parents=True, exist_ok=True) |
122 |
| - else: |
123 |
| - out_path.parent.mkdir(parents=True, exist_ok=True) |
124 |
| - src = tf.extractfile(m) |
125 |
| - if src is None: |
126 |
| - # Skip non-regular files (links, devices, etc.) |
127 |
| - continue |
128 |
| - with src, open(out_path, "wb") as dst: |
129 |
| - dst.write(src.read()) |
| 209 | + candidates = list(lib_path.glob("*.so*")) |
| 210 | + |
| 211 | + priority = ["libc++abi", "libc++"] |
| 212 | + sorted_candidates = [] |
| 213 | + |
| 214 | + for name in priority: |
| 215 | + for f in candidates: |
| 216 | + if f.name.startswith(name): |
| 217 | + sorted_candidates.append(f) |
| 218 | + |
| 219 | + for f in candidates: |
| 220 | + if f not in sorted_candidates: |
| 221 | + sorted_candidates.append(f) |
| 222 | + |
| 223 | + for sofile in sorted_candidates: |
| 224 | + try: |
| 225 | + ctypes.CDLL(str(sofile), mode=ctypes.RTLD_GLOBAL) |
| 226 | + print(f"Loaded {sofile.name}") |
| 227 | + except OSError as e: |
| 228 | + print(f"[WARN] Failed to load {sofile.name}: {e}") |
0 commit comments