Skip to content

Commit e56836c

Browse files
authored
Fix DLL load failure on Windows , update workflow and simplify sdist packaging (#1053)
* Fix: Replace %ls with %s and use PyUnicode_AsUTF8() in debug log messages Signed-off-by: Earamma K <ek@rocketsoftware.com> * test release to testpypi 3.2.6.10 * test release to testpypi 3.2.6.10 * Fix DLL load failure on Windows , update workflow and simplify sdist packaging * Updated cibuildwheel 3.1.4 for windows 32-bit * move clidriver fallback path logic into an else block when IBM_DB_HOME is not set --------- Signed-off-by: Earamma K <ek@rocketsoftware.com>
1 parent ee7140a commit e56836c

8 files changed

Lines changed: 193 additions & 53 deletions

File tree

.github/workflows/bld_wheels_and_upload.yml

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ jobs:
2121
steps:
2222
- uses: actions/checkout@v5
2323
- name: Build wheels
24-
uses: pypa/cibuildwheel@v3.1.4
24+
uses: pypa/cibuildwheel@v3.4.1
2525
env:
2626
CIBW_BUILD: "*-win_amd64"
2727
CIBW_SKIP: "cp38-*"
2828

29-
- uses: actions/upload-artifact@v4.4.3
29+
- name: Inject ibm_db_dll.pth into wheels
30+
run: python scripts/inject_pth_into_wheel.py wheelhouse
31+
32+
- uses: actions/upload-artifact@v6
3033
with:
3134
name: ibmdb-wheels64-${{ matrix.os }}
3235
path: wheelhouse/*.whl
@@ -46,7 +49,10 @@ jobs:
4649
CIBW_BUILD: "*-win32"
4750
CIBW_SKIP: "cp38-*"
4851

49-
- uses: actions/upload-artifact@v4.4.3
52+
- name: Inject ibm_db_dll.pth into wheels
53+
run: python scripts/inject_pth_into_wheel.py wheelhouse
54+
55+
- uses: actions/upload-artifact@v6
5056
with:
5157
name: ibmdb-wheels32-${{ matrix.os }}
5258
path: wheelhouse/*.whl
@@ -61,7 +67,7 @@ jobs:
6167
steps:
6268
- uses: actions/checkout@v5
6369
- name: Build wheels
64-
uses: pypa/cibuildwheel@v3.1.4
70+
uses: pypa/cibuildwheel@v3.4.1
6571
env:
6672
CIBW_ARCHS_LINUX: "x86_64 i686"
6773
CIBW_MANYLINUX_I686_IMAGE: manylinux2014
@@ -81,7 +87,7 @@ jobs:
8187
--wheel-dir {dest_dir}
8288
{wheel}
8389

84-
- uses: actions/upload-artifact@v4.4.3
90+
- uses: actions/upload-artifact@v6
8591
with:
8692
name: ibmdb-wheels-${{ matrix.os }}
8793
path: wheelhouse/*.whl
@@ -96,12 +102,12 @@ jobs:
96102
steps:
97103
- uses: actions/checkout@v5
98104
- name: Build wheels
99-
uses: pypa/cibuildwheel@v3.1.4
105+
uses: pypa/cibuildwheel@v3.4.1
100106
env:
101107
CIBW_SKIP: "cp38-*"
102108
MACOSX_DEPLOYMENT_TARGET: 14.0
103109

104-
- uses: actions/upload-artifact@v4.4.3
110+
- uses: actions/upload-artifact@v6
105111
with:
106112
name: ibmdb-wheelsarm64
107113
path: wheelhouse/*.whl
@@ -116,13 +122,13 @@ jobs:
116122
steps:
117123
- uses: actions/checkout@v5
118124
- name: Build wheels
119-
uses: pypa/cibuildwheel@v3.1.4
125+
uses: pypa/cibuildwheel@v3.4.1
120126
env:
121127
CIBW_ARCHS: "x86_64"
122128
CIBW_SKIP: "cp38-*"
123129
MACOSX_DEPLOYMENT_TARGET: 10.15
124130

125-
- uses: actions/upload-artifact@v4.4.3
131+
- uses: actions/upload-artifact@v6
126132
with:
127133
name: ibmdb-wheelsx86-${{ matrix.os }}
128134
path: wheelhouse/*.whl
@@ -136,26 +142,19 @@ jobs:
136142
run: python -m pip install --upgrade pip build
137143
- name: Build sdist
138144
run: python -m build --sdist --no-isolation
139-
- name: Package version
140-
id: version
145+
- name: Remove clidriver from sdist
141146
run: |
142-
cd dist
143-
pip install ibm_db*
144-
echo "VERSION=$(python -c 'import ibm_db; print(ibm_db.__version__)')" >> $GITHUB_OUTPUT
145-
- name: Build source distribution
146-
run: |
147-
PACKAGE="ibm_db-$VERSION"
148147
cd dist
149-
tar -xzf $PACKAGE.tar.gz
150-
rm -rf $PACKAGE/clidriver*
151-
rm -rf $PACKAGE.tar.gz
152-
tar -czf $PACKAGE.tar.gz $PACKAGE
153-
rm -rf $PACKAGE
154-
env:
155-
VERSION: ${{ steps.version.outputs.VERSION}}
148+
TARBALL=$(ls ibm?db-*.tar.gz | head -1)
149+
DIRNAME="${TARBALL%.tar.gz}"
150+
tar -xzf "$TARBALL"
151+
rm -rf "$DIRNAME"/clidriver*
152+
rm -rf "$TARBALL"
153+
tar -czf "$TARBALL" "$DIRNAME"
154+
rm -rf "$DIRNAME"
156155
157156
- name: Upload sdist
158-
uses: actions/upload-artifact@v4.4.3
157+
uses: actions/upload-artifact@v6
159158
with:
160159
name: ibmdb-sdist
161160
path: dist/*.tar.gz
@@ -173,12 +172,12 @@ jobs:
173172
#upload to PyPI on every tag starting with 'v'
174173
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
175174
steps:
176-
- uses: actions/download-artifact@v5
175+
- uses: actions/download-artifact@v7
177176
with:
178177
path: dist
179178
pattern: ibmdb-*
180179
merge-multiple: true
181180

182181
- name: Publish distribution to PyPI
183182
if: startsWith(github.ref, 'refs/tags')
184-
uses: pypa/gh-action-pypi-publish@release/v1.12
183+
uses: pypa/gh-action-pypi-publish@release/v1.13

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
include ibm_db.h ibm_db.c
22
include CHANGES.md LICENSE README.md
33
include config.py.sample
4+
include ibm_db_dll.pth
5+
include _ibm_db_register_dll.py
46
recursive-include ibm_db_tests *.py *.png *.jpg
57
include MANIFEST.in
68
recursive-include clidriver *

README.md

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -119,31 +119,38 @@ pip install ibm_db --no-binary :all: --no-cache-dir
119119

120120
- When ibm_db get installed from wheel package, you can find clidriver under site_packages directory of Python. You need to copy license file under `site_packages/clidriver/license` to be effective, if any.
121121

122-
**Note:** For windows after installing ibm_db, recieves the below error when we try to import ibm_db :
122+
**Windows DLL resolution (Python 3.8+):**
123+
124+
Since Python 3.8, the `PATH` environment variable is no longer used for DLL resolution on Windows (see https://bugs.python.org/issue36085). The `ibm_db` package now handles this **automatically** by installing an `ibm_db_dll.pth` file into `site-packages`. This file runs at Python startup and registers the clidriver `bin` directory via `os.add_dll_directory()`, so `import ibm_db` works out of the box.
125+
126+
If `IBM_DB_HOME` is set, the `.pth` file uses `%IBM_DB_HOME%\bin`; otherwise it uses the bundled `site-packages\clidriver\bin`.
127+
128+
**If you still see `ImportError: DLL load failed` after a fresh install**, verify that the `.pth` file exists:
123129

124-
```>python
125-
Python 3.11.4 (tags/v3.11.4:d2340ef, Jun 7 2023, 05:45:37) [MSC v.1934 64 bit (AMD64)] on win32
126-
Type "help", "copyright", "credits" or "license" for more information.
127-
>>> import ibm_db
128-
Traceback (most recent call last):
129-
File "<stdin>", line 1, in <module>
130-
ImportError: DLL load failed while importing ibm_db: The specified module could not be found.
131-
>>>
130+
```
131+
python -c "import os, sysconfig; print(os.path.isfile(os.path.join(sysconfig.get_path('purelib'), 'ibm_db_dll.pth')))"
132132
```
133133

134-
We need to make sure to set dll path of dependent library of clidriver before importing the module as:
134+
If it prints `False`, reinstall ibm_db:
135135

136136
```
137+
pip uninstall ibm_db
138+
pip install ibm_db
139+
```
140+
141+
**Manual fallback:** If the automatic fix does not work in your environment, you can set the DLL path directly in your code before importing the module:
142+
143+
```python
137144
import os
138145
os.add_dll_directory('path to clidriver installation until bin')
139146
import ibm_db
140-
141-
e.g:
142-
os.add_dll_directory('C:\\Program Files\\IBM\\CLIDRIVER\\bin')
143-
import ibm_db
144147
```
145148

146-
Refer https://bugs.python.org/issue36085 for more details.
149+
To find your clidriver `bin` path, run:
150+
151+
```
152+
python -c "import os, site, sysconfig; paths=[os.path.join(site.getusersitepackages(),'clidriver','bin'), os.path.join(sysconfig.get_path('purelib'),'clidriver','bin')]; print(next((p for p in paths if os.path.isdir(p)), 'clidriver not found - reinstall ibm_db'))"
153+
```
147154

148155
- <a name="docker"></a>For installing ibm_db on docker Linux container, you can refer as below:
149156

_ibm_db_register_dll.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Auto-generated by ibm_db setup.py
2+
# Registers clidriver DLL directory for Python 3.8+ on Windows.
3+
# This module is imported at startup via ibm_db_dll.pth.
4+
import os, sys, site, sysconfig
5+
6+
if sys.platform == "win32" and hasattr(os, "add_dll_directory"):
7+
candidates = []
8+
9+
# 1. IBM_DB_HOME environment variable (highest priority)
10+
ibm_home = os.environ.get("IBM_DB_HOME")
11+
if ibm_home:
12+
candidates.append(os.path.join(ibm_home.strip('"'), "bin"))
13+
else:
14+
# 2. User site-packages/clidriver (pip install --user)
15+
try:
16+
usp = site.getusersitepackages()
17+
if usp:
18+
candidates.append(os.path.join(usp, "clidriver", "bin"))
19+
except Exception:
20+
pass
21+
22+
# 3. System site-packages/clidriver (standard pip install)
23+
candidates.append(
24+
os.path.join(sysconfig.get_path("purelib"), "clidriver", "bin")
25+
)
26+
27+
# 4. PATH entries that look like DB2/clidriver installs
28+
for d in os.environ.get("PATH", "").split(";"):
29+
if d and os.path.basename(d).lower() == "bin":
30+
if (os.path.isfile(os.path.join(d, "db2cli.exe")) or
31+
os.path.isdir(os.path.join(os.path.dirname(d), "license"))):
32+
candidates.append(d)
33+
34+
# Register the first valid DLL directory
35+
for p in candidates:
36+
if p and os.path.isdir(p):
37+
os.add_dll_directory(p)
38+
break

ibm_db.c

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2354,8 +2354,8 @@ static PyObject *_python_ibm_db_connect_helper(PyObject *self, PyObject *args, i
23542354
database, SQL_NTS, NULL, 0, NULL,
23552355
SQL_DRIVER_NOPROMPT);
23562356
Py_END_ALLOW_THREADS;
2357-
snprintf(messageStr, sizeof(messageStr), "SQLDriverConnectW called with parameters: conn_res->hdbc=%p, SQLHWND=NULL, database=%ls, SQL_NTS=%d, NULL, 0, NULL, SQL_DRIVER_NOPROMPT=%d and returned rc=%d",
2358-
(void *)conn_res->hdbc, database, SQL_NTS, SQL_DRIVER_NOPROMPT, rc);
2357+
snprintf(messageStr, sizeof(messageStr), "SQLDriverConnectW called with parameters: conn_res->hdbc=%p, SQLHWND=NULL, database=%s, SQL_NTS=%d, NULL, 0, NULL, SQL_DRIVER_NOPROMPT=%d and returned rc=%d",
2358+
(void *)conn_res->hdbc, PyUnicode_AsUTF8(databaseObj), SQL_NTS, SQL_DRIVER_NOPROMPT, rc);
23592359
LogMsg(DEBUG, messageStr);
23602360
}
23612361
else
@@ -2378,10 +2378,10 @@ static PyObject *_python_ibm_db_connect_helper(PyObject *self, PyObject *args, i
23782378
PyUnicode_GetLength(uidObj) * 2,
23792379
password,
23802380
PyUnicode_GetLength(passwordObj) * 2);
2381-
snprintf(messageStr, sizeof(messageStr), "SQLConnectW called with parameters: conn_res->hdbc=%p, database=%ls, databaseLen=%zd, uid=%ls, uidLen=%zd, password=%ls, passwordLen=%zd and returned rc=%d",
2382-
(void *)conn_res->hdbc, database,
2383-
PyUnicode_GetLength(databaseObj) * 2, uid,
2384-
PyUnicode_GetLength(uidObj) * 2, password,
2381+
snprintf(messageStr, sizeof(messageStr), "SQLConnectW called with parameters: conn_res->hdbc=%p, database=%s, databaseLen=%zd, uid=%s, uidLen=%zd, password=%s, passwordLen=%zd and returned rc=%d",
2382+
(void *)conn_res->hdbc, PyUnicode_AsUTF8(databaseObj),
2383+
PyUnicode_GetLength(databaseObj) * 2, PyUnicode_AsUTF8(uidObj),
2384+
PyUnicode_GetLength(uidObj) * 2, PyUnicode_AsUTF8(passwordObj),
23852385
PyUnicode_GetLength(passwordObj) * 2, rc);
23862386
LogMsg(DEBUG, messageStr);
23872387
#else
@@ -2392,10 +2392,10 @@ static PyObject *_python_ibm_db_connect_helper(PyObject *self, PyObject *args, i
23922392
PyUnicode_GetLength(uidObj),
23932393
password,
23942394
PyUnicode_GetLength(passwordObj));
2395-
snprintf(messageStr, sizeof(messageStr), "SQLConnectW called with parameters: conn_res->hdbc=%p, database=%ls, databaseLen=%zd, uid=%ls, uidLen=%zd, password=%ls, passwordLen=%zd and returned rc=%d",
2396-
(void *)conn_res->hdbc, database,
2397-
PyUnicode_GetLength(databaseObj), uid,
2398-
PyUnicode_GetLength(uidObj), password,
2395+
snprintf(messageStr, sizeof(messageStr), "SQLConnectW called with parameters: conn_res->hdbc=%p, database=%s, databaseLen=%zd, uid=%s, uidLen=%zd, password=%s, passwordLen=%zd and returned rc=%d",
2396+
(void *)conn_res->hdbc, PyUnicode_AsUTF8(databaseObj),
2397+
PyUnicode_GetLength(databaseObj), PyUnicode_AsUTF8(uidObj),
2398+
PyUnicode_GetLength(uidObj), PyUnicode_AsUTF8(passwordObj),
23992399
PyUnicode_GetLength(passwordObj), rc);
24002400
LogMsg(DEBUG, messageStr);
24012401
#endif

ibm_db_dll.pth

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import _ibm_db_register_dll

scripts/inject_pth_into_wheel.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Inject ibm_db_dll.pth into a wheel so it lands in site-packages on install.
2+
3+
Usage: python scripts/inject_pth_into_wheel.py <wheel_dir>
4+
5+
Wheels are zip files. Files at the root level of a wheel (alongside .py
6+
modules) are installed to site-packages. This script adds ibm_db_dll.pth
7+
to every .whl file in the given directory.
8+
"""
9+
import os, sys, hashlib, base64, zipfile, glob, tempfile, shutil
10+
11+
PTH_FILENAME = 'ibm_db_dll.pth'
12+
PTH_CONTENT = 'import _ibm_db_register_dll\n'
13+
14+
15+
def _record_line(name, content_bytes):
16+
"""Build a RECORD entry: name,sha256=<digest>,<length>"""
17+
digest = hashlib.sha256(content_bytes).digest()
18+
b64 = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
19+
return f'{name},sha256={b64},{len(content_bytes)}'
20+
21+
22+
def inject_pth(whl_path):
23+
"""Add ibm_db_dll.pth to a wheel file and remove any misplaced copies."""
24+
with zipfile.ZipFile(whl_path, 'r') as zin:
25+
names = zin.namelist()
26+
# Skip if the .pth file is already at the wheel root
27+
if PTH_FILENAME in names:
28+
print(f' {PTH_FILENAME} already at root of {os.path.basename(whl_path)}, skipping')
29+
return
30+
31+
tmp_fd, tmp_path = tempfile.mkstemp(suffix='.whl')
32+
os.close(tmp_fd)
33+
34+
pth_bytes = PTH_CONTENT.encode('utf-8')
35+
pth_record = _record_line(PTH_FILENAME, pth_bytes)
36+
37+
with zipfile.ZipFile(whl_path, 'r') as zin, \
38+
zipfile.ZipFile(tmp_path, 'w', zipfile.ZIP_DEFLATED) as zout:
39+
40+
for item in zin.infolist():
41+
# Drop any misplaced copies of the .pth file (absolute-path junk from data_files)
42+
if item.filename != PTH_FILENAME and item.filename.endswith('/' + PTH_FILENAME):
43+
print(f' Removing misplaced {item.filename}')
44+
continue
45+
46+
data = zin.read(item.filename)
47+
48+
# Append our .pth entry to the RECORD file
49+
if item.filename.endswith('/RECORD'):
50+
data = data.rstrip(b'\n') + b'\n' + pth_record.encode('utf-8') + b'\n'
51+
52+
zout.writestr(item, data)
53+
54+
# Add the .pth file at the wheel root
55+
zout.writestr(PTH_FILENAME, pth_bytes)
56+
57+
shutil.move(tmp_path, whl_path)
58+
print(f' Injected {PTH_FILENAME} into {os.path.basename(whl_path)}')
59+
60+
61+
def main():
62+
if len(sys.argv) != 2:
63+
print(f'Usage: {sys.argv[0]} <wheel_dir>')
64+
sys.exit(1)
65+
66+
wheel_dir = sys.argv[1]
67+
wheels = glob.glob(os.path.join(wheel_dir, '*.whl'))
68+
69+
if not wheels:
70+
print(f'No .whl files found in {wheel_dir}')
71+
sys.exit(1)
72+
73+
for whl in wheels:
74+
inject_pth(whl)
75+
76+
print(f'Done: processed {len(wheels)} wheel(s)')
77+
78+
79+
if __name__ == '__main__':
80+
main()

setup.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from distutils.sysconfig import get_python_lib
2929
from setuptools import setup, find_packages, Extension
3030
from setuptools.command.build_ext import build_ext
31+
from setuptools.command.build_py import build_py
3132
from setuptools.command.install import install
3233

3334
PACKAGE = 'ibm_db'
@@ -501,7 +502,7 @@ def print_exception( e, url):
501502
(get_python_lib(), ['./LICENSE']),
502503
(get_python_lib(), ['./config.py.sample'])]
503504

504-
modules = ['ibm_db_dbi', 'testfunctions', 'ibmdb_tests', 'ibm_db_ctx']
505+
modules = ['ibm_db_dbi', 'testfunctions', 'ibmdb_tests', 'ibm_db_ctx', '_ibm_db_register_dll']
505506

506507
if 'zos' == sys.platform:
507508
ext_modules = _ext_modules(os.path.join(os.getcwd(), include_dir), library, ibm_db_lib, ibm_db_lib_runtime)
@@ -525,6 +526,18 @@ def print_exception( e, url):
525526
_checkGcc()
526527
_checkPythonHeaderFile()
527528

529+
# Custom build_py to include ibm_db_dll.pth at the wheel root
530+
# so it lands in site-packages and triggers DLL registration on startup.
531+
class _build_py_with_pth(build_py):
532+
def run(self):
533+
super().run()
534+
pth_src = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ibm_db_dll.pth')
535+
if os.path.isfile(pth_src):
536+
pth_dst = os.path.join(self.build_lib, 'ibm_db_dll.pth')
537+
self.copy_file(pth_src, pth_dst)
538+
539+
cmd_class['build_py'] = _build_py_with_pth
540+
528541
#'Operating System :: z/OS', pypi upload fails with error - Not a valid classifier
529542
setup( name = PACKAGE,
530543
version = VERSION,

0 commit comments

Comments
 (0)