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