Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions hpccm/primitives/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ class copy(object):

dest: Path in the container image to copy the file(s)

_exclude_from: String or list of strings. One or more filenames
containing rsync-style exclude patterns (e.g., `.apptainerignore`).
Only used when building for Singularity or Apptainer. If specified,
the copy operation is emitted in the `%setup` section using
`rsync --exclude-from=<file>` rather than the standard `%files`
copy directive. This enables selective exclusion of files and
directories during the image build, for example to omit large data
files, caches, or temporary artifacts. Multiple exclusion files may
be provided as a list or tuple. The default is an empty list
(Singularity specific).

files: A dictionary of file pairs, source and destination, to copy
into the container image. If specified, has precedence over
`dest` and `src`.
Expand Down Expand Up @@ -80,6 +91,10 @@ class copy(object):
copy(files={'a': '/tmp/a', 'b': '/opt/b'})
```

```python
copy(src='.', dest='/opt/app', _exclude_from='.apptainerignore')
```

"""

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

ef = kwargs.get('_exclude_from', [])
if isinstance(ef, (list, tuple)):
self.__exclude_from = list(ef)
elif ef:
self.__exclude_from = [ef]

if self._mkdir and self._post:
logging.error('_mkdir and _post are mutually exclusive!')
self._post = False # prefer _mkdir
Expand Down Expand Up @@ -211,6 +232,13 @@ def __str__(self):
dest = pair['dest']
src = pair['src']

# Use rsync if exclusion file provided and not multi-stage copy
if self.__exclude_from and not self.__from:
excl_opts = ' '.join('--exclude-from={}'.format(x) for x in self.__exclude_from)
pre.append(' mkdir -p ${{SINGULARITY_ROOTFS}}{0}'.format(dest))
pre.append(' rsync -av {0} {1}/ ${{SINGULARITY_ROOTFS}}{2}/'.format(excl_opts, src, dest))
continue

if self._post:
dest = '/'

Expand Down
29 changes: 29 additions & 0 deletions test/test_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,32 @@ def test_from_temp_staging(self):
"""Singularity files from previous stage in tmp"""
c = copy(_from='base', src='foo', dest='/var/tmp/foo')
self.assertEqual(str(c), '%files from base\n foo /var/tmp/foo')

@singularity
def test_exclude_from_single_singularity(self):
"""rsync-based copy with _exclude_from (single source)"""
c = copy(src='.', dest='/opt/app', _exclude_from='.apptainerignore')
self.assertEqual(str(c),
r'''%setup
mkdir -p ${SINGULARITY_ROOTFS}/opt/app
rsync -av --exclude-from=.apptainerignore ./ ${SINGULARITY_ROOTFS}/opt/app/
%files
''')

@singularity
def test_exclude_from_multiple_singularity(self):
"""rsync-based copy with multiple _exclude_from files"""
c = copy(src='data', dest='/opt/data',
_exclude_from=['.ignore1', '.ignore2'])
self.assertEqual(str(c),
r'''%setup
mkdir -p ${SINGULARITY_ROOTFS}/opt/data
rsync -av --exclude-from=.ignore1 --exclude-from=.ignore2 data/ ${SINGULARITY_ROOTFS}/opt/data/
%files
''')

@docker
def test_exclude_from_docker_ignored(self):
"""_exclude_from ignored in Docker context"""
c = copy(src='.', dest='/opt/app', _exclude_from='.apptainerignore')
self.assertEqual(str(c), 'COPY . /opt/app')
Loading