Repository: NVIDIA/hpc-container-maker

Issue:
Add ability to include llvm tools (clang-tidy/format)
Currently, adding the llvm building block only adds the base compiler to the image.
It would be great if it was also possible to add the corresponding tools as well, to allow the generated image to be used for source code linting.

Discussion:
samcmill: @acmnpv does this address #238?
samcmill: I should be releasing HPCCM version 20.3 sometime this week.

Patch:
diff --git a/docs/building_blocks.md b/docs/building_blocks.md
index 79a20c8..4d8b12d 100644
--- a/docs/building_blocks.md
+++ b/docs/building_blocks.md
@@ -2007,6 +2007,9 @@ packages.  For Ubuntu, setting this flag to True enables the
 distributions, setting this flag to True enables the Software
 Collections (SCL) repository.  The default is False.
 
+- __extra_tools__: Boolean flag to specify whether to also install
+`clang-format` and `clang-tidy`.  The default is False.
+
 - __version__: The version of the LLVM compilers to install.  Note that
 the version refers to the Linux distribution packaging, not the
 actual compiler version.  For Ubuntu, the version is appended to
diff --git a/hpccm/building_blocks/llvm.py b/hpccm/building_blocks/llvm.py
index 06e3f87..be8632c 100644
--- a/hpccm/building_blocks/llvm.py
+++ b/hpccm/building_blocks/llvm.py
@@ -21,6 +21,7 @@ from __future__ import unicode_literals
 from __future__ import print_function
 
 from distutils.version import StrictVersion
+import logging
 
 import hpccm.config
 import hpccm.templates.envvars
@@ -54,6 +55,9 @@ class llvm(bb_base, hpccm.templates.envvars):
     distributions, setting this flag to True enables the Software
     Collections (SCL) repository.  The default is False.
 
+    extra_tools: Boolean flag to specify whether to also install
+    `clang-format` and `clang-tidy`.  The default is False.
+
     version: The version of the LLVM compilers to install.  Note that
     the version refers to the Linux distribution packaging, not the
     actual compiler version.  For Ubuntu, the version is appended to
@@ -90,6 +94,7 @@ class llvm(bb_base, hpccm.templates.envvars):
         self.__compiler_debs = []  # Filled in below
         self.__compiler_rpms = []  # Filled in below
         self.__extra_repo = kwargs.get('extra_repository', False)
+        self.__extra_tools = kwargs.get('extra_tools', False)
         self.__ospackages = kwargs.get('ospackages', []) # Filled in below
         self.__runtime_debs = []   # Filled in below
         self.__runtime_rpms = []   # Filled in below
@@ -123,10 +128,22 @@ class llvm(bb_base, hpccm.templates.envvars):
                 # version is the new default
                 self.__commands.append('update-alternatives --install /usr/bin/clang clang $(which clang-{}) 30'.format(self.__version))
                 self.__commands.append('update-alternatives --install /usr/bin/clang++ clang++ $(which clang++-{}) 30'.format(self.__version))
+
+                # Install and configure clang-format and clang-tidy
+                if self.__extra_tools:
+                    self.__compiler_debs.append('clang-format-{}'.format(
+                        self.__version))
+                    self.__compiler_debs.append('clang-tidy-{}'.format(
+                        self.__version))
+                    self.__commands.append('update-alternatives --install /usr/bin/clang-format clang-format $(which clang-format-{}) 30'.format(self.__version))
+                    self.__commands.append('update-alternatives --install /usr/bin/clang-tidy clang-tidy $(which clang-tidy-{}) 30'.format(self.__version))
             else:
                 self.__compiler_debs = ['clang', 'libomp-dev']
                 self.__runtime_debs = ['libclang1', 'libomp5']
 
+                if self.__extra_tools:
+                    self.__compiler_debs.extend(['clang-format', 'clang-tidy'])
+
         elif hpccm.config.g_linux_distro == linux_distro.CENTOS:
             # Dependencies on the GNU compiler
             self.__ospackages = ['gcc', 'gcc-c++']
@@ -140,6 +157,9 @@ class llvm(bb_base, hpccm.templates.envvars):
                     self.__compiler_rpms = ['llvm-toolset-8.0.1']
                     self.__runtime_rpms = ['llvm-libs-8.0.1', 'libomp']
                     compiler_version = '8'
+
+                    if self.__extra_tools:
+                        self.__compiler_rpms.append('clang-tools-extra-8.0.1')
                 else:
                     # CentOS 7
                     self.__compiler_rpms = [
@@ -151,6 +171,9 @@ class llvm(bb_base, hpccm.templates.envvars):
                         'llvm-toolset-{}-compiler-rt'.format(self.__version)]
                     compiler_version = '4.8.2'
 
+                    if self.__extra_tools:
+                        self.__compiler_rpms.append('llvm-toolset-{}-clang-tools-extra'.format(self.__version))
+
                     # Setup environment for devtoolset
                     self.environment_variables['PATH'] = '/opt/rh/llvm-toolset-{}/root/usr/bin:$PATH'.format(self.__version)
                     self.environment_variables['LD_LIBRARY_PATH'] = '/opt/rh/llvm-toolset-{}/root/usr/lib64:$LD_LIBRARY_PATH'.format(self.__version)
@@ -160,11 +183,17 @@ class llvm(bb_base, hpccm.templates.envvars):
                     # CentOS 8
                     self.__runtime_rpms = ['llvm-libs', 'libomp']
                     compiler_version = '8'
+
+                    if self.__extra_tools:
+                        self.__compiler_rpms.append('clang-tools-extra')
                 else:
                     # CentOS 7
                     self.__runtime_rpms = ['llvm-libs', 'libgomp']
                     compiler_version = '4.8.2'
 
+                    if self.__extra_tools: # pragma: no cover
+                        logging.warning('llvm extra tools are not available for default CentOS 7, specify a LLVM version')
+
             # The default llvm configuration for CentOS is unable to
             # locate some gcc components. Setup the necessary gcc
             # environment.


Test patch:
diff --git a/test/test_llvm.py b/test/test_llvm.py
index b683eec..3803656 100644
--- a/test/test_llvm.py
+++ b/test/test_llvm.py
@@ -84,23 +84,27 @@ ENV CPATH=/usr/lib/gcc/x86_64-redhat-linux/8/include:$CPATH''')
     @docker
     def test_version_ubuntu(self):
         """LLVM compiler version"""
-        l = llvm(extra_repository=True, version='6.0')
+        l = llvm(extra_repository=True, extra_tools=True, version='6.0')
         self.assertEqual(str(l),
 r'''# LLVM compiler
 RUN apt-get update -y && \
     DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
         clang-6.0 \
+        clang-format-6.0 \
+        clang-tidy-6.0 \
         libomp-dev && \
     rm -rf /var/lib/apt/lists/*
 RUN update-alternatives --install /usr/bin/clang clang $(which clang-6.0) 30 && \
-    update-alternatives --install /usr/bin/clang++ clang++ $(which clang++-6.0) 30''')
+    update-alternatives --install /usr/bin/clang++ clang++ $(which clang++-6.0) 30 && \
+    update-alternatives --install /usr/bin/clang-format clang-format $(which clang-format-6.0) 30 && \
+    update-alternatives --install /usr/bin/clang-tidy clang-tidy $(which clang-tidy-6.0) 30''')
 
     @x86_64
     @centos
     @docker
     def test_version_centos(self):
         """LLVM compiler version"""
-        l = llvm(extra_repository=True, version='7')
+        l = llvm(extra_repository=True, extra_tools=True, version='7')
         self.assertEqual(str(l),
 r'''# LLVM compiler
 RUN yum install -y \
@@ -110,6 +114,7 @@ RUN yum install -y \
 RUN yum install -y centos-release-scl && \
     yum install -y \
         llvm-toolset-7-clang \
+        llvm-toolset-7-clang-tools-extra \
         llvm-toolset-7-libomp-devel && \
     rm -rf /var/cache/yum/*
 ENV CPATH=/usr/lib/gcc/x86_64-redhat-linux/4.8.2/include:$CPATH \
@@ -162,6 +167,40 @@ ENV COMPILER_PATH=/usr/lib/gcc/aarch64-redhat-linux/8:$COMPILER_PATH \
         with self.assertRaises(RuntimeError):
             llvm()
 
+    @x86_64
+    @ubuntu
+    @docker
+    def test_extra_tools_ubuntu(self):
+        """clang-format and clang-tidy"""
+        l = llvm(extra_tools=True)
+        self.assertEqual(str(l),
+r'''# LLVM compiler
+RUN apt-get update -y && \
+    DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
+        clang \
+        clang-format \
+        clang-tidy \
+        libomp-dev && \
+    rm -rf /var/lib/apt/lists/*''')
+
+    @x86_64
+    @centos8
+    @docker
+    def test_extra_tools_centos8(self):
+        """Default llvm building block"""
+        l = llvm(extra_tools=True, version='8')
+        self.assertEqual(str(l),
+r'''# LLVM compiler
+RUN yum install -y \
+        gcc \
+        gcc-c++ && \
+    rm -rf /var/cache/yum/*
+RUN yum install -y \
+        clang-tools-extra-8.0.1 \
+        llvm-toolset-8.0.1 && \
+    rm -rf /var/cache/yum/*
+ENV CPATH=/usr/lib/gcc/x86_64-redhat-linux/8/include:$CPATH''')
+
     @x86_64
     @ubuntu
     @docker