Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions installer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ pip install -e ".[mssql,postgres]" pyinstaller
python installer/build.py
```

Drivers that reach their dependencies from compiled extension modules need
those dependencies collected explicitly — PyInstaller only analyses Python
bytecode, so imports made from a `.so`/`.pyd` are invisible to it. These are
listed in `DRIVER_HIDDEN_PACKAGES` in `build.py`: Oracle's thin mode
(`oracledb.thin_impl`, a compiled Cython module) imports `cryptography`, and
without it the binary fails at connection time with *“python-oracledb thin
mode cannot be used because the cryptography package cannot be imported”*.
The build aborts if such a companion package is missing from the environment.

## Spec file (advanced)

For tweaks that don’t fit `build.py`’s flags (custom hooks, code signing,
Expand Down
30 changes: 27 additions & 3 deletions installer/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@
# ``psycopg2`` module under a different distribution name.
OPTIONAL_DRIVERS = ("pymssql", "pymysql", "psycopg2", "oracledb")

# Packages a driver imports from *compiled* extension modules. PyInstaller
# analyses Python bytecode only, so anything imported from a .so/.pyd is
# invisible to it and must be collected explicitly.
#
# oracledb's thin mode lives in ``thin_impl`` (a compiled Cython module) and
# imports ``cryptography`` — notably ``cryptography.x509``. Only the parts
# reachable from ``oracledb.plugins.oci_tokens``
# (``cryptography.hazmat.primitives``) end up in the bundle by accident, hence
# the runtime failure "thin mode cannot be used because the cryptography
# package cannot be imported / cannot import name x509".
DRIVER_HIDDEN_PACKAGES = {"oracledb": ("cryptography",)}


def _is_module_available(module_name: str) -> bool:
return importlib.util.find_spec(module_name) is not None
Expand Down Expand Up @@ -242,6 +254,16 @@ def build(
dist_name = _resolve_dist_name(module_name)
if dist_name is not None:
args.extend(["--copy-metadata", dist_name])
for package in DRIVER_HIDDEN_PACKAGES.get(module_name, ()):
if not _is_module_available(package):
raise SystemExit(
f"{module_name} is installed but its required companion package "
f"'{package}' is not: the resulting binary would fail at runtime. "
f"Install it in the build environment (pip install {package})."
)
# --collect-all pulls submodules, data files, binaries (the Rust
# extension of cryptography) and metadata in one go.
args.extend(["--collect-all", package])

if platform.system() == "Windows":
version_file = build_dir / "windows-version-file.txt"
Expand All @@ -262,12 +284,14 @@ def build(

_run_pyinstaller(args, env)

# In --onedir mode ``dist/db2sql`` is the bundle *directory*, and the
# executable sits inside it — hence the is_file() checks.
binary = dist_dir / _executable_name()
if not binary.exists():
if not binary.is_file():
candidate = dist_dir / EXE_NAME / _executable_name()
if candidate.exists():
if candidate.is_file():
binary = candidate
if not binary.exists():
if not binary.is_file():
raise SystemExit(f"PyInstaller did not produce the expected binary at {binary}")

_smoke_test(binary)
Expand Down
Loading