Skip to content

Commit 80cd5e7

Browse files
committed
Fix pipe worker OpenBabel failure by injecting LD_LIBRARY_PATH
Pipe workers invoke Python directly, bypassing conda activation hooks. This causes LD_LIBRARY_PATH to be missing, so OpenBabel plugin .so files fail to load their shared library dependencies. Auto-inject CONDA_PREFIX and LD_LIBRARY_PATH exports into the submit script preamble, and add a configurable pre_cmd field to pipe_settings for additional global setup.
1 parent 2669734 commit 80cd5e7

3 files changed

Lines changed: 93 additions & 6 deletions

File tree

arc/job/pipe/pipe_run.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,39 @@ def _submission_resources(self):
198198
array_size = min(self.max_workers, len(self.tasks)) if self.tasks else self.max_workers
199199
return cpus, memory_mb, array_size
200200

201+
def _build_env_preamble(self) -> str:
202+
"""
203+
Build the shell preamble injected into ``{env_setup}`` in submit templates.
204+
205+
Combines (in order):
206+
1. Auto-detected ``CONDA_PREFIX`` and ``LD_LIBRARY_PATH`` exports
207+
(needed because pipe workers invoke Python directly, bypassing
208+
conda activation hooks that normally set these).
209+
2. User-configured ``pre_cmd`` from ``pipe_settings``.
210+
3. Engine-specific ``env_setup`` from ``pipe_settings``.
211+
4. Scratch directory setup from ``pipe_settings``.
212+
"""
213+
lines = []
214+
prefix_lib = os.path.join(sys.prefix, 'lib')
215+
if os.path.isdir(prefix_lib):
216+
lines.append(f'export CONDA_PREFIX="{sys.prefix}"')
217+
lines.append(
218+
f'export LD_LIBRARY_PATH="{prefix_lib}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}"'
219+
)
220+
pre_cmd = pipe_settings.get('pre_cmd', '')
221+
if pre_cmd:
222+
lines.append(pre_cmd)
223+
engine = self.tasks[0].engine if self.tasks else ''
224+
engine_setup = pipe_settings.get('env_setup', {}).get(engine, '')
225+
if engine_setup:
226+
lines.append(engine_setup)
227+
scratch_base = pipe_settings.get('scratch_base', '')
228+
if scratch_base:
229+
lines.append(
230+
f'export TMPDIR="{scratch_base}/${{PBS_JOBID%%[*}}/$PBS_ARRAY_INDEX"\nmkdir -p "$TMPDIR"'
231+
)
232+
return '\n'.join(lines)
233+
201234
def write_submit_script(self) -> str:
202235
"""
203236
Generate an array submission script for the configured cluster scheduler.
@@ -217,12 +250,7 @@ def write_submit_script(self) -> str:
217250
cpus, memory_mb, array_size = self._submission_resources()
218251
server = servers_dict.get('local', {})
219252
queue, _ = next(iter(server.get('queues', {}).items()), ('', None))
220-
engine = self.tasks[0].engine if self.tasks else ''
221-
env_setup = pipe_settings.get('env_setup', {}).get(engine, '')
222-
scratch_base = pipe_settings.get('scratch_base', '')
223-
if scratch_base:
224-
scratch_export = f'export TMPDIR="{scratch_base}/${{PBS_JOBID%%[*}}/$PBS_ARRAY_INDEX"\nmkdir -p "$TMPDIR"'
225-
env_setup = f'{env_setup}\n{scratch_export}' if env_setup else scratch_export
253+
env_setup = self._build_env_preamble()
226254
content = pipe_submit[template_key].format(
227255
name=f'pipe_{self.run_id}',
228256
max_task_num=array_size,

arc/job/pipe/pipe_run_test.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,62 @@ def test_htcondor_sub_not_executable(self):
198198
self.assertFalse(mode & stat.S_IXUSR, '.sub should not be executable')
199199

200200

201+
class TestPipeRunEnvPreamble(unittest.TestCase):
202+
"""Tests for _build_env_preamble and env injection into submit scripts."""
203+
204+
def setUp(self):
205+
self.tmpdir = tempfile.mkdtemp(prefix='pipe_env_preamble_')
206+
207+
def tearDown(self):
208+
shutil.rmtree(self.tmpdir, ignore_errors=True)
209+
210+
def _make_run(self, cluster_software='slurm', n_tasks=3):
211+
tasks = [_make_spec(f't_{i}') for i in range(n_tasks)]
212+
run = PipeRun(project_directory=self.tmpdir, run_id='env_test',
213+
tasks=tasks, cluster_software=cluster_software,
214+
max_workers=n_tasks)
215+
run.stage()
216+
return run
217+
218+
def test_ld_library_path_in_slurm_script(self):
219+
"""LD_LIBRARY_PATH export appears in generated slurm submit script."""
220+
run = self._make_run('slurm')
221+
path = run.write_submit_script()
222+
with open(path) as f:
223+
content = f.read()
224+
self.assertIn('export LD_LIBRARY_PATH=', content)
225+
self.assertIn('export CONDA_PREFIX=', content)
226+
227+
def test_ld_library_path_in_pbs_script(self):
228+
"""LD_LIBRARY_PATH export appears in generated PBS submit script."""
229+
run = self._make_run('pbs')
230+
path = run.write_submit_script()
231+
with open(path) as f:
232+
content = f.read()
233+
self.assertIn('export LD_LIBRARY_PATH=', content)
234+
235+
def test_ld_library_path_before_python(self):
236+
"""LD_LIBRARY_PATH export appears before the python command."""
237+
run = self._make_run('slurm')
238+
path = run.write_submit_script()
239+
with open(path) as f:
240+
content = f.read()
241+
ld_pos = content.index('export LD_LIBRARY_PATH=')
242+
py_pos = content.index('-m arc.scripts.pipe_worker')
243+
self.assertLess(ld_pos, py_pos)
244+
245+
def test_pre_cmd_injected(self):
246+
"""User-configured pre_cmd appears in the submit script."""
247+
from unittest.mock import patch
248+
run = self._make_run('slurm')
249+
patched = {'pre_cmd': 'module load openbabel/3.1'}
250+
with patch.dict('arc.job.pipe.pipe_run.pipe_settings', patched):
251+
path = run.write_submit_script()
252+
with open(path) as f:
253+
content = f.read()
254+
self.assertIn('module load openbabel/3.1', content)
255+
256+
201257
class TestPipeRunReconcile(unittest.TestCase):
202258

203259
def setUp(self):

arc/settings/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,9 @@
311311
'env_setup': {}, # Engine-specific shell setup commands, e.g.,
312312
# {'gaussian': 'source /usr/local/g09/setup.sh',
313313
# 'orca': 'source /usr/local/orca-5.0.4/setup.sh && source /usr/local/openmpi-4.1.1/setup.sh'}
314+
'pre_cmd': '', # Global shell commands injected before every pipe worker invocation.
315+
# Useful for e.g. 'conda activate arc_env' when the auto-detected
316+
# LD_LIBRARY_PATH is not sufficient.
314317
'scratch_base': '', # Base directory for worker scratch (e.g., '/gtmp'). Leave empty for system default.
315318
}
316319

0 commit comments

Comments
 (0)