Skip to content

Commit 0c4bff7

Browse files
authored
feat(copy): add exclude_from parameter for rsync-based copy in Singularity builds (#526)
* feat(copy): add exclude_from parameter for rsync-based copy in Singularity builds * refactor(copy): rename exclude_from to _exclude_from and reorder docstring alphabetically Renamed the parameter `exclude_from` to `_exclude_from` to follow HPCCM convention that container framework-specific options begin with an underscore (e.g., `_chown`, `_mkdir`, `_post`). Also reordered the parameter documentation block in `copy.py` to maintain alphabetical order within the class docstring for consistency. * chore(copy): remove unnecessary info log for rsync exclude_from Removes the redundant `logging.info()` statement in the rsync exclusion branch of the copy primitive to keep logging output minimal and consistent with other primitives. * test(copy): improve _exclude_from tests Refactors the _exclude_from tests to use assertEqual() with full expected recipe strings instead of multiple substring checks. This aligns the test style with other HPCCM copy primitive tests. Note: When `_exclude_from` is used, an empty %files section is still emitted after the rsync-based %setup block. This is intentional to preserve compatibility with the existing copy control flow. The extra section is harmless and may be removed in a future cleanup. * refactor(copy): simplify _exclude_from initialization with default []
1 parent 1238f2e commit 0c4bff7

File tree

2 files changed

+57
-0
lines changed

2 files changed

+57
-0
lines changed

hpccm/primitives/copy.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ class copy(object):
4444
4545
dest: Path in the container image to copy the file(s)
4646
47+
_exclude_from: String or list of strings. One or more filenames
48+
containing rsync-style exclude patterns (e.g., `.apptainerignore`).
49+
Only used when building for Singularity or Apptainer. If specified,
50+
the copy operation is emitted in the `%setup` section using
51+
`rsync --exclude-from=<file>` rather than the standard `%files`
52+
copy directive. This enables selective exclusion of files and
53+
directories during the image build, for example to omit large data
54+
files, caches, or temporary artifacts. Multiple exclusion files may
55+
be provided as a list or tuple. The default is an empty list
56+
(Singularity specific).
57+
4758
files: A dictionary of file pairs, source and destination, to copy
4859
into the container image. If specified, has precedence over
4960
`dest` and `src`.
@@ -80,6 +91,10 @@ class copy(object):
8091
copy(files={'a': '/tmp/a', 'b': '/opt/b'})
8192
```
8293
94+
```python
95+
copy(src='.', dest='/opt/app', _exclude_from='.apptainerignore')
96+
```
97+
8398
"""
8499

85100
def __init__(self, **kwargs):
@@ -96,6 +111,12 @@ def __init__(self, **kwargs):
96111
self._post = kwargs.get('_post', '') # Singularity specific
97112
self.__src = kwargs.get('src', '')
98113

114+
ef = kwargs.get('_exclude_from', [])
115+
if isinstance(ef, (list, tuple)):
116+
self.__exclude_from = list(ef)
117+
elif ef:
118+
self.__exclude_from = [ef]
119+
99120
if self._mkdir and self._post:
100121
logging.error('_mkdir and _post are mutually exclusive!')
101122
self._post = False # prefer _mkdir
@@ -211,6 +232,13 @@ def __str__(self):
211232
dest = pair['dest']
212233
src = pair['src']
213234

235+
# Use rsync if exclusion file provided and not multi-stage copy
236+
if self.__exclude_from and not self.__from:
237+
excl_opts = ' '.join('--exclude-from={}'.format(x) for x in self.__exclude_from)
238+
pre.append(' mkdir -p ${{SINGULARITY_ROOTFS}}{0}'.format(dest))
239+
pre.append(' rsync -av {0} {1}/ ${{SINGULARITY_ROOTFS}}{2}/'.format(excl_opts, src, dest))
240+
continue
241+
214242
if self._post:
215243
dest = '/'
216244

test/test_copy.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,32 @@ def test_from_temp_staging(self):
255255
"""Singularity files from previous stage in tmp"""
256256
c = copy(_from='base', src='foo', dest='/var/tmp/foo')
257257
self.assertEqual(str(c), '%files from base\n foo /var/tmp/foo')
258+
259+
@singularity
260+
def test_exclude_from_single_singularity(self):
261+
"""rsync-based copy with _exclude_from (single source)"""
262+
c = copy(src='.', dest='/opt/app', _exclude_from='.apptainerignore')
263+
self.assertEqual(str(c),
264+
r'''%setup
265+
mkdir -p ${SINGULARITY_ROOTFS}/opt/app
266+
rsync -av --exclude-from=.apptainerignore ./ ${SINGULARITY_ROOTFS}/opt/app/
267+
%files
268+
''')
269+
270+
@singularity
271+
def test_exclude_from_multiple_singularity(self):
272+
"""rsync-based copy with multiple _exclude_from files"""
273+
c = copy(src='data', dest='/opt/data',
274+
_exclude_from=['.ignore1', '.ignore2'])
275+
self.assertEqual(str(c),
276+
r'''%setup
277+
mkdir -p ${SINGULARITY_ROOTFS}/opt/data
278+
rsync -av --exclude-from=.ignore1 --exclude-from=.ignore2 data/ ${SINGULARITY_ROOTFS}/opt/data/
279+
%files
280+
''')
281+
282+
@docker
283+
def test_exclude_from_docker_ignored(self):
284+
"""_exclude_from ignored in Docker context"""
285+
c = copy(src='.', dest='/opt/app', _exclude_from='.apptainerignore')
286+
self.assertEqual(str(c), 'COPY . /opt/app')

0 commit comments

Comments
 (0)