Skip to content

Commit a06c957

Browse files
jameskermodeclaude
andcommitted
Fix missing CLI executables in quippy-ase wheel (#703)
Bundle quip, gap_fit, and md executables in the wheel by: - Add [project.scripts] entry points to pyproject.toml for CLI commands - Create install_executables.py to copy executables during meson install - Update meson.build to run install script via meson.add_install_script() - Improve cli.py error handling with helpful messages when executables missing - Add CIBW_TEST_COMMAND to verify executables are bundled and functional The executables are copied to the quippy package directory during install, and delocate/auditwheel will bundle the required shared libraries. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6834d7e commit a06c957

5 files changed

Lines changed: 83 additions & 12 deletions

File tree

.github/workflows/build-wheels.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ jobs:
138138
# Architecture setting
139139
CIBW_ARCHS: ${{ steps.arch.outputs.arch }}
140140

141+
# Test that the wheel works correctly
142+
CIBW_TEST_COMMAND: >
143+
python -c "import quippy; print(quippy.__version__)" &&
144+
python -c "import os, quippy; assert os.path.exists(os.path.join(quippy.__path__[0], 'quip')), 'quip executable missing'" &&
145+
python -c "import os, quippy; assert os.path.exists(os.path.join(quippy.__path__[0], 'gap_fit')), 'gap_fit executable missing'" &&
146+
python -c "import os, quippy; assert os.path.exists(os.path.join(quippy.__path__[0], 'md')), 'md executable missing'" &&
147+
quip --help &&
148+
gap_fit --help &&
149+
md --help
150+
141151
- name: Debug - list environment
142152
if: failure()
143153
run: |

quippy/install_executables.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env python3
2+
"""Install QUIP executables into the quippy package directory."""
3+
import os
4+
import sys
5+
import shutil
6+
import stat
7+
8+
def main():
9+
# Arguments: source_dir, dest_dir
10+
if len(sys.argv) < 3:
11+
print("Usage: install_executables.py <source_dir> <dest_dir>")
12+
sys.exit(1)
13+
14+
source_dir = sys.argv[1]
15+
dest_dir = sys.argv[2]
16+
17+
# Handle DESTDIR for meson install (used for staged installs / wheel building)
18+
destdir = os.environ.get('DESTDIR', '')
19+
if destdir:
20+
# dest_dir is absolute, so we need to handle the join carefully
21+
if dest_dir.startswith('/'):
22+
dest_dir = destdir + dest_dir
23+
else:
24+
dest_dir = os.path.join(destdir, dest_dir)
25+
26+
executables = ['quip', 'gap_fit', 'md']
27+
28+
os.makedirs(dest_dir, exist_ok=True)
29+
30+
for exe in executables:
31+
src = os.path.join(source_dir, exe)
32+
dst = os.path.join(dest_dir, exe)
33+
if os.path.exists(src):
34+
shutil.copy2(src, dst)
35+
# Ensure executable permissions
36+
os.chmod(dst, os.stat(dst).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
37+
print(f"Installed {exe} to {dst}")
38+
else:
39+
print(f"Warning: {exe} not found at {src}", file=sys.stderr)
40+
41+
if __name__ == '__main__':
42+
main()

quippy/meson.build

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,4 +330,15 @@ install_subdir(
330330
strip_directory: false,
331331
)
332332

333+
# Install CLI executables (quip, gap_fit, md) into the quippy package
334+
# Use install script to handle files from external build directory
335+
install_exe_script = find_program('install_executables.py', required: true)
336+
programs_builddir = quip_builddir / 'Programs'
337+
338+
meson.add_install_script(
339+
install_exe_script,
340+
programs_builddir,
341+
py.get_install_dir() / 'quippy',
342+
)
343+
333344
message('f90wrap wrapper generation and extension build configured')

quippy/pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ dependencies = [
2323
[project.optional-dependencies]
2424
dev = ["pytest"]
2525

26+
[project.scripts]
27+
quip = "quippy.cli:quip"
28+
gap_fit = "quippy.cli:gap_fit"
29+
md = "quippy.cli:md"
30+
quip-config = "quippy.cli:quip_config"
31+
2632
[tool.meson-python.args]
2733
setup = ["-Dbuildtype=release"]
2834
compile = []

quippy/quippy/cli.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,27 @@
44
import quippy
55
import argparse
66

7-
def gap_fit():
7+
def _run_command(name):
8+
"""Run a bundled QUIP executable."""
89
path = quippy.__path__[0]
9-
command = os.path.join(path, 'gap_fit')
10-
sp.call([command] + sys.argv[1:])
10+
command = os.path.join(path, name)
11+
if not os.path.exists(command):
12+
print(f"Error: '{name}' executable not found at {command}", file=sys.stderr)
13+
print("This may indicate an incomplete installation.", file=sys.stderr)
14+
sys.exit(1)
15+
return sp.call([command] + sys.argv[1:])
16+
17+
def gap_fit():
18+
sys.exit(_run_command('gap_fit'))
1119

1220
def quip():
13-
path = quippy.__path__[0]
14-
command = os.path.join(path, 'quip')
15-
sp.call([command] + sys.argv[1:])
21+
sys.exit(_run_command('quip'))
1622

1723
def md():
18-
path = quippy.__path__[0]
19-
command = os.path.join(path, 'md')
20-
sp.call([command] + sys.argv[1:])
24+
sys.exit(_run_command('md'))
2125

2226
def vasp_driver():
23-
path = quippy.__path__[0]
24-
command = os.path.join(path, 'vasp_driver')
25-
sp.call([command] + sys.argv[1:])
27+
sys.exit(_run_command('vasp_driver'))
2628

2729
def quip_config():
2830
parser = argparse.ArgumentParser(description='Configuration tool for QUIP')

0 commit comments

Comments
 (0)