Skip to content

ggsql-jupyter macOS wheel: binary cannot find bundled libodbc.dylib (delocate places it under site-packages, binary expects @loader_path/..) #348

@shntnu

Description

@shntnu

Summary

The ggsql-jupyter wheel built for macOS arm64 (and likely x86_64) is broken when installed via uv tool install ggsql-jupyter — the binary cannot find the bundled libodbc.dylib at runtime. The dylib is present in the wheel, but at a path the binary's load command does not reference.

Environment

  • ggsql-jupyter 0.2.7 from PyPI (installed via uv tool install ggsql-jupyter)
  • macOS 14.x, arm64 (Apple Silicon)
  • uv 0.x (default tool install location: ~/.local/share/uv/tools/)

Reproducible example

uv tool install ggsql-jupyter
ggsql-jupyter --help

Result:

dyld[53454]: Library not loaded: @loader_path/../ggsql_jupyter.dylibs/libodbc-166255bd.2.dylib
  Referenced from: <...> /Users/.../.local/share/uv/tools/ggsql-jupyter/bin/ggsql-jupyter
  Reason: tried: '/Users/.../.local/share/uv/tools/ggsql-jupyter/bin/../ggsql_jupyter.dylibs/libodbc-166255bd.2.dylib' (no such file)

Diagnosis

The dylib is in the installed tool, but at the wrong relative path:

$ find ~/.local/share/uv/tools/ggsql-jupyter -name "libodbc*"
~/.local/share/uv/tools/ggsql-jupyter/lib/python3.14/site-packages/ggsql_jupyter.dylibs/libodbc-166255bd.2.dylib

The binary lives at bin/ggsql-jupyter and its load command is hardcoded:

$ otool -L ~/.local/share/uv/tools/ggsql-jupyter/bin/ggsql-jupyter
	@loader_path/../ggsql_jupyter.dylibs/libodbc-166255bd.2.dylib (...)
	...

@loader_path resolves to bin/, so the binary expects <install_root>/ggsql_jupyter.dylibs/libodbc-...dylib. delocate placed it at <install_root>/lib/python3.14/site-packages/ggsql_jupyter.dylibs/libodbc-...dylib. The two locations don't match.

Likely cause

ggsql-jupyter/pyproject.toml declares bindings = "bin", telling maturin this is a binary-only project. The macOS CI step in .github/workflows/release-jupyter.yml runs maturin with --auditwheel=repair, which on macOS invokes delocate. delocate is designed for wheels that wrap a Python module — it bundles dylibs into a <module>.dylibs/ dir adjacent to the Python module in site-packages, and rewrites consumers to load from @loader_path/../<module>.dylibs/. That works when the consumer is a .so extension inside the Python package, where @loader_path is site-packages/<module>/ and the bundled dylibs sit one level up. It does not work when the consumer is a binary that maturin places in bin/, because bin/../<module>.dylibs/ is not where delocate put anything.

Workaround

The native .pkg installer from GitHub Releases (ggsql_0.2.7_aarch64.pkg) ships a working binary. Extract without sudo:

gh release download v0.2.7 --repo posit-dev/ggsql --pattern "ggsql_0.2.7_aarch64.pkg"
pkgutil --expand ggsql_0.2.7_aarch64.pkg expanded
mkdir unpacked && cd unpacked && gunzip -c ../expanded/Payload | cpio -i
# Working binaries at usr/local/bin/ggsql and usr/local/bin/ggsql-jupyter

Possible fixes

  1. Have the macOS wheel build place dylibs at <install_root>/ggsql_jupyter.dylibs/ (where the binary's load command points) — likely requires a custom delocate invocation rather than relying on --auditwheel=repair.
  2. Add an LC_RPATH to the binary pointing at the site-packages dylibs location (relative path through lib/python*/site-packages/ggsql_jupyter.dylibs/).
  3. Statically link libodbc into ggsql-jupyter, eliminating the bundled dylib.
  4. Drop ODBC support from the Jupyter wheel build (since most users won't need ODBC for local DuckDB / SQLite work) and document the .pkg path for ODBC users.

Happy to test a fix on macOS arm64.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions