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
- 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.
- Add an
LC_RPATH to the binary pointing at the site-packages dylibs location (relative path through lib/python*/site-packages/ggsql_jupyter.dylibs/).
- Statically link
libodbc into ggsql-jupyter, eliminating the bundled dylib.
- 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.
Summary
The
ggsql-jupyterwheel built for macOS arm64 (and likely x86_64) is broken when installed viauv tool install ggsql-jupyter— the binary cannot find the bundledlibodbc.dylibat runtime. The dylib is present in the wheel, but at a path the binary's load command does not reference.Environment
uv tool install ggsql-jupyter)~/.local/share/uv/tools/)Reproducible example
Result:
Diagnosis
The dylib is in the installed tool, but at the wrong relative path:
The binary lives at
bin/ggsql-jupyterand its load command is hardcoded:@loader_pathresolves tobin/, 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.tomldeclaresbindings = "bin", telling maturin this is a binary-only project. The macOS CI step in.github/workflows/release-jupyter.ymlruns 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 insite-packages, and rewrites consumers to load from@loader_path/../<module>.dylibs/. That works when the consumer is a.soextension inside the Python package, where@loader_pathissite-packages/<module>/and the bundled dylibs sit one level up. It does not work when the consumer is a binary that maturin places inbin/, becausebin/../<module>.dylibs/is not where delocate put anything.Workaround
The native
.pkginstaller from GitHub Releases (ggsql_0.2.7_aarch64.pkg) ships a working binary. Extract without sudo:Possible fixes
<install_root>/ggsql_jupyter.dylibs/(where the binary's load command points) — likely requires a custom delocate invocation rather than relying on--auditwheel=repair.LC_RPATHto the binary pointing at the site-packages dylibs location (relative path throughlib/python*/site-packages/ggsql_jupyter.dylibs/).libodbcintoggsql-jupyter, eliminating the bundled dylib.Happy to test a fix on macOS arm64.