zarchive-py 0.0.1b3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ include README.md
2
+ include setup.py
3
+ include _bundle_native.py
4
+ include _plat.py
5
+ include zarchive/_bin/.gitkeep
6
+ recursive-exclude zarchive/_bin *.dll *.so *.dylib *.so.*
7
+ graft tests
8
+ global-exclude *.py[cod] __pycache__
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: zarchive-py
3
+ Version: 0.0.1b3
4
+ Summary: Python bindings for the zArchive library (identify, create, read, extract, verify).
5
+ Home-page: https://github.com/Leon-Zhang/lib_main
6
+ Author: Leon Zhang
7
+ Author-email: leonzh.work@outlook.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: C++
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Operating System :: MacOS
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/plain
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: requires-python
22
+ Dynamic: summary
23
+
24
+ zArchive reads and writes archive files (zip, gzip, bzip2, tar, 7z, cab, rar, and others): identify a file, open it, list and read members, extract, verify, and create new archives. This package calls that library from Python. The wheel includes the matching native library for the platform it was built on.
@@ -0,0 +1,241 @@
1
+ # zarchive-py
2
+
3
+ Python ctypes bindings for the **zArchive** native library. Install from PyPI as `zarchive-py`; import the module as `zarchive`. The package wraps the C API in `zArchive/CApi.h` plus the older exports:
4
+
5
+ - Identify archive paths (`zarchive_identify_*` / `ArchiveFSItemIdentifyA/W`)
6
+ - Open, list, read, verify, and extract (`zarchive_open_w`, `zarchive_enum_items`, `zarchive_read_file_alloc_w`, `zarchive_extract_*`, `zarchive_verify_*`)
7
+ - Create archives (`zarchive_create_begin_w`, `zarchive_add_*`, `zarchive_create_finish`, `zarchive_create_empty_w`)
8
+ - `CreateCompressedFile` / `FreeCompressedFile` — allocate a format-specific `CArchive*` (zip, gz, bz, tar, 7z, cab, za, ...)
9
+ - `zarchive_deinitialize` / `DeinitializeAll` — tear down native globals
10
+
11
+ `pip install zarchive-py` installs a **platform wheel** that already embeds the native `zArchive` library under `zarchive/_bin`. pip selects the wheel that matches the install OS/arch. No manual copy of `zArchive.dll` / `libzArchive.so` / `libzArchive.dylib` is required after install.
12
+
13
+ Published wheel tags:
14
+
15
+ | Platform | Wheel tag | Native library |
16
+ |---|---|---|
17
+ | Windows x64 | `win_amd64` | `zArchive.dll` |
18
+ | Linux x86_64 | `manylinux_2_28_x86_64` or `manylinux_2_35_x86_64` | `libzArchive.so` |
19
+ | Linux aarch64 | `manylinux_*_aarch64` | `libzArchive.so` |
20
+ | macOS Intel | `macosx_11_0_x86_64` | `libzArchive.dylib` |
21
+ | macOS Apple Silicon | `macosx_11_0_arm64` | `libzArchive.dylib` |
22
+ | iOS device | `ios_13_0_arm64_iphoneos` | `libzArchive.so` |
23
+ | iOS simulator | `ios_13_0_arm64_iphonesimulator` or `ios_13_0_x86_64_iphonesimulator` | `libzArchive.so` |
24
+ | Android | `android_21_arm64_v8a` (also `armeabi_v7a`, `x86_64`, `x86`) | `libzArchive.so` |
25
+
26
+ The published `sdist` is source-only and cannot be installed without a native build.
27
+
28
+ ## Requirements
29
+
30
+ - Python 3.8+ (`python` or `python3` on `PATH`)
31
+ - A matching platform wheel. The published `sdist` is source-only.
32
+ - To **build** a wheel: a native `zArchive` shared library for that OS/arch (see below). The build copies it into the package.
33
+
34
+ DLL / shared-library search order at runtime:
35
+
36
+ 1. Extra paths passed to `resolve_dll_path`
37
+ 2. The installed package's `zarchive/_bin` directory
38
+ 3. Under `$WORKSPACE_PATH` and `$WORKSPACE_PATH/lib_main` (`bin/x64`, `bin/linux_x86_64`, `bin/macos_arm64`, `bin/ios_arm64_iphoneos`, `bin/android_arm64_v8a`, …)
39
+ 4. The same folders under the repo root
40
+
41
+ ## Build the native library
42
+
43
+ Required only when creating a new wheel, not when installing from PyPI.
44
+
45
+ **Windows:** from this directory, `build_native.bat` (or `powershell -File build_native.ps1`) builds `zArchive.vcxproj` as **Dynamic_Release|x64**. Output: `%WORKSPACE_PATH%\bin\x64\zArchive.dll`. Set `ZARCHIVE_VS_CONFIG=Dynamic_Debug` for `zArchived.dll`. Debug wheels also need sibling engine DLLs in `bin\x64` and the CUDA 12 runtime (`cudart64_12.dll`). Import libraries under `%WORKSPACE_PATH%\lib\x64` must already exist; this script builds the zArchive project, not the rest of the solution.
46
+
47
+ **Linux / macOS:** on that OS, from this directory:
48
+
49
+ ```sh
50
+ chmod +x build_native.sh
51
+ ./build_native.sh
52
+ ```
53
+
54
+ `build_native.sh` does not run on Windows. It runs the CMake project at `build/cmake/zArchive` for the machine it is on (no cross-compile) and installs:
55
+
56
+ - Linux x86_64: `bin/linux_x86_64/libzArchive.so` (and `bin/linux/`)
57
+ - Linux aarch64: `bin/linux_aarch64/libzArchive.so`
58
+ - macOS Intel: `bin/macos_x86_64/libzArchive.dylib` (and `bin/macos/`)
59
+ - macOS Apple Silicon: `bin/macos_arm64/libzArchive.dylib`
60
+
61
+ **iOS** (macOS with Xcode) and **Android** (NDK) are cross builds. They produce `libzArchive.so` and do not replace the host library.
62
+
63
+ ```sh
64
+ chmod +x build_native_ios.sh build_native_android.sh
65
+ ./build_native_ios.sh
66
+ ./build_native_android.sh
67
+ ```
68
+
69
+ - iOS device: `bin/ios_arm64_iphoneos/libzArchive.so` (`ZARCHIVE_IOS_SDK=iphoneos`, deployment target 13.0)
70
+ - iOS simulator: `bin/ios_arm64_iphonesimulator/libzArchive.so` (`ZARCHIVE_IOS_SDK=iphonesimulator`)
71
+ - Android arm64: `bin/android_arm64_v8a/libzArchive.so` (`ZARCHIVE_ANDROID_ABI=arm64-v8a`, API 21)
72
+
73
+ On Windows the Android NDK build is `build_native_android.bat`. Install and smoke tests for those wheels run only under CPython with `sys.platform` of `ios` or `android`:
74
+
75
+ ```sh
76
+ ./devops/ios/_ios_install_test.sh
77
+ ./devops/android/_android_install_test.sh
78
+ ```
79
+
80
+ CMake needs a C++14 compiler. The shared library compiles zArchive plus sibling engine sources (`ZARCHIVE_EMBED_DEPS=ON`). Build each desktop architecture on a machine of that architecture. iOS and Android use the cross scripts above.
81
+
82
+ Ubuntu packages the script can pick up: `python3-dev`, `libmysqlclient-dev`, `liblua5.3-dev`, `mono-devel`, and `libnode-dev`. On macOS the same script looks up Homebrew (`python@3`, `mysql-client`, `lua`, `mono`, `node`) and the active `python3` include path, and sets `MACOSX_DEPLOYMENT_TARGET` to `11.0` when it is unset so the dylib matches the `macosx_11_0_*` wheel tag. Ogg/Vorbis headers ship in `extlib`.
83
+
84
+ GitHub Actions workflow `.github/workflows/zarchive-wheels.yml` builds the Linux and macOS wheels on `ubuntu-22.04`, `macos-13`, and `macos-14`.
85
+
86
+ ## Build the Python package
87
+
88
+ From this directory (`build/Python/zArchive`).
89
+
90
+ Windows:
91
+
92
+ ```bat
93
+ build_package.bat
94
+ ```
95
+
96
+ Linux / macOS:
97
+
98
+ ```sh
99
+ chmod +x build_package.sh
100
+ ./build_package.sh
101
+ ```
102
+
103
+ That stages the native library into `zarchive/_bin`, then writes:
104
+
105
+ - `dist/zarchive_py-0.0.1-py3-none-<platform>.whl` (embeds native libs)
106
+ - `dist/zarchive_py-0.0.1.tar.gz` (source only)
107
+
108
+ Equivalent: `build_package.ps1` or `python build_wheels.py`. A `py3-none-any` wheel is not produced.
109
+
110
+ To package extra wheels from natives already sitting under `bin/linux_*` or `bin/macos_*` (even on another OS):
111
+
112
+ ```sh
113
+ python build_wheels.py --all
114
+ # or one target
115
+ python build_wheels.py --plat manylinux_2_28_x86_64 --native-dir ../../../../bin/linux_x86_64
116
+ ```
117
+
118
+ Override the wheel tag with `ZARCHIVE_WHEEL_PLAT` and the library folder with `ZARCHIVE_NATIVE_DIR`. On Linux, `ZARCHIVE_MANYLINUX` defaults to `manylinux_2_28`.
119
+
120
+ Install the wheel into the current interpreter:
121
+
122
+ ```bat
123
+ python -m pip install --force-reinstall dist\zarchive_py-0.0.1-py3-none-win_amd64.whl
124
+ ```
125
+
126
+ ```sh
127
+ python3 -m pip install --force-reinstall dist/zarchive_py-0.0.1-py3-none-*.whl
128
+ ```
129
+
130
+ After uploading every platform wheel to the same PyPI version, `pip install zarchive-py` picks the file for the current OS.
131
+
132
+ ## Publish to PyPI
133
+
134
+ Scripts live in `build/Python` (default package folder is `zArchive`). Set `TWINE_USERNAME=__token__` and `TWINE_PASSWORD` to a PyPI/TestPyPI API token, or write `%USERPROFILE%\.pypirc`.
135
+
136
+ ```bat
137
+ cd /d ..
138
+ publish_check.bat
139
+ publish_testpypi.bat
140
+ publish_pypi.bat
141
+ ```
142
+
143
+ ```sh
144
+ cd ..
145
+ ./publish_check.sh
146
+ ./publish_testpypi.sh
147
+ ./publish_pypi.sh
148
+ ```
149
+
150
+ Production upload prompts for `YES` unless `PYPI_UPLOAD_CONFIRM=YES`. The PyPI project name is `zarchive-py`. After upload:
151
+
152
+ ```bat
153
+ python -m pip install zarchive-py
154
+ ```
155
+
156
+ ## Run tests
157
+
158
+ Uses the bundled native library in `zarchive/_bin` after a package build, or a workspace build via `WORKSPACE_PATH`.
159
+
160
+ ```bat
161
+ test_package.bat
162
+ ```
163
+
164
+ ```sh
165
+ chmod +x test_package.sh
166
+ ./test_package.sh
167
+ ```
168
+
169
+ Each case prints its description, key values, and PASS/FAIL/SKIP/ERROR, then a module summary. Same output via:
170
+
171
+ ```sh
172
+ python tests/test_zarchive.py
173
+ ```
174
+
175
+ A short smoke script is also available:
176
+
177
+ ```sh
178
+ python test_smoke.py
179
+ ```
180
+
181
+ Tests that need the native library are skipped if it is not found in `zarchive/_bin` or on the search path.
182
+
183
+ ## Usage
184
+
185
+ ```python
186
+ from zarchive import (
187
+ Archive,
188
+ ArchiveReader,
189
+ ArchiveWriter,
190
+ GzArchive,
191
+ SevenZArchive,
192
+ ZipArchive,
193
+ extract,
194
+ format_from_path,
195
+ format_infos,
196
+ identify,
197
+ supported_formats,
198
+ get_library,
199
+ resolve_dll_path,
200
+ )
201
+
202
+ print(resolve_dll_path())
203
+ print(identify(r"C:\data\pack.zip"))
204
+ print(supported_formats())
205
+ print(format_infos())
206
+ print(format_from_path(r"C:\data\pack.7z"))
207
+
208
+ with ArchiveReader(r"C:\data\pack.tar") as arc:
209
+ print(arc.namelist())
210
+ print(arc.read("readme.txt"))
211
+ arc.extractall(r"C:\data\out")
212
+
213
+ extract(r"C:\data\pack.gz", r"C:\data\out2")
214
+
215
+ # Extension selects the format when archive_type is ARCHIVE_AUTO (the default).
216
+ with ArchiveWriter(r"C:\data\new.7z") as writer:
217
+ writer.writestr("hello.txt", "hello")
218
+ writer.write(r"C:\data\file.bin", "file.bin")
219
+
220
+ with GzArchive.create(r"C:\data\new.gz") as writer:
221
+ writer.writestr("hello.txt", "hello")
222
+
223
+ with ZipArchive() as zf:
224
+ print(hex(zf.handle))
225
+ with SevenZArchive() as z7:
226
+ print(hex(z7.handle))
227
+
228
+ get_library().deinitialize_all()
229
+ ```
230
+
231
+ `WORKSPACE_PATH` is only needed when running from a source checkout that has not staged `zarchive/_bin`:
232
+
233
+ ```bat
234
+ set WORKSPACE_PATH=d:\Workspace
235
+ ```
236
+
237
+ ```sh
238
+ export WORKSPACE_PATH="$HOME/Workspace"
239
+ ```
240
+
241
+ On Windows, the loader adds the DLL directory and CUDA `bin` folders (`CUDA_PATH`, `CUDA_PATH_V*`, or a `PATH` entry that contains `cudart64_12.dll`) via `os.add_dll_directory`. Debug `zCoded` / `zArchive` builds need the CUDA 12 runtime. On Linux it prepends `LD_LIBRARY_PATH`; on macOS, `DYLD_LIBRARY_PATH`.
@@ -0,0 +1,425 @@
1
+ """Copy the built zArchive shared library and its local deps into zarchive/_bin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from _plat import (
14
+ family_from_wheel_tag,
15
+ host_family,
16
+ host_wheel_tag,
17
+ native_search_dirs,
18
+ primary_library_names,
19
+ )
20
+
21
+ _HERE = Path(__file__).resolve().parent
22
+ _BIN_DIR = _HERE / "zarchive" / "_bin"
23
+ _REPO_ROOT = _HERE.parents[2]
24
+
25
+ _SYSTEM_LIBS = {
26
+ "kernel32.dll",
27
+ "user32.dll",
28
+ "gdi32.dll",
29
+ "ole32.dll",
30
+ "oleaut32.dll",
31
+ "advapi32.dll",
32
+ "shell32.dll",
33
+ "shlwapi.dll",
34
+ "ws2_32.dll",
35
+ "comctl32.dll",
36
+ "comdlg32.dll",
37
+ "cabinet.dll",
38
+ "ntdll.dll",
39
+ "bcrypt.dll",
40
+ "crypt32.dll",
41
+ "winhttp.dll",
42
+ "wininet.dll",
43
+ "imm32.dll",
44
+ "msvcrt.dll",
45
+ "ucrtbase.dll",
46
+ "vcruntime140.dll",
47
+ "vcruntime140_1.dll",
48
+ "msvcp140.dll",
49
+ "rpcrt4.dll",
50
+ "setupapi.dll",
51
+ "version.dll",
52
+ "winmm.dll",
53
+ "iphlpapi.dll",
54
+ "dbghelp.dll",
55
+ "psapi.dll",
56
+ "powrprof.dll",
57
+ "wlanapi.dll",
58
+ "python3.dll",
59
+ "python311.dll",
60
+ "python310.dll",
61
+ "python39.dll",
62
+ "python38.dll",
63
+ "libsystem.b.dylib",
64
+ "libc.so.6",
65
+ "libm.so.6",
66
+ "libdl.so.2",
67
+ "libpthread.so.0",
68
+ "librt.so.1",
69
+ "ld-linux-x86-64.so.2",
70
+ "ld-linux-aarch64.so.1",
71
+ "libgcc_s.so.1",
72
+ "libstdc++.so.6",
73
+ "libcxx.so.1",
74
+ "libc++.1.dylib",
75
+ "libSystem.B.dylib",
76
+ }
77
+
78
+ _SIBLING_PREFIXES = (
79
+ "zArchive",
80
+ "zMemLib",
81
+ "zPlugin",
82
+ "ude",
83
+ "SIMDProcessor",
84
+ "zThread",
85
+ "zTask",
86
+ "cpuidt",
87
+ "zDebug",
88
+ "zScript",
89
+ "zCode",
90
+ "zDataStorage",
91
+ "zDevEnv",
92
+ "zPlatform",
93
+ "zCompiler",
94
+ "CodeGen",
95
+ "zMaths",
96
+ "COMHelper",
97
+ "zLocale",
98
+ "zArcMain",
99
+ )
100
+
101
+
102
+ def package_bin_dir() -> Path:
103
+ return _BIN_DIR
104
+
105
+
106
+ def _target_family() -> str:
107
+ override = os.environ.get("ZARCHIVE_WHEEL_PLAT")
108
+ if override:
109
+ return family_from_wheel_tag(override)
110
+ return host_family()
111
+
112
+
113
+ def _primary_names() -> list[str]:
114
+ return primary_library_names(_target_family())
115
+
116
+
117
+ def _plat_dirs() -> tuple[str, ...]:
118
+ return native_search_dirs(_target_family())
119
+
120
+
121
+ def _search_roots() -> list[Path]:
122
+ roots: list[Path] = []
123
+ workspace = os.environ.get("WORKSPACE_PATH")
124
+ if workspace:
125
+ roots.append(Path(workspace))
126
+ roots.append(Path(workspace) / "lib_main")
127
+ roots.append(_REPO_ROOT)
128
+ seen: set[str] = set()
129
+ unique: list[Path] = []
130
+ for root in roots:
131
+ key = os.path.normcase(str(root.resolve())) if root.exists() else os.path.normcase(str(root))
132
+ if key not in seen:
133
+ seen.add(key)
134
+ unique.append(root)
135
+ return unique
136
+
137
+
138
+ def _explicit_native_dir() -> Optional[Path]:
139
+ raw = os.environ.get("ZARCHIVE_NATIVE_DIR")
140
+ if not raw:
141
+ return None
142
+ path = Path(raw)
143
+ return path if path.is_dir() else None
144
+
145
+
146
+ def find_primary_library() -> Optional[Path]:
147
+ explicit = _explicit_native_dir()
148
+ if explicit is not None:
149
+ for name in _primary_names():
150
+ candidate = explicit / name
151
+ if candidate.is_file():
152
+ return candidate
153
+ for item in explicit.iterdir():
154
+ if item.is_file() and item.suffix.lower() in {".dll", ".so", ".dylib"}:
155
+ if "zarchive" in item.name.lower():
156
+ return item
157
+ for root in _search_roots():
158
+ for rel in _plat_dirs():
159
+ directory = root / Path(rel)
160
+ for name in _primary_names():
161
+ candidate = directory / name
162
+ if candidate.is_file():
163
+ return candidate
164
+ return None
165
+
166
+
167
+ def _is_system_lib(name: str) -> bool:
168
+ lowered = name.lower()
169
+ if lowered in _SYSTEM_LIBS:
170
+ return True
171
+ if lowered.startswith("api-ms-win-"):
172
+ return True
173
+ if lowered.startswith("vcruntime") or lowered.startswith("msvcp") or lowered.startswith("ucrtbase"):
174
+ return True
175
+ if lowered.startswith("libpython"):
176
+ return True
177
+ return False
178
+
179
+
180
+ def _dumpbin_path() -> Optional[str]:
181
+ vswhere = Path(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")) / (
182
+ "Microsoft Visual Studio/Installer/vswhere.exe"
183
+ )
184
+ if not vswhere.is_file():
185
+ return None
186
+ try:
187
+ out = subprocess.check_output(
188
+ [str(vswhere), "-latest", "-products", "*", "-find", r"**\dumpbin.exe"],
189
+ text=True,
190
+ stderr=subprocess.DEVNULL,
191
+ )
192
+ except (subprocess.CalledProcessError, OSError):
193
+ return None
194
+ fallback = None
195
+ for line in out.splitlines():
196
+ path = line.strip()
197
+ if not path or not Path(path).is_file():
198
+ continue
199
+ lowered = path.lower()
200
+ if lowered.endswith(r"\hostx64\x64\dumpbin.exe"):
201
+ return path
202
+ if "dumpbin.exe" in lowered:
203
+ fallback = path
204
+ return fallback
205
+
206
+
207
+ def _dependents_windows(dll: Path) -> list[str]:
208
+ dumpbin = _dumpbin_path()
209
+ if not dumpbin:
210
+ return []
211
+ try:
212
+ out = subprocess.check_output([dumpbin, "/dependents", str(dll)], text=True, stderr=subprocess.STDOUT)
213
+ except (subprocess.CalledProcessError, OSError):
214
+ return []
215
+ names: list[str] = []
216
+ capture = False
217
+ for line in out.splitlines():
218
+ if "Image has the following dependencies" in line:
219
+ capture = True
220
+ continue
221
+ if capture and line.strip() == "Summary":
222
+ break
223
+ match = re.match(r"\s+(\S+\.dll)\s*$", line, re.I)
224
+ if capture and match:
225
+ names.append(match.group(1))
226
+ return names
227
+
228
+
229
+ def _dependents_unix(library: Path) -> list[str]:
230
+ if sys.platform == "darwin":
231
+ cmd = ["otool", "-L", str(library)]
232
+ else:
233
+ cmd = ["ldd", str(library)]
234
+ try:
235
+ out = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT)
236
+ except (subprocess.CalledProcessError, OSError, FileNotFoundError):
237
+ return []
238
+ names: list[str] = []
239
+ for line in out.splitlines():
240
+ if sys.platform == "darwin":
241
+ match = re.search(r"^\s+(\S+\.dylib)", line)
242
+ else:
243
+ match = re.search(r"(\S+\.so(?:\.\d+)*)\s+=>\s+(\S+)", line)
244
+ if match and match.group(2) not in {"not", "found"}:
245
+ names.append(os.path.basename(match.group(2)))
246
+ continue
247
+ match = re.search(r"^\s+(\S+\.so(?:\.\d+)*)", line)
248
+ if match:
249
+ names.append(os.path.basename(match.group(1)))
250
+ return names
251
+
252
+
253
+ def _can_inspect_library(library: Path) -> bool:
254
+ family = _target_family()
255
+ suffix = library.suffix.lower()
256
+ if family == "windows":
257
+ return os.name == "nt" and suffix == ".dll"
258
+ if family == "macos":
259
+ return sys.platform == "darwin" and suffix == ".dylib"
260
+ if family in {"android", "ios"}:
261
+ return False
262
+ return os.name != "nt" and sys.platform != "darwin" and ".so" in library.name
263
+
264
+
265
+ def _copy_all_shared_from(directory: Path) -> list[Path]:
266
+ files: list[Path] = []
267
+ for item in directory.iterdir():
268
+ if not item.is_file():
269
+ continue
270
+ name = item.name.lower()
271
+ if name.endswith((".dll", ".dylib")) or ".so" in name:
272
+ files.append(item)
273
+ return files
274
+
275
+
276
+ def _fix_loader_paths() -> None:
277
+ """Make bundled Unix libraries look next to themselves for siblings."""
278
+ family = _target_family()
279
+ if family == "windows" or os.name == "nt":
280
+ return
281
+ if family == "macos" and sys.platform == "darwin":
282
+ for item in _BIN_DIR.iterdir():
283
+ if not item.is_file() or item.suffix != ".dylib":
284
+ continue
285
+ subprocess.run(
286
+ ["install_name_tool", "-id", f"@loader_path/{item.name}", str(item)],
287
+ check=False,
288
+ stdout=subprocess.DEVNULL,
289
+ stderr=subprocess.DEVNULL,
290
+ )
291
+ subprocess.run(
292
+ ["install_name_tool", "-add_rpath", "@loader_path", str(item)],
293
+ check=False,
294
+ stdout=subprocess.DEVNULL,
295
+ stderr=subprocess.DEVNULL,
296
+ )
297
+ return
298
+ if family == "linux" and os.name != "nt" and sys.platform != "darwin":
299
+ patchelf = shutil.which("patchelf")
300
+ if not patchelf:
301
+ return
302
+ for item in _BIN_DIR.iterdir():
303
+ if not item.is_file() or ".so" not in item.name:
304
+ continue
305
+ subprocess.run(
306
+ [patchelf, "--set-rpath", "$ORIGIN", str(item)],
307
+ check=False,
308
+ stdout=subprocess.DEVNULL,
309
+ stderr=subprocess.DEVNULL,
310
+ )
311
+
312
+
313
+ def _dependents(library: Path) -> list[str]:
314
+ if os.name == "nt":
315
+ return _dependents_windows(library)
316
+ return _dependents_unix(library)
317
+
318
+
319
+ def _cuda_runtime_dirs() -> list[Path]:
320
+ dirs: list[Path] = []
321
+ for key, value in os.environ.items():
322
+ if key == "CUDA_PATH" or key.startswith("CUDA_PATH_V"):
323
+ dirs.append(Path(value) / "bin")
324
+ for part in os.environ.get("PATH", "").split(os.pathsep):
325
+ if part and (Path(part) / "cudart64_12.dll").is_file():
326
+ dirs.append(Path(part))
327
+ extra = Path(r"E:\ProgramFiles\nVidia\CUDA\v12.6\bin")
328
+ if extra.is_dir():
329
+ dirs.append(extra)
330
+ return dirs
331
+
332
+
333
+ def _copy_optional_runtime(name: str, dest: Path) -> None:
334
+ if (dest / name).is_file():
335
+ return
336
+ search: list[Path] = []
337
+ if name.lower().startswith("cudart"):
338
+ search.extend(_cuda_runtime_dirs())
339
+ for directory in search:
340
+ src = directory / name
341
+ if src.is_file():
342
+ shutil.copy2(src, dest / name)
343
+ print(f" bundled runtime {name}")
344
+ return
345
+
346
+
347
+ def _sibling_fallback(primary: Path) -> list[Path]:
348
+ files: list[Path] = []
349
+ suffix = primary.suffix
350
+ for item in primary.parent.iterdir():
351
+ if not item.is_file() or item.suffix.lower() != suffix.lower():
352
+ continue
353
+ if any(item.name.startswith(prefix) for prefix in _SIBLING_PREFIXES):
354
+ files.append(item)
355
+ return files
356
+
357
+
358
+ def _clear_bin_dir() -> None:
359
+ _BIN_DIR.mkdir(parents=True, exist_ok=True)
360
+ for item in _BIN_DIR.iterdir():
361
+ if item.name == ".gitkeep":
362
+ continue
363
+ if item.is_file():
364
+ item.unlink()
365
+ elif item.is_dir():
366
+ shutil.rmtree(item)
367
+
368
+
369
+ def stage_native_libs() -> Path:
370
+ """Copy the primary library and local deps into zarchive/_bin. Raises if missing."""
371
+ primary = find_primary_library()
372
+ if primary is None:
373
+ raise FileNotFoundError(
374
+ "Native zArchive library not found. Build the shared library for this "
375
+ "platform (Windows: build_native.ps1; Linux/macOS: build_native.sh; "
376
+ "iOS: build_native_ios.sh; Android: build_native_android.sh), "
377
+ "or set WORKSPACE_PATH / ZARCHIVE_NATIVE_DIR."
378
+ )
379
+
380
+ _clear_bin_dir()
381
+ copied: dict[str, Path] = {}
382
+ source_dir = primary.parent
383
+ inspect = _can_inspect_library(primary)
384
+ queue = [primary]
385
+ if not inspect:
386
+ queue.extend(_copy_all_shared_from(source_dir))
387
+
388
+ while queue:
389
+ current = queue.pop(0)
390
+ key = current.name.lower()
391
+ if key in copied:
392
+ continue
393
+ dest = _BIN_DIR / current.name
394
+ shutil.copy2(current, dest)
395
+ copied[key] = dest
396
+ print(f" bundled {current.name}")
397
+ if not inspect:
398
+ continue
399
+ for dep_name in _dependents(current):
400
+ if _is_system_lib(dep_name):
401
+ continue
402
+ local = source_dir / dep_name
403
+ if local.is_file():
404
+ queue.append(local)
405
+ else:
406
+ _copy_optional_runtime(dep_name, _BIN_DIR)
407
+
408
+ if inspect and len(copied) == 1:
409
+ for sibling in _sibling_fallback(primary):
410
+ key = sibling.name.lower()
411
+ if key not in copied:
412
+ shutil.copy2(sibling, _BIN_DIR / sibling.name)
413
+ copied[key] = sibling
414
+ print(f" bundled sibling {sibling.name}")
415
+
416
+ _fix_loader_paths()
417
+ print(f" wheel tag: {host_wheel_tag()}")
418
+
419
+ if not any((_BIN_DIR / name).is_file() for name in _primary_names()):
420
+ raise FileNotFoundError(f"Failed to stage {primary.name} into {_BIN_DIR}")
421
+ return _BIN_DIR
422
+
423
+
424
+ def bundled_primary_names() -> list[str]:
425
+ return [name for name in _primary_names() if (_BIN_DIR / name).is_file()]