Fix llvm build in docker container
[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.icc is not None:
211         raise RuntimeError('Intel compiler toolchain recipe not implemented yet')
212
213     if args.llvm is not None:
214         # Build our own version instead to get TSAN + OMP
215         if args.tsan is not None:
216             if compiler_build_stage is not None:
217                 compiler = compiler_build_stage.runtime(_from='tsan')
218             else:
219                 raise RuntimeError('No TSAN compiler build stage!')
220         # Build the default compiler if we don't need special support
221         else:
222             compiler = hpccm.building_blocks.llvm(extra_repository=True, version=args.llvm)
223
224     elif args.oneapi is not None:
225         if compiler_build_stage is not None:
226             compiler = compiler_build_stage.runtime(_from='oneapi')
227             # Prepare the toolchain (needed only for builds done within the Dockerfile, e.g.
228             # OpenMPI builds, which don't currently work for other reasons)
229             oneapi_toolchain = hpccm.toolchain(CC='/opt/intel/oneapi/compiler/latest/linux/bin/intel64/icc',
230                                                CXX='/opt/intel/oneapi/compiler/latest/linux/bin/intel64/icpc')
231             setattr(compiler, 'toolchain', oneapi_toolchain)
232
233         else:
234             raise RuntimeError('No oneAPI compiler build stage!')
235
236     elif args.gcc is not None:
237         compiler = hpccm.building_blocks.gnu(extra_repository=True,
238                                              version=args.gcc,
239                                              fortran=False)
240     else:
241         raise RuntimeError('Logic error: no compiler toolchain selected.')
242     return compiler
243
244
245 def get_mpi(args, compiler):
246     # If needed, add MPI to the image
247     if args.mpi is not None:
248         if args.mpi == 'openmpi':
249             use_cuda = False
250             if args.cuda is not None:
251                 use_cuda = True
252
253             if hasattr(compiler, 'toolchain'):
254                 if args.oneapi is not None:
255                     raise RuntimeError('oneAPI building OpenMPI is not supported')
256                 return hpccm.building_blocks.openmpi(toolchain=compiler.toolchain, cuda=use_cuda, infiniband=False)
257             else:
258                 raise RuntimeError('compiler is not an HPCCM compiler building block!')
259
260         elif args.mpi == 'impi':
261             # TODO Intel MPI from the oneAPI repo is not working reliably,
262             # reasons are unclear. When solved, add packagages called:
263             # 'intel-oneapi-mpi', 'intel-oneapi-mpi-devel'
264             # during the compiler stage.
265             # TODO also consider hpccm's intel_mpi package if that doesn't need
266             # a license to run.
267             raise RuntimeError('Intel MPI recipe not implemented yet.')
268         else:
269             raise RuntimeError('Requested unknown MPI implementation.')
270     else:
271         return None
272
273
274 def get_clfft(args):
275     if (args.clfft is not None):
276         return hpccm.building_blocks.generic_cmake(
277             repository='https://github.com/clMathLibraries/clFFT.git',
278             prefix='/usr/local', recursive=True, branch=args.clfft, directory='clFFT/src')
279     else:
280         return None
281
282
283 def add_tsan_compiler_build_stage(input_args, output_stages: typing.Mapping[str, hpccm.Stage]):
284     """Isolate the expensive TSAN preparation stage.
285
286     This is a very expensive stage, but has few and disjoint dependencies, and
287     its output is easily compartmentalized (/usr/local) so we can isolate this
288     build stage to maximize build cache hits and reduce rebuild time, bookkeeping,
289     and final image size.
290     """
291     if not isinstance(output_stages, collections.abc.MutableMapping):
292         raise RuntimeError('Need output_stages container.')
293     tsan_stage = hpccm.Stage()
294     tsan_stage += hpccm.primitives.baseimage(image=base_image_tag(input_args), _as='tsan')
295
296     tsan_stage += hpccm.building_blocks.packages(ospackages=['git', 'ca-certificates', 'build-essential', 'cmake'])
297     # CMake will get duplicated later, but this is an expensive image, and it isn't worth optimizing
298     # out that duplication...
299     tsan_stage += hpccm.building_blocks.python(python3=True, python2=False, devel=False)
300
301     compiler_branch = 'release/' + str(input_args.llvm) + '.x'
302     tsan_stage += hpccm.building_blocks.generic_cmake(
303         repository='https://github.com/llvm/llvm-project.git',
304         directory='/var/tmp/llvm-project/llvm/',
305         prefix='/usr/local', recursive=True, branch=compiler_branch,
306         cmake_opts=['-D CMAKE_BUILD_TYPE=Release', '-D LLVM_ENABLE_PROJECTS="clang;openmp;clang-tools-extra;compiler-rt;lld"',
307                     '-D LIBOMP_TSAN_SUPPORT=on'],
308         postinstall=['ln -s /usr/local/bin/clang++ /usr/local/bin/clang++-' + str(input_args.llvm),
309                      'ln -s /usr/local/bin/clang-format /usr/local/bin/clang-format-' + str(input_args.llvm),
310                      'ln -s /usr/local/bin/clang-tidy /usr/local/bin/clang-tidy-' + str(input_args.llvm),
311                      'ln -s /usr/local/share/clang/run-clang-tidy.py /usr/local/bin/run-clang-tidy-' + str(input_args.llvm) + '.py',
312                      'ln -s /usr/local/bin/run-clang-tidy-' + str(input_args.llvm) + '.py /usr/local/bin/run-clang-tidy-' + str(input_args.llvm),
313                      'ln -s /usr/local/libexec/c++-analyzer /usr/local/bin/c++-analyzer-' + str(input_args.llvm)])
314     output_stages['compiler_build'] = tsan_stage
315
316 def oneapi_runtime(_from='0'):
317     oneapi_runtime_stage = hpccm.Stage()
318     oneapi_runtime_stage += hpccm.primitives.copy(_from='oneapi-build',
319                                                   files={"/opt/intel": "/opt/intel",
320                                                          "/etc/bash.bashrc": "/etc/bash.bashrc"})
321     return oneapi_runtime_stage
322
323 def add_oneapi_compiler_build_stage(input_args, output_stages: typing.Mapping[str, hpccm.Stage]):
324     """Isolate the oneAPI preparation stage.
325
326     This stage is isolated so that its installed components are minimized in the
327     final image (chiefly /opt/intel) and its environment setup script can be
328     sourced. This also helps with rebuild time and final image size.
329
330     Note that the ICC compiler inside oneAPI on linux also needs
331     gcc to build other components and provide libstdc++.
332     """
333     if not isinstance(output_stages, collections.abc.MutableMapping):
334         raise RuntimeError('Need output_stages container.')
335     oneapi_stage = hpccm.Stage()
336     oneapi_stage += hpccm.primitives.baseimage(image=base_image_tag(input_args), _as='oneapi-build')
337
338     version = str(input_args.oneapi)
339
340     # Add required components for the next stage (both for hpccm and Intel's setvars.sh script)
341     oneapi_stage += hpccm.building_blocks.packages(ospackages=['wget', 'gnupg2', 'ca-certificates', 'lsb-release'])
342     oneapi_stage += hpccm.building_blocks.packages(
343         apt_keys=['https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS-2023.PUB'],
344         apt_repositories=['deb https://apt.repos.intel.com/oneapi all main'],
345         # Add minimal packages (not the whole HPC toolkit!)
346         ospackages=['intel-oneapi-dpcpp-cpp-{}'.format(version),
347             'intel-oneapi-openmp-{}'.format(version),
348             'intel-oneapi-mkl-{}'.format(version),
349             'intel-oneapi-mkl-devel-{}'.format(version)]
350     )
351     # Ensure that all bash shells on the final container will have access to oneAPI
352     oneapi_stage += hpccm.primitives.shell(
353             commands=['echo "source /opt/intel/oneapi/setvars.sh" >> /etc/bash.bashrc']
354             )
355     setattr(oneapi_stage, 'runtime', oneapi_runtime)
356
357     output_stages['compiler_build'] = oneapi_stage
358
359 def prepare_venv(version: StrictVersion) -> typing.Sequence[str]:
360     """Get shell commands to set up the venv for the requested Python version."""
361     major = version.version[0]
362     minor = version.version[1]  # type: int
363
364     pyenv = '$HOME/.pyenv/bin/pyenv'
365
366     py_ver = '{}.{}'.format(major, minor)
367     venv_path = '$HOME/venv/py{}'.format(py_ver)
368     commands = ['$({pyenv} prefix `{pyenv} whence python{py_ver}`)/bin/python -m venv {path}'.format(
369         pyenv=pyenv,
370         py_ver=py_ver,
371         path=venv_path
372     )]
373
374     commands.append('{path}/bin/python -m pip install --upgrade pip setuptools'.format(
375         path=venv_path
376     ))
377     # Install dependencies for building and testing gmxapi Python package.
378     # WARNING: Please keep this list synchronized with python_packaging/requirements-test.txt
379     # TODO: Get requirements.txt from an input argument.
380     commands.append("""{path}/bin/python -m pip install --upgrade \
381             'cmake>=3.13' \
382             'flake8>=3.7.7' \
383             'mpi4py>=3.0.3' \
384             'networkx>=2.0' \
385             'numpy>=1' \
386             'pip>=10.1' \
387             'pytest>=3.9' \
388             'setuptools>=42' \
389             'scikit-build>=0.10'""".format(path=venv_path))
390
391     # TODO: Remove 'importlib_resources' dependency when Python >=3.7 is required.
392     if minor == 6:
393         commands.append("""{path}/bin/python -m pip install --upgrade \
394                 'importlib_resources'""".format(path=venv_path))
395
396     return commands
397
398
399 def add_python_stages(building_blocks: typing.Mapping[str, bb_base],
400                       input_args,
401                       output_stages: typing.MutableMapping[str, hpccm.Stage]):
402     """Add the stage(s) necessary for the requested venvs.
403
404     One intermediate build stage is created for each venv (see --venv option).
405
406     Each stage partially populates Python installations and venvs in the home
407     directory. The home directory is collected by the 'pyenv' stage for use by
408     the main build stage.
409     """
410     if len(input_args.venvs) < 1:
411         raise RuntimeError('No venvs to build...')
412     if output_stages is None or not isinstance(output_stages, collections.abc.Mapping):
413         raise RuntimeError('Need a container for output stages.')
414
415     # Main Python stage that collects the environments from individual stages.
416     # We collect the stages individually, rather than chaining them, because the
417     # copy is a bit slow and wastes local Docker image space for each filesystem
418     # layer.
419     pyenv_stage = hpccm.Stage()
420     pyenv_stage += hpccm.primitives.baseimage(image=base_image_tag(input_args), _as='pyenv')
421     pyenv_stage += building_blocks['compiler']
422     pyenv_stage += building_blocks['mpi']
423     pyenv_stage += hpccm.building_blocks.packages(ospackages=_python_extra_packages)
424
425     for version in [StrictVersion(py_ver) for py_ver in sorted(input_args.venvs)]:
426         stage_name = 'py' + str(version)
427         stage = hpccm.Stage()
428         stage += hpccm.primitives.baseimage(image=base_image_tag(input_args), _as=stage_name)
429         stage += building_blocks['compiler']
430         stage += building_blocks['mpi']
431         stage += hpccm.building_blocks.packages(ospackages=_python_extra_packages)
432
433         # TODO: Use a non-root user for testing and Python virtual environments.
434         stage += hpccm.primitives.shell(commands=[
435             'curl https://pyenv.run | bash',
436             """echo 'export PYENV_ROOT="$HOME/.pyenv"' >> $HOME/.bashrc""",
437             """echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> $HOME/.bashrc""",
438             """echo 'eval "$(pyenv init -)"' >> $HOME/.bashrc""",
439             """echo 'eval "$(pyenv virtualenv-init -)"' >> $HOME/.bashrc"""])
440         pyenv = '$HOME/.pyenv/bin/pyenv'
441         commands = ['PYTHON_CONFIGURE_OPTS="--enable-shared" {pyenv} install -s {version}'.format(
442             pyenv=pyenv,
443             version=str(version))]
444         stage += hpccm.primitives.shell(commands=commands)
445
446         commands = prepare_venv(version)
447         stage += hpccm.primitives.shell(commands=commands)
448
449         # TODO: Update user home directory.
450         pyenv_stage += hpccm.primitives.copy(_from=stage_name, _mkdir=True, src=['/root/'],
451                                              dest='/root')
452
453         # Add the intermediate build stage to the sequence
454         output_stages[stage_name] = stage
455
456     # TODO: If we activate pyenv for login shells, the `global` "version" should be full-featured.
457     # # `version` should be a system installation or pyenv environment (or pyenv-virtualenv)
458     # # with the dependencies for all of the Python aspects of CMake-driven builds.
459     # commands = '{pyenv} global {version}'.format(
460     #             pyenv=pyenv,
461     #             version=...)
462     # pyenv_stage += hpccm.primitives.shell(commands=commands)
463
464     # Add the aggregating build stage to the sequence. This allows the main stage to copy
465     # the files in a single stage, potentially reducing the overall output image size.
466     output_stages['pyenv'] = pyenv_stage
467
468
469 def add_documentation_dependencies(input_args,
470                                    output_stages: typing.MutableMapping[str, hpccm.Stage]):
471     """Add appropriate layers according to doxygen input arguments."""
472     if input_args.doxygen is None:
473         return
474     output_stages['main'] += hpccm.primitives.shell(
475         commands=['sed -i \'/\"XPS\"/d;/\"PDF\"/d;/\"PS\"/d;/\"EPS\"/d;/disable ghostscript format types/d\' /etc/ImageMagick-6/policy.xml'])
476     output_stages['main'] += hpccm.building_blocks.pip(pip='pip3', packages=['sphinx==1.6.1', 'gcovr'])
477     if input_args.doxygen == '1.8.5':
478         doxygen_commit = 'ed4ed873ab0e7f15116e2052119a6729d4589f7a'
479         output_stages['main'] += hpccm.building_blocks.generic_autotools(
480             repository='https://github.com/westes/flex.git',
481             commit='f7788a9a0ecccdc953ed12043ccb59ca25714018',
482             prefix='/tmp/install-of-flex',
483             configure_opts=['--disable-shared'],
484             preconfigure=['./autogen.sh'])
485         output_stages['main'] += hpccm.building_blocks.generic_autotools(
486             repository='https://github.com/doxygen/doxygen.git',
487             commit=doxygen_commit,
488             prefix='',
489             configure_opts=[
490                 '--flex /tmp/install-of-flex/bin/flex',
491                 '--static'])
492     else:
493         version = input_args.doxygen
494         archive_name = 'doxygen-{}.linux.bin.tar.gz'.format(version)
495         archive_url = 'https://sourceforge.net/projects/doxygen/files/rel-{}/{}'.format(
496             version,
497             archive_name
498         )
499         binary_path = 'doxygen-{}/bin/doxygen'.format(version)
500         commands = [
501             'mkdir doxygen && cd doxygen',
502             'wget {}'.format(archive_url),
503             'tar xf {} {}'.format(archive_name, binary_path),
504             'cp {} /usr/local/bin/'.format(binary_path),
505             'cd .. && rm -rf doxygen'
506         ]
507         output_stages['main'] += hpccm.primitives.shell(commands=commands)
508
509
510 def build_stages(args) -> typing.Iterable[hpccm.Stage]:
511     """Define and sequence the stages for the recipe corresponding to *args*."""
512
513     # A Dockerfile or Singularity recipe can have multiple build stages.
514     # The main build stage can copy files from previous stages, though only
515     # the last stage is included in the tagged output image. This means that
516     # large or expensive sets of build instructions can be isolated in
517     # local/temporary images, but all of the stages need to be output by this
518     # script, and need to occur in the correct order, so we create a sequence
519     # object early in this function.
520     stages = collections.OrderedDict()
521
522     # If we need TSAN or oneAPI support the early build is more complex,
523     # so that our compiler images don't have all the cruft needed to get those things
524     # installed.
525     if args.llvm is not None and args.tsan is not None:
526         add_tsan_compiler_build_stage(input_args=args, output_stages=stages)
527     if args.oneapi is not None:
528         add_oneapi_compiler_build_stage(input_args=args, output_stages=stages)
529
530     # Building blocks are chunks of container-builder instructions that can be
531     # copied to any build stage with the addition operator.
532     building_blocks = collections.OrderedDict()
533     building_blocks['base_packages'] = hpccm.building_blocks.packages(
534         ospackages=_common_packages)
535
536     # These are the most expensive and most reusable layers, so we put them first.
537     building_blocks['compiler'] = get_compiler(args, compiler_build_stage=stages.get('compiler_build'))
538     building_blocks['mpi'] = get_mpi(args, building_blocks['compiler'])
539     for i, cmake in enumerate(args.cmake):
540         building_blocks['cmake' + str(i)] = hpccm.building_blocks.cmake(
541             eula=True,
542             prefix='/usr/local/cmake-{}'.format(cmake),
543             version=cmake)
544
545     # Install additional packages early in the build to optimize Docker build layer cache.
546     os_packages = list(get_llvm_packages(args)) + get_opencl_packages(args)
547     if args.doxygen is not None:
548         os_packages += _docs_extra_packages
549     if args.oneapi is not None:
550         os_packages += ['lsb-release']
551     building_blocks['extra_packages'] = hpccm.building_blocks.packages(
552         ospackages=os_packages,
553         apt_ppas=['ppa:intel-opencl/intel-opencl'],
554         apt_keys=['http://repo.radeon.com/rocm/apt/debian/rocm.gpg.key'],
555         apt_repositories=['deb [arch=amd64] http://repo.radeon.com/rocm/apt/debian/ xenial main']
556     )
557
558     building_blocks['clfft'] = get_clfft(args)
559
560     # Add Python environments to MPI images, only, so we don't have to worry
561     # about whether to install mpi4py.
562     if args.mpi is not None and len(args.venvs) > 0:
563         add_python_stages(building_blocks=building_blocks, input_args=args, output_stages=stages)
564
565     # Create the stage from which the targeted image will be tagged.
566     stages['main'] = hpccm.Stage()
567
568     stages['main'] += hpccm.primitives.baseimage(image=base_image_tag(args))
569     for bb in building_blocks.values():
570         if bb is not None:
571             stages['main'] += bb
572
573     # We always add Python3 and Pip
574     stages['main'] += hpccm.building_blocks.python(python3=True, python2=False, devel=True)
575     stages['main'] += hpccm.building_blocks.pip(upgrade=True, pip='pip3',
576                                                 packages=['pytest', 'networkx', 'numpy'])
577
578     # Add documentation requirements (doxygen and sphinx + misc).
579     if args.doxygen is not None:
580         add_documentation_dependencies(args, stages)
581
582     if 'pyenv' in stages and stages['pyenv'] is not None:
583         stages['main'] += hpccm.primitives.copy(_from='pyenv', _mkdir=True, src=['/root/.pyenv/'],
584                                                 dest='/root/.pyenv')
585         stages['main'] += hpccm.primitives.copy(_from='pyenv', _mkdir=True, src=['/root/venv/'],
586                                                 dest='/root/venv')
587         # TODO: Update user home directory.
588         # TODO: If we activate pyenv for login shells, the `global` "version" should be full-featured.
589         # stages['main'] += hpccm.primitives.copy(_from='pyenv', src=['/root/.bashrc'],
590         #                                         dest='/root/')
591
592     # Make sure that `python` resolves to something.
593     stages['main'] += hpccm.primitives.shell(commands=['test -x /usr/bin/python || '
594                                                        'update-alternatives --install /usr/bin/python python /usr/bin/python3 1 && '
595                                                        '/usr/bin/python --version'])
596
597     # Note that the list of stages should be sorted in dependency order.
598     for build_stage in stages.values():
599         if build_stage is not None:
600             yield build_stage
601
602
603 if __name__ == '__main__':
604     args = parser.parse_args()
605
606     # Set container specification output format
607     hpccm.config.set_container_format(args.format)
608
609     container_recipe = build_stages(args)
610
611     # Output container specification
612     for stage in container_recipe:
613         print(stage)