c15e21d59664291da2d548a8c1a10d7f847f1b81
[alexxy/gromacs.git] / admin / containers / scripted_gmx_docker_builds.py
1 #!/usr/bin/env python
2 #
3 # This file is part of the GROMACS molecular simulation package.
4 #
5 # Copyright (c) 2020,2021, by the GROMACS development team, led by
6 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7 # and including many others, as listed in the AUTHORS file in the
8 # top-level source directory and at http://www.gromacs.org.
9 #
10 # GROMACS is free software; you can redistribute it and/or
11 # modify it under the terms of the GNU Lesser General Public License
12 # as published by the Free Software Foundation; either version 2.1
13 # of the License, or (at your option) any later version.
14 #
15 # GROMACS is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18 # Lesser General Public License for more details.
19 #
20 # You should have received a copy of the GNU Lesser General Public
21 # License along with GROMACS; if not, see
22 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
23 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
24 #
25 # If you want to redistribute modifications to GROMACS, please
26 # consider that scientific software is very special. Version
27 # control is crucial - bugs must be traceable. We will be happy to
28 # consider code for inclusion in the official distribution, but
29 # derived work must not be called official GROMACS. Details are found
30 # in the README & COPYING files - if they are missing, get the
31 # official version at http://www.gromacs.org.
32 #
33 # To help us fund GROMACS development, we humbly ask that you cite
34 # the research papers on the package. Check out http://www.gromacs.org.
35
36 """Building block based Dockerfile generation for CI testing images.
37
38 Generates a set of docker images used for running GROMACS CI on Gitlab.
39 The images are prepared according to a selection of build configuration targets
40 that hope to cover a broad enough scope of different possible systems,
41 allowing us to check compiler types and versions, as well as libraries used
42 for accelerators and parallel communication systems. Each combinations is
43 described as an entry in the build_configs dictionary, with the script
44 analysing the logic and adding build stages as needed.
45
46 Based on the example script provided by the NVidia HPCCM repository.
47
48 Reference:
49     `NVidia HPC Container Maker <https://github.com/NVIDIA/hpc-container-maker>`__
50
51 Authors:
52     * Paul Bauer <paul.bauer.q@gmail.com>
53     * Eric Irrgang <ericirrgang@gmail.com>
54     * Joe Jordan <e.jjordan12@gmail.com>
55     * Mark Abraham <mark.j.abraham@gmail.com>
56
57 Usage::
58
59     $ python3 scripted_gmx_docker_builds.py --help
60     $ python3 scripted_gmx_docker_builds.py --format docker > Dockerfile && docker build .
61     $ python3 scripted_gmx_docker_builds.py | docker build -
62
63 See Also:
64     :file:`buildall.sh`
65
66 """
67
68 import argparse
69 import collections
70 import typing
71 from distutils.version import StrictVersion
72
73 import hpccm
74 import hpccm.config
75 from hpccm.building_blocks.base import bb_base
76
77 try:
78     import utility
79 except ImportError:
80     raise RuntimeError(
81         'This module assumes availability of supporting modules in the same directory. Add the directory to '
82         'PYTHONPATH or invoke Python from within the module directory so module location can be resolved.')
83
84 # Basic packages for all final images.
85 _common_packages = ['build-essential',
86                     'ca-certificates',
87                     'ccache',
88                     'git',
89                     'gnupg',
90                     'gpg-agent',
91                     'libfftw3-dev',
92                     'libhwloc-dev',
93                     'liblapack-dev',
94                     'libx11-dev',
95                     'moreutils',
96                     'ninja-build',
97                     'rsync',
98                     'valgrind',
99                     'vim',
100                     'wget',
101                     'xsltproc']
102
103 _opencl_extra_packages = [
104     'nvidia-opencl-dev',
105     # The following require apt_ppas=['ppa:intel-opencl/intel-opencl']
106     'intel-opencl-icd',
107     'ocl-icd-libopencl1',
108     'ocl-icd-opencl-dev',
109     'opencl-headers',
110     # The following require
111     #             apt_keys=['http://repo.radeon.com/rocm/apt/debian/rocm.gpg.key'],
112     #             apt_repositories=['deb [arch=amd64] http://repo.radeon.com/rocm/apt/debian/ xenial main']
113     'libelf1',
114     'rocm-opencl',
115     'rocm-dev',
116     'clinfo'
117 ]
118
119 # Extra packages needed to build Python installations from source.
120 _python_extra_packages = ['build-essential',
121                           'ca-certificates',
122                           'ccache',
123                           'curl',
124                           'git',
125                           'libbz2-dev',
126                           'libffi-dev',
127                           'liblzma-dev',
128                           'libncurses5-dev',
129                           'libncursesw5-dev',
130                           'libreadline-dev',
131                           'libsqlite3-dev',
132                           'libssl-dev',
133                           'llvm',
134                           'python-openssl',
135                           'vim',
136                           'wget',
137                           'zlib1g-dev']
138
139 # Extra packages needed for images for building documentation.
140 _docs_extra_packages = ['autoconf',
141                         'automake',
142                         'autopoint',
143                         'autotools-dev',
144                         'bison',
145                         'flex',
146                         'ghostscript',
147                         'graphviz',
148                         'help2man',
149                         'imagemagick',
150                         'libtool',
151                         'linkchecker',
152                         'mscgen',
153                         'm4',
154                         'openssh-client',
155                         'texinfo',
156                         'texlive-latex-base',
157                         'texlive-latex-extra',
158                         'texlive-fonts-recommended',
159                         'texlive-fonts-extra']
160
161 # Parse command line arguments
162 parser = argparse.ArgumentParser(description='GROMACS CI image creation script',
163                                  parents=[utility.parser])
164
165 parser.add_argument('--format', type=str, default='docker',
166                     choices=['docker', 'singularity'],
167                     help='Container specification format (default: docker)')
168
169
170 def base_image_tag(args) -> str:
171     # Check if we use CUDA images or plain linux images
172     if args.cuda is not None:
173         cuda_version_tag = 'nvidia/cuda:' + args.cuda + '-devel'
174         if args.centos is not None:
175             cuda_version_tag += '-centos' + args.centos
176         elif args.ubuntu is not None:
177             cuda_version_tag += '-ubuntu' + args.ubuntu
178         else:
179             raise RuntimeError('Logic error: no Linux distribution selected.')
180
181         base_image_tag = cuda_version_tag
182     else:
183         if args.centos is not None:
184             base_image_tag = 'centos:centos' + args.centos
185         elif args.ubuntu is not None:
186             base_image_tag = 'ubuntu:' + args.ubuntu
187         else:
188             raise RuntimeError('Logic error: no Linux distribution selected.')
189     return base_image_tag
190
191
192 def get_llvm_packages(args) -> typing.Iterable[str]:
193     # If we use the package version of LLVM, we need to install extra packages for it.
194     if (args.llvm is not None) and (args.tsan is None):
195         return ['libomp-dev',
196                 'libomp5',
197                 'clang-format-' + str(args.llvm),
198                 'clang-tidy-' + str(args.llvm)]
199     else:
200         return []
201
202 def get_opencl_packages(args) -> typing.Iterable[str]:
203     if (args.doxygen is None) and (args.oneapi is None):
204         return _opencl_extra_packages
205     else:
206         return []
207
208 def get_compiler(args, compiler_build_stage: hpccm.Stage = None) -> bb_base:
209     # Compiler
210     if args.llvm is not None:
211         # Build our own version instead to get TSAN + OMP
212         if args.tsan is not None:
213             if compiler_build_stage is not None:
214                 compiler = compiler_build_stage.runtime(_from='tsan')
215             else:
216                 raise RuntimeError('No TSAN compiler build stage!')
217         # Build the default compiler if we don't need special support
218         else:
219             compiler = hpccm.building_blocks.llvm(extra_repository=True, version=args.llvm)
220
221     elif args.oneapi is not None:
222         if compiler_build_stage is not None:
223             compiler = compiler_build_stage.runtime(_from='oneapi')
224             # Prepare the toolchain (needed only for builds done within the Dockerfile, e.g.
225             # OpenMPI builds, which don't currently work for other reasons)
226             oneapi_toolchain = hpccm.toolchain(CC='/opt/intel/oneapi/compiler/latest/linux/bin/intel64/icx',
227                                                CXX='/opt/intel/oneapi/compiler/latest/linux/bin/intel64/icpx')
228             setattr(compiler, 'toolchain', oneapi_toolchain)
229
230         else:
231             raise RuntimeError('No oneAPI compiler build stage!')
232
233     elif args.gcc is not None:
234         compiler = hpccm.building_blocks.gnu(extra_repository=True,
235                                              version=args.gcc,
236                                              fortran=False)
237     else:
238         raise RuntimeError('Logic error: no compiler toolchain selected.')
239     return compiler
240
241
242 def get_mpi(args, compiler):
243     # If needed, add MPI to the image
244     if args.mpi is not None:
245         if args.mpi == 'openmpi':
246             use_cuda = False
247             if args.cuda is not None:
248                 use_cuda = True
249
250             if hasattr(compiler, 'toolchain'):
251                 if args.oneapi is not None:
252                     raise RuntimeError('oneAPI building OpenMPI is not supported')
253                 return hpccm.building_blocks.openmpi(toolchain=compiler.toolchain, cuda=use_cuda, infiniband=False)
254             else:
255                 raise RuntimeError('compiler is not an HPCCM compiler building block!')
256
257         elif args.mpi == 'impi':
258             # TODO Intel MPI from the oneAPI repo is not working reliably,
259             # reasons are unclear. When solved, add packagages called:
260             # 'intel-oneapi-mpi', 'intel-oneapi-mpi-devel'
261             # during the compiler stage.
262             # TODO also consider hpccm's intel_mpi package if that doesn't need
263             # a license to run.
264             raise RuntimeError('Intel MPI recipe not implemented yet.')
265         else:
266             raise RuntimeError('Requested unknown MPI implementation.')
267     else:
268         return None
269
270
271 def get_clfft(args):
272     if (args.clfft is not None):
273         return hpccm.building_blocks.generic_cmake(
274             repository='https://github.com/clMathLibraries/clFFT.git',
275             prefix='/usr/local', recursive=True, branch=args.clfft, directory='clFFT/src')
276     else:
277         return None
278
279
280 def add_tsan_compiler_build_stage(input_args, output_stages: typing.Mapping[str, hpccm.Stage]):
281     """Isolate the expensive TSAN preparation stage.
282
283     This is a very expensive stage, but has few and disjoint dependencies, and
284     its output is easily compartmentalized (/usr/local) so we can isolate this
285     build stage to maximize build cache hits and reduce rebuild time, bookkeeping,
286     and final image size.
287     """
288     if not isinstance(output_stages, collections.abc.MutableMapping):
289         raise RuntimeError('Need output_stages container.')
290     tsan_stage = hpccm.Stage()
291     tsan_stage += hpccm.primitives.baseimage(image=base_image_tag(input_args), _as='tsan')
292
293     tsan_stage += hpccm.building_blocks.packages(ospackages=['git', 'ca-certificates', 'build-essential', 'cmake'])
294     # CMake will get duplicated later, but this is an expensive image, and it isn't worth optimizing
295     # out that duplication...
296     tsan_stage += hpccm.building_blocks.python(python3=True, python2=False, devel=False)
297
298     compiler_branch = 'release/' + str(input_args.llvm) + '.x'
299     tsan_stage += hpccm.building_blocks.generic_cmake(
300         repository='https://github.com/llvm/llvm-project.git',
301         directory='/var/tmp/llvm-project/llvm/',
302         prefix='/usr/local', recursive=True, branch=compiler_branch,
303         cmake_opts=['-D CMAKE_BUILD_TYPE=Release', '-D LLVM_ENABLE_PROJECTS="clang;openmp;clang-tools-extra;compiler-rt;lld"',
304                     '-D LIBOMP_TSAN_SUPPORT=on'],
305         postinstall=['ln -s /usr/local/bin/clang++ /usr/local/bin/clang++-' + str(input_args.llvm),
306                      'ln -s /usr/local/bin/clang-format /usr/local/bin/clang-format-' + str(input_args.llvm),
307                      'ln -s /usr/local/bin/clang-tidy /usr/local/bin/clang-tidy-' + str(input_args.llvm),
308                      'ln -s /usr/local/share/clang/run-clang-tidy.py /usr/local/bin/run-clang-tidy-' + str(input_args.llvm) + '.py',
309                      'ln -s /usr/local/bin/run-clang-tidy-' + str(input_args.llvm) + '.py /usr/local/bin/run-clang-tidy-' + str(input_args.llvm),
310                      'ln -s /usr/local/libexec/c++-analyzer /usr/local/bin/c++-analyzer-' + str(input_args.llvm)])
311     output_stages['compiler_build'] = tsan_stage
312
313 def oneapi_runtime(_from='0'):
314     oneapi_runtime_stage = hpccm.Stage()
315     oneapi_runtime_stage += hpccm.primitives.copy(_from='oneapi-build',
316                                                   files={"/opt/intel": "/opt/intel",
317                                                          "/etc/bash.bashrc": "/etc/bash.bashrc"})
318     return oneapi_runtime_stage
319
320 def add_oneapi_compiler_build_stage(input_args, output_stages: typing.Mapping[str, hpccm.Stage]):
321     """Isolate the oneAPI preparation stage.
322
323     This stage is isolated so that its installed components are minimized in the
324     final image (chiefly /opt/intel) and its environment setup script can be
325     sourced. This also helps with rebuild time and final image size.
326     """
327     if not isinstance(output_stages, collections.abc.MutableMapping):
328         raise RuntimeError('Need output_stages container.')
329     oneapi_stage = hpccm.Stage()
330     oneapi_stage += hpccm.primitives.baseimage(image=base_image_tag(input_args), _as='oneapi-build')
331
332     version = str(input_args.oneapi)
333
334     # Add required components for the next stage (both for hpccm and Intel's setvars.sh script)
335     oneapi_stage += hpccm.building_blocks.packages(ospackages=['wget', 'gnupg2', 'ca-certificates', 'lsb-release'])
336     oneapi_stage += hpccm.building_blocks.packages(
337         apt_keys=['https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS-2023.PUB'],
338         apt_repositories=['deb https://apt.repos.intel.com/oneapi all main'],
339         # Add minimal packages (not the whole HPC toolkit!)
340         ospackages=['intel-oneapi-dpcpp-{}'.format(version),
341             'intel-oneapi-openmp-{}'.format(version),
342             'intel-oneapi-mkl-{}'.format(version),
343             'intel-oneapi-mkl-devel-{}'.format(version)]
344     )
345     # Ensure that all bash shells on the final container will have access to oneAPI
346     oneapi_stage += hpccm.primitives.shell(
347             commands=['echo "source /opt/intel/oneapi/setvars.sh" >> /etc/bash.bashrc']
348             )
349     setattr(oneapi_stage, 'runtime', oneapi_runtime)
350
351     output_stages['compiler_build'] = oneapi_stage
352
353 def prepare_venv(version: StrictVersion) -> typing.Sequence[str]:
354     """Get shell commands to set up the venv for the requested Python version."""
355     major = version.version[0]
356     minor = version.version[1]  # type: int
357
358     pyenv = '$HOME/.pyenv/bin/pyenv'
359
360     py_ver = '{}.{}'.format(major, minor)
361     venv_path = '$HOME/venv/py{}'.format(py_ver)
362     commands = ['$({pyenv} prefix `{pyenv} whence python{py_ver}`)/bin/python -m venv {path}'.format(
363         pyenv=pyenv,
364         py_ver=py_ver,
365         path=venv_path
366     )]
367
368     commands.append('{path}/bin/python -m pip install --upgrade pip setuptools'.format(
369         path=venv_path
370     ))
371     # Install dependencies for building and testing gmxapi Python package.
372     # WARNING: Please keep this list synchronized with python_packaging/requirements-test.txt
373     # TODO: Get requirements.txt from an input argument.
374     commands.append("""{path}/bin/python -m pip install --upgrade \
375             'cmake>=3.13' \
376             'flake8>=3.7.7' \
377             'mpi4py>=3.0.3' \
378             'networkx>=2.0' \
379             'numpy>=1' \
380             'pip>=10.1' \
381             'pytest>=3.9' \
382             'setuptools>=42' \
383             'scikit-build>=0.10'""".format(path=venv_path))
384
385     # TODO: Remove 'importlib_resources' dependency when Python >=3.7 is required.
386     if minor == 6:
387         commands.append("""{path}/bin/python -m pip install --upgrade \
388                 'importlib_resources'""".format(path=venv_path))
389
390     return commands
391
392
393 def add_python_stages(building_blocks: typing.Mapping[str, bb_base],
394                       input_args,
395                       output_stages: typing.MutableMapping[str, hpccm.Stage]):
396     """Add the stage(s) necessary for the requested venvs.
397
398     One intermediate build stage is created for each venv (see --venv option).
399
400     Each stage partially populates Python installations and venvs in the home
401     directory. The home directory is collected by the 'pyenv' stage for use by
402     the main build stage.
403     """
404     if len(input_args.venvs) < 1:
405         raise RuntimeError('No venvs to build...')
406     if output_stages is None or not isinstance(output_stages, collections.abc.Mapping):
407         raise RuntimeError('Need a container for output stages.')
408
409     # Main Python stage that collects the environments from individual stages.
410     # We collect the stages individually, rather than chaining them, because the
411     # copy is a bit slow and wastes local Docker image space for each filesystem
412     # layer.
413     pyenv_stage = hpccm.Stage()
414     pyenv_stage += hpccm.primitives.baseimage(image=base_image_tag(input_args), _as='pyenv')
415     pyenv_stage += building_blocks['compiler']
416     pyenv_stage += building_blocks['mpi']
417     pyenv_stage += hpccm.building_blocks.packages(ospackages=_python_extra_packages)
418
419     for version in [StrictVersion(py_ver) for py_ver in sorted(input_args.venvs)]:
420         stage_name = 'py' + str(version)
421         stage = hpccm.Stage()
422         stage += hpccm.primitives.baseimage(image=base_image_tag(input_args), _as=stage_name)
423         stage += building_blocks['compiler']
424         stage += building_blocks['mpi']
425         stage += hpccm.building_blocks.packages(ospackages=_python_extra_packages)
426
427         # TODO: Use a non-root user for testing and Python virtual environments.
428         stage += hpccm.primitives.shell(commands=[
429             'curl https://pyenv.run | bash',
430             """echo 'export PYENV_ROOT="$HOME/.pyenv"' >> $HOME/.bashrc""",
431             """echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> $HOME/.bashrc""",
432             """echo 'eval "$(pyenv init -)"' >> $HOME/.bashrc""",
433             """echo 'eval "$(pyenv virtualenv-init -)"' >> $HOME/.bashrc"""])
434         pyenv = '$HOME/.pyenv/bin/pyenv'
435         commands = ['PYTHON_CONFIGURE_OPTS="--enable-shared" {pyenv} install -s {version}'.format(
436             pyenv=pyenv,
437             version=str(version))]
438         stage += hpccm.primitives.shell(commands=commands)
439
440         commands = prepare_venv(version)
441         stage += hpccm.primitives.shell(commands=commands)
442
443         # TODO: Update user home directory.
444         pyenv_stage += hpccm.primitives.copy(_from=stage_name, _mkdir=True, src=['/root/'],
445                                              dest='/root')
446
447         # Add the intermediate build stage to the sequence
448         output_stages[stage_name] = stage
449
450     # TODO: If we activate pyenv for login shells, the `global` "version" should be full-featured.
451     # # `version` should be a system installation or pyenv environment (or pyenv-virtualenv)
452     # # with the dependencies for all of the Python aspects of CMake-driven builds.
453     # commands = '{pyenv} global {version}'.format(
454     #             pyenv=pyenv,
455     #             version=...)
456     # pyenv_stage += hpccm.primitives.shell(commands=commands)
457
458     # Add the aggregating build stage to the sequence. This allows the main stage to copy
459     # the files in a single stage, potentially reducing the overall output image size.
460     output_stages['pyenv'] = pyenv_stage
461
462
463 def add_documentation_dependencies(input_args,
464                                    output_stages: typing.MutableMapping[str, hpccm.Stage]):
465     """Add appropriate layers according to doxygen input arguments."""
466     if input_args.doxygen is None:
467         return
468     output_stages['main'] += hpccm.primitives.shell(
469         commands=['sed -i \'/\"XPS\"/d;/\"PDF\"/d;/\"PS\"/d;/\"EPS\"/d;/disable ghostscript format types/d\' /etc/ImageMagick-6/policy.xml'])
470     output_stages['main'] += hpccm.building_blocks.pip(pip='pip3', packages=['sphinx==1.6.1', 'gcovr'])
471     if input_args.doxygen == '1.8.5':
472         doxygen_commit = 'ed4ed873ab0e7f15116e2052119a6729d4589f7a'
473         output_stages['main'] += hpccm.building_blocks.generic_autotools(
474             repository='https://github.com/westes/flex.git',
475             commit='f7788a9a0ecccdc953ed12043ccb59ca25714018',
476             prefix='/tmp/install-of-flex',
477             configure_opts=['--disable-shared'],
478             preconfigure=['./autogen.sh'])
479         output_stages['main'] += hpccm.building_blocks.generic_autotools(
480             repository='https://github.com/doxygen/doxygen.git',
481             commit=doxygen_commit,
482             prefix='',
483             configure_opts=[
484                 '--flex /tmp/install-of-flex/bin/flex',
485                 '--static'])
486     else:
487         version = input_args.doxygen
488         archive_name = 'doxygen-{}.linux.bin.tar.gz'.format(version)
489         archive_url = 'https://sourceforge.net/projects/doxygen/files/rel-{}/{}'.format(
490             version,
491             archive_name
492         )
493         binary_path = 'doxygen-{}/bin/doxygen'.format(version)
494         commands = [
495             'mkdir doxygen && cd doxygen',
496             'wget {}'.format(archive_url),
497             'tar xf {} {}'.format(archive_name, binary_path),
498             'cp {} /usr/local/bin/'.format(binary_path),
499             'cd .. && rm -rf doxygen'
500         ]
501         output_stages['main'] += hpccm.primitives.shell(commands=commands)
502
503
504 def build_stages(args) -> typing.Iterable[hpccm.Stage]:
505     """Define and sequence the stages for the recipe corresponding to *args*."""
506
507     # A Dockerfile or Singularity recipe can have multiple build stages.
508     # The main build stage can copy files from previous stages, though only
509     # the last stage is included in the tagged output image. This means that
510     # large or expensive sets of build instructions can be isolated in
511     # local/temporary images, but all of the stages need to be output by this
512     # script, and need to occur in the correct order, so we create a sequence
513     # object early in this function.
514     stages = collections.OrderedDict()
515
516     # If we need TSAN or oneAPI support the early build is more complex,
517     # so that our compiler images don't have all the cruft needed to get those things
518     # installed.
519     if args.llvm is not None and args.tsan is not None:
520         add_tsan_compiler_build_stage(input_args=args, output_stages=stages)
521     if args.oneapi is not None:
522         add_oneapi_compiler_build_stage(input_args=args, output_stages=stages)
523
524     # Building blocks are chunks of container-builder instructions that can be
525     # copied to any build stage with the addition operator.
526     building_blocks = collections.OrderedDict()
527     building_blocks['base_packages'] = hpccm.building_blocks.packages(
528         ospackages=_common_packages)
529
530     # These are the most expensive and most reusable layers, so we put them first.
531     building_blocks['compiler'] = get_compiler(args, compiler_build_stage=stages.get('compiler_build'))
532     building_blocks['mpi'] = get_mpi(args, building_blocks['compiler'])
533     for i, cmake in enumerate(args.cmake):
534         building_blocks['cmake' + str(i)] = hpccm.building_blocks.cmake(
535             eula=True,
536             prefix='/usr/local/cmake-{}'.format(cmake),
537             version=cmake)
538
539     # Install additional packages early in the build to optimize Docker build layer cache.
540     os_packages = list(get_llvm_packages(args)) + get_opencl_packages(args)
541     if args.doxygen is not None:
542         os_packages += _docs_extra_packages
543     if args.oneapi is not None:
544         os_packages += ['lsb-release']
545     building_blocks['extra_packages'] = hpccm.building_blocks.packages(
546         ospackages=os_packages,
547         apt_ppas=['ppa:intel-opencl/intel-opencl'],
548         apt_keys=['http://repo.radeon.com/rocm/apt/debian/rocm.gpg.key'],
549         apt_repositories=['deb [arch=amd64] http://repo.radeon.com/rocm/apt/debian/ xenial main']
550     )
551
552     building_blocks['clfft'] = get_clfft(args)
553
554     # Add Python environments to MPI images, only, so we don't have to worry
555     # about whether to install mpi4py.
556     if args.mpi is not None and len(args.venvs) > 0:
557         add_python_stages(building_blocks=building_blocks, input_args=args, output_stages=stages)
558
559     # Create the stage from which the targeted image will be tagged.
560     stages['main'] = hpccm.Stage()
561
562     stages['main'] += hpccm.primitives.baseimage(image=base_image_tag(args))
563     for bb in building_blocks.values():
564         if bb is not None:
565             stages['main'] += bb
566
567     # We always add Python3 and Pip
568     stages['main'] += hpccm.building_blocks.python(python3=True, python2=False, devel=True)
569     stages['main'] += hpccm.building_blocks.pip(upgrade=True, pip='pip3',
570                                                 packages=['pytest', 'networkx', 'numpy'])
571
572     # Add documentation requirements (doxygen and sphinx + misc).
573     if args.doxygen is not None:
574         add_documentation_dependencies(args, stages)
575
576     if 'pyenv' in stages and stages['pyenv'] is not None:
577         stages['main'] += hpccm.primitives.copy(_from='pyenv', _mkdir=True, src=['/root/.pyenv/'],
578                                                 dest='/root/.pyenv')
579         stages['main'] += hpccm.primitives.copy(_from='pyenv', _mkdir=True, src=['/root/venv/'],
580                                                 dest='/root/venv')
581         # TODO: Update user home directory.
582         # TODO: If we activate pyenv for login shells, the `global` "version" should be full-featured.
583         # stages['main'] += hpccm.primitives.copy(_from='pyenv', src=['/root/.bashrc'],
584         #                                         dest='/root/')
585
586     # Make sure that `python` resolves to something.
587     stages['main'] += hpccm.primitives.shell(commands=['test -x /usr/bin/python || '
588                                                        'update-alternatives --install /usr/bin/python python /usr/bin/python3 1 && '
589                                                        '/usr/bin/python --version'])
590
591     # Note that the list of stages should be sorted in dependency order.
592     for build_stage in stages.values():
593         if build_stage is not None:
594             yield build_stage
595
596
597 if __name__ == '__main__':
598     args = parser.parse_args()
599
600     # Set container specification output format
601     hpccm.config.set_container_format(args.format)
602
603     container_recipe = build_stages(args)
604
605     # Output container specification
606     for stage in container_recipe:
607         print(stage)