Build shader-tools using clang

Change-Id: I119403158e831ad9ecfd586255b48879d95616df
diff --git a/build/tools/build-shader-tools.py b/build/tools/build-shader-tools.py
deleted file mode 100755
index 716ed00..0000000
--- a/build/tools/build-shader-tools.py
+++ /dev/null
@@ -1,150 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright (C) 2016 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Builds the glslc, spirv-as, spirv-dis, and spirv-val host executables."""
-
-from __future__ import print_function
-
-import os
-import site
-import shutil
-import subprocess
-
-site.addsitedir(os.path.join(os.path.dirname(__file__), '../lib'))
-site.addsitedir(os.path.join(os.path.dirname(__file__), '../..'))
-
-# pylint: disable=import-error,wrong-import-position
-import build_support
-from build_support import ArgParser
-import ndk.hosts
-# pylint: enable=import-error,wrong-import-position
-
-
-def main(args):
-    host_tag = build_support.host_to_tag(args.host)
-    build_host_tag = build_support.get_default_host().value + "-x86"
-
-    package_dir = args.dist_dir
-
-    # TODO(danalbert): use ndk/sources/third_party/googletest/googletest
-    # after it has been updated to a version with CMakeLists
-    gtest_dir = build_support.android_path('external', 'googletest')
-    gtest_cmd = f'-DSHADERC_GOOGLE_TEST_DIR={gtest_dir}'
-
-    # SPIRV-Tools tests require effcee and re2.
-    # Don't enable RE2 testing because it's long and not useful to us.
-    effcee_dir = build_support.android_path('external', 'effcee')
-    re2_dir = build_support.android_path('external', 'regex-re2')
-    effcee_args = [('-DSHADERC_EFFCEE_DIR=' + effcee_dir),
-                   ('-DSHADERC_RE2_DIR=' + re2_dir),
-                   ('-DEFFCEE_GOOGLETEST_DIR=' + gtest_dir),
-                   ('-DEFFCEE_RE2_DIR=' + re2_dir),
-                   ('-DRE2_BUILD_TESTING=OFF')]
-
-    obj_out = os.path.join(args.out_dir, 'shader_tools/obj')
-    install_dir = os.path.join(args.out_dir, 'shader_tools/install')
-
-    package_src = '-'.join([os.path.join(args.out_dir,
-                                         'shader_tools/shader-tools'),
-                            host_tag])
-    package_name = '-'.join(['shader-tools', host_tag])
-
-    source_root = build_support.android_path('external', 'shaderc')
-    shaderc_shaderc_dir = os.path.join(source_root, 'shaderc')
-    spirv_headers_dir = os.path.join(source_root, 'spirv-headers')
-
-    cmake = build_support.android_path('prebuilts', 'cmake',
-                                       build_host_tag, 'bin', 'cmake')
-    ctest = build_support.android_path('prebuilts', 'cmake',
-                                       build_host_tag, 'bin', 'ctest')
-    ninja = build_support.android_path('prebuilts', 'ninja',
-                                       build_host_tag, 'ninja')
-    file_extension = ''
-
-    additional_args = list(effcee_args)
-    if args.host.is_windows:
-        gtest_cmd = ''
-        mingw_root = os.path.join(build_support.android_path(),
-                                  'prebuilts', 'gcc', build_host_tag, 'host',
-                                  'x86_64-w64-mingw32-4.8')
-        mingw_compilers = os.path.join(mingw_root, 'bin', 'x86_64-w64-mingw32')
-        mingw_toolchain = os.path.join(source_root, 'shaderc',
-                                       'cmake', 'linux-mingw-toolchain.cmake')
-        gtest_root = build_support.android_path('external', 'googletest')
-        additional_args.extend(['-DCMAKE_TOOLCHAIN_FILE=' + mingw_toolchain,
-                                '-DMINGW_SYSROOT=' + mingw_root,
-                                '-DMINGW_COMPILER_PREFIX=' + mingw_compilers,
-                                '-DSHADERC_GOOGLE_TEST_DIR=' + gtest_root])
-        file_extension = '.exe'
-        if args.host == ndk.hosts.Host.Windows64:
-            additional_args.extend(
-                ['-DCMAKE_CXX_FLAGS=-fno-rtti -fno-exceptions'])
-        else:
-            additional_args.extend(
-                ['-DCMAKE_CXX_FLAGS=-m32 -fno-rtti -fno-exceptions',
-                 '-DCMAKE_C_FLAGS=-m32'])
-
-    for d in [package_src, obj_out, install_dir]:
-        try:
-            os.makedirs(d)
-        except:
-            pass
-
-    cmake_command = [cmake, '-GNinja', '-DCMAKE_MAKE_PROGRAM=' + ninja,
-                     '-DCMAKE_BUILD_TYPE=Release',
-                     '-DCMAKE_INSTALL_PREFIX=' + install_dir,
-                     '-DSHADERC_THIRD_PARTY_ROOT_DIR=' + source_root,
-                     '-DSPIRV-Headers_SOURCE_DIR=' + spirv_headers_dir,
-                     gtest_cmd,
-                     shaderc_shaderc_dir]
-
-    cmake_command.extend(additional_args)
-
-    subprocess.check_call(cmake_command, cwd=obj_out)
-    subprocess.check_call([cmake, '--build', obj_out, '--', '-v'])
-    subprocess.check_call([cmake, '--build', obj_out,
-                           '--target', 'install/strip'])
-
-    files_to_copy = ['glslc' + file_extension,
-                     'spirv-as' + file_extension,
-                     'spirv-dis' + file_extension,
-                     'spirv-val' + file_extension,
-                     'spirv-cfg' + file_extension,
-                     'spirv-opt' + file_extension,
-                     'spirv-link' + file_extension,
-                     'spirv-reduce' + file_extension]
-    scripts_to_copy = ['spirv-lesspipe.sh',]
-    files_to_copy.extend(scripts_to_copy)
-
-    # Test, except on windows.
-    if not args.host.is_windows:
-        subprocess.check_call([ctest, '--verbose'], cwd=obj_out)
-
-    # Copy to install tree.
-    for src in files_to_copy:
-        shutil.copy2(os.path.join(install_dir, 'bin', src),
-                     os.path.join(package_src, src))
-    if args.host.is_windows:
-        for src in scripts_to_copy:
-            # Convert line endings on scripts.
-            # Do it in place to preserve executable permissions.
-            subprocess.check_call(['unix2dos', '-o',
-                                   os.path.join(package_src, src)])
-
-    build_support.make_package(package_name, package_src, package_dir)
-
-if __name__ == '__main__':
-    build_support.run(main, ArgParser)
diff --git a/ndk/builds.py b/ndk/builds.py
index 336d2cb..ed011c5 100644
--- a/ndk/builds.py
+++ b/ndk/builds.py
@@ -23,6 +23,7 @@
 # https://github.com/PyCQA/pylint/issues/73
 from distutils.dir_util import copy_tree
 from enum import auto, Enum, unique
+from ndk.cmake import CMakeBuilder
 import os
 from pathlib import Path, PureWindowsPath
 import shutil
@@ -333,6 +334,48 @@
             str(install_dir))
 
 
+class CMakeModule(Module):
+    # Path to the source code
+    src: Path
+    _builder: Optional[CMakeBuilder] = None
+    run_ctest: bool = False
+
+    @property
+    def builder(self) -> CMakeBuilder:
+        """Returns the lazily initialized builder for this module."""
+        if self._builder is None:
+            self._builder = CMakeBuilder(
+                self.src,
+                self.intermediate_out_dir,
+                self.host,
+                additional_flags=self.flags,
+                additional_env=self.env,
+                run_ctest=self.run_ctest)
+        return self._builder
+
+    @property
+    def env(self) -> Dict[str, str]:
+        return dict()
+
+    @property
+    def flags(self) -> List[str]:
+        return []
+
+    @property
+    def defines(self) -> Dict[str, str]:
+        return dict()
+
+    def build(self) -> None:
+        self.builder.build(self.defines)
+
+    def install(self) -> None:
+        install_dir = self.get_install_path()
+        install_dir.mkdir(parents=True, exist_ok=True)
+        copy_tree(
+            str(self.builder.install_directory),
+            str(install_dir))
+
+
 class PackageModule(Module):
     """A directory to be installed to the NDK.
 
diff --git a/ndk/checkbuild.py b/ndk/checkbuild.py
index dd68104..75e5616 100755
--- a/ndk/checkbuild.py
+++ b/ndk/checkbuild.py
@@ -612,11 +612,13 @@
     ]
 
 
-class ShaderTools(ndk.builds.InvokeBuildModule):
+class ShaderTools(ndk.builds.CMakeModule):
     name = 'shader-tools'
+    src = ANDROID_DIR / 'external' / 'shaderc' / 'shaderc'
     path = Path('shader-tools/{host}')
-    script = Path('build-shader-tools.py')
+    run_ctest = True
     notice_group = ndk.builds.NoticeGroup.TOOLCHAIN
+    deps = {'clang'}
 
     @property
     def notices(self) -> Iterator[Path]:
@@ -629,6 +631,92 @@
         yield glslang_dir / 'LICENSE.txt'
         yield spirv_dir / 'LICENSE'
 
+    @property
+    def defines(self) -> Dict[str, str]:
+        gtest_dir = ANDROID_DIR / 'external' / 'googletest'
+        effcee_dir = ANDROID_DIR / 'external' / 'effcee'
+        re2_dir = ANDROID_DIR / 'external' / 'regex-re2'
+        spirv_headers_dir = self.src.parent / 'spirv-headers'
+        defines = {
+            'SHADERC_EFFCEE_DIR': str(effcee_dir),
+            'SHADERC_RE2_DIR': str(re2_dir),
+            'SHADERC_GOOGLE_TEST_DIR': str(gtest_dir),
+            'SHADERC_THIRD_PARTY_ROOT_DIR': str(self.src.parent),
+            'EFFCEE_GOOGLETEST_DIR': str(gtest_dir),
+            'EFFCEE_RE2_DIR': str(re2_dir),
+            # SPIRV-Tools tests require effcee and re2.
+            # Don't enable RE2 testing because it's long and not useful to us.
+            'RE2_BUILD_TESTING': 'OFF',
+            'SPIRV-Headers_SOURCE_DIR': str(spirv_headers_dir),
+        }
+        return defines
+
+    @property
+    def flags(self) -> List[str]:
+        return super().flags + [
+            '-Wno-unused-command-line-argument',
+            '-fno-rtti',
+            '-fno-exceptions',
+        ]
+
+    @property
+    def env(self) -> Dict[str, str]:
+        # Sets path for libc++, for ctest.
+        if self.host == Host.Linux:
+            return {'LD_LIBRARY_PATH': str(self._libcxx_dir)}
+        elif self.host == Host.Darwin:
+            return {'DYLD_LIBRARY_PATH': str(self._libcxx_dir)}
+        return {}
+
+    @property
+    def _libcxx_dir(self) -> List[Path]:
+        return self.get_dep('clang').get_build_host_install() / 'lib64'
+
+    @property
+    def _libcxx(self) -> List[Path]:
+        path = self._libcxx_dir
+        if self.host == Host.Linux:
+            return [
+                path / 'libc++.so.1',
+                path / 'libc++abi.so.1',
+            ]
+        elif self.host == Host.Darwin:
+            return [
+                path / 'libc++.1.dylib',
+                path / 'libc++abi.1.dylib',
+            ]
+        return []
+
+    def install(self) -> None:
+        self.get_install_path().mkdir(parents=True, exist_ok=True)
+        ext = '.exe' if self.host.is_windows else ''
+        files_to_copy = [
+            f'glslc{ext}', f'spirv-as{ext}', f'spirv-dis{ext}',
+            f'spirv-val{ext}', f'spirv-cfg{ext}', f'spirv-opt{ext}',
+            f'spirv-link{ext}', f'spirv-reduce{ext}'
+        ]
+        scripts_to_copy = ['spirv-lesspipe.sh']
+
+        # Copy to install tree.
+        for src in files_to_copy + scripts_to_copy:
+            shutil.copy2(self.builder.install_directory / 'bin' / src,
+                         self.get_install_path())
+
+        if self.host.is_windows:
+            for src in scripts_to_copy:
+                # Convert line endings on scripts.
+                # Do it in place to preserve executable permissions.
+                subprocess.check_call(
+                    ['unix2dos', '-o',
+                     self.get_install_path() / src])
+
+        # Symlink libc++ to install path.
+        for lib in self._libcxx:
+            symlink_name = self.get_install_path() / lib.name
+            symlink_name.unlink(missing_ok=True)
+            symlink_name.symlink_to(
+                Path(os.path.relpath(lib, symlink_name.parent)))
+
 
 class Make(ndk.builds.AutoconfModule):
     name = 'make'
diff --git a/ndk/cmake.py b/ndk/cmake.py
index 98471aa..f9c40b7 100644
--- a/ndk/cmake.py
+++ b/ndk/cmake.py
@@ -49,8 +49,9 @@
                  src_path: Path,
                  build_dir: Path,
                  host: Host,
-                 additional_flags: List[str] = None,
-                 additional_env: Optional[Dict[str, str]] = None) -> None:
+                 additional_flags: Optional[List[str]] = None,
+                 additional_env: Optional[Dict[str, str]] = None,
+                 run_ctest: bool = False) -> None:
         """Initializes an autoconf builder.
 
         Args:
@@ -68,6 +69,7 @@
         self.host = host
         self.additional_flags = additional_flags
         self.additional_env = additional_env
+        self.run_ctest = run_ctest
 
         self.working_directory = self.build_directory / 'build'
         self.install_directory = self.build_directory / 'install'
@@ -116,6 +118,11 @@
                 (get_default_host().value + '-x86') / 'ninja')
 
     @property
+    def _ctest(self) -> Path:
+        return (ndk.paths.ANDROID_DIR / 'prebuilts' / 'cmake' /
+                (get_default_host().value + '-x86') / 'bin' / 'ctest')
+
+    @property
     def cmake_defines(self) -> Dict[str, str]:
         """CMake defines."""
         flags = self.toolchain.flags + self.flags
@@ -175,9 +182,13 @@
         """Builds the project."""
         self._run([str(self._ninja)])
 
+    def test(self) -> None:
+        """Runs tests."""
+        self._run([str(self._ctest), '--verbose'])
+
     def install(self) -> None:
         """Installs the project."""
-        self._run([str(self._ninja), 'install'])
+        self._run([str(self._ninja), 'install/strip'])
 
     def build(self,
               additional_defines: Optional[Dict[str, str]] = None) -> None:
@@ -192,4 +203,6 @@
         self.configure(
             {} if additional_defines is None else additional_defines)
         self.make()
+        if not self.host.is_windows and self.run_ctest:
+            self.test()
         self.install()