From 9fd0a50a6fcb5891eb4b363db7f61395d9912b21 Mon Sep 17 00:00:00 2001 From: Tom Finegan Date: Wed, 2 Feb 2022 16:05:53 -0800 Subject: [PATCH] Add install target testing. (#796) Add a script that builds and installs Draco in shared and static configurations, and confirms that a simple test application can compile, link and run successfully using the CMake configuration provided by each Draco installation. To run the test script: cd draco/src/draco/tools/install_test python3 test.py By default the script runs silently using the default generator for the CMake executable in the user's path. Verbose output is behind the usual flags (`-v`). The CMake generator can be specified using the `-G` argument. The script is known to work with the following generators: - Unix Makefiles - MSVC (Visual Studio 16 2019) - Xcode --- cmake/draco-config.cmake.template | 7 +- src/draco/tools/install_test/CMakeLists.txt | 56 ++++ src/draco/tools/install_test/main.cc | 30 ++ src/draco/tools/install_test/test.py | 319 ++++++++++++++++++++ 4 files changed, 410 insertions(+), 2 deletions(-) create mode 100644 src/draco/tools/install_test/CMakeLists.txt create mode 100644 src/draco/tools/install_test/main.cc create mode 100755 src/draco/tools/install_test/test.py diff --git a/cmake/draco-config.cmake.template b/cmake/draco-config.cmake.template index 913b1b0..cc5a8f7 100644 --- a/cmake/draco-config.cmake.template +++ b/cmake/draco-config.cmake.template @@ -1,4 +1,7 @@ @PACKAGE_INIT@ set_and_check(DRACO_INCLUDE_DIR "@CMAKE_INSTALL_FULL_INCLUDEDIR@") -set_and_check(DRACO_LIB_DIR "@CMAKE_INSTALL_FULL_LIBDIR@") -set_and_check(DRACO_LIBRARY "draco") +set_and_check(DRACO_LIBRARY_DIR "@CMAKE_INSTALL_FULL_LIBDIR@") +set(DRACO_NAMES draco.dll libdraco.dylib libdraco.so draco.lib libdraco.a) +find_library(_DRACO_LIBRARY PATHS ${DRACO_LIBRARY_DIR} NAMES ${DRACO_NAMES}) +set_and_check(DRACO_LIBRARY ${_DRACO_LIBRARY}) +set(DRACO_FOUND YES) diff --git a/src/draco/tools/install_test/CMakeLists.txt b/src/draco/tools/install_test/CMakeLists.txt new file mode 100644 index 0000000..e98c8ce --- /dev/null +++ b/src/draco/tools/install_test/CMakeLists.txt @@ -0,0 +1,56 @@ +# Copyright 2022 The Draco Authors +# +# 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. + +cmake_minimum_required(VERSION 3.12 FATAL_ERROR) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_CXX_STANDARD 11) +project(install_test C CXX) + +include(GNUInstallDirs) + +# Tell find_package() where Draco is installed. +if(BUILD_SHARED_LIBS) + set(CMAKE_PREFIX_PATH + "${CMAKE_CURRENT_LIST_DIR}/_draco_install_shared/share/cmake") +else() + set(CMAKE_PREFIX_PATH + "${CMAKE_CURRENT_LIST_DIR}/_draco_install_static/share/cmake") +endif() + +find_package(draco REQUIRED) + +if(BUILD_SHARED_LIBS) + # Add rpath to resolve dylib dependency on Linux/MacOS. + list(APPEND CMAKE_BUILD_RPATH "${DRACO_LIBRARY_DIR}") + list(APPEND CMAKE_INSTALL_RPATH "${DRACO_LIBRARY_DIR}") +endif() + +list(APPEND install_test_sources "main.cc") +add_executable(install_check ${install_test_sources}) + +# Update include paths and dependencies to allow for successful build of the +# install_check target using Draco as configured for the current installation. +target_include_directories(install_check PUBLIC "${DRACO_INCLUDE_DIR}") +target_link_libraries(install_check "${DRACO_LIBRARY}") + +install(TARGETS install_check DESTINATION "${CMAKE_INSTALL_FULL_BINDIR}") + +if(BUILD_SHARED_LIBS AND WIN32) + # Copy the Draco DLL into the bin dir for Windows: Windows doesn't really have + # a concept of rpath, but it does look in the current directory by default + # when a program tries to load a DLL. + install(FILES "${DRACO_LIBRARY_DIR}/draco.dll" + DESTINATION "${CMAKE_INSTALL_FULL_BINDIR}") +endif() diff --git a/src/draco/tools/install_test/main.cc b/src/draco/tools/install_test/main.cc new file mode 100644 index 0000000..cddcdc1 --- /dev/null +++ b/src/draco/tools/install_test/main.cc @@ -0,0 +1,30 @@ +// Copyright 2022 The Draco Authors. +// +// 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. +// + +// This program is used to test the installed version of Draco. It does just +// enough to confirm that an application using Draco can compile and link +// against an installed version of Draco without errors. It does not perform +// any sort of library tests. + +#include + +#include "draco/core/decoder_buffer.h" + +int main(int /*argc*/, char** /*argv*/) { + std::vector empty_buffer; + draco::DecoderBuffer buffer; + buffer.Init(empty_buffer.data(), empty_buffer.size()); + return 0; +} diff --git a/src/draco/tools/install_test/test.py b/src/draco/tools/install_test/test.py new file mode 100755 index 0000000..45f2f9d --- /dev/null +++ b/src/draco/tools/install_test/test.py @@ -0,0 +1,319 @@ +#!/usr/bin/python3 +# +# Copyright 2022 The Draco Authors +# +# 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. +"""Tests installations of the Draco library. + +Builds the library in shared and static configurations on the current host +system, and then confirms that a simple test application can link in both +configurations. +""" + +import argparse +import multiprocessing +import os +import pathlib +import shlex +import shutil +import subprocess + +# CMake executable. +CMAKE = shutil.which('cmake') + +# List of generators available in the current CMake executable. +CMAKE_AVAILABLE_GENERATORS = [] + +# CMake builds use the specified generator. +CMAKE_GENERATOR = None + +# The Draco tree that this script uses. +DRACO_SOURCES_PATH = os.path.abspath(os.path.join('..', '..', '..', '..')) + +# Path to this script and the rest of the test project files. +TEST_SOURCES_PATH = os.path.dirname(os.path.abspath(__file__)) + +# The Draco build directories. +DRACO_SHARED_BUILD_PATH = os.path.join(TEST_SOURCES_PATH, '_draco_build_shared') +DRACO_STATIC_BUILD_PATH = os.path.join(TEST_SOURCES_PATH, '_draco_build_static') + +# The Draco install roots. +DRACO_SHARED_INSTALL_PATH = os.path.join(TEST_SOURCES_PATH, + '_draco_install_shared') +DRACO_STATIC_INSTALL_PATH = os.path.join(TEST_SOURCES_PATH, + '_draco_install_static') + +# Argument for -j when using make, or -m when using Visual Studio. Number of +# build jobs. +NUM_PROCESSES = multiprocessing.cpu_count() - 1 + +# The test project build directories. +TEST_SHARED_BUILD_PATH = os.path.join(TEST_SOURCES_PATH, '_test_build_shared') +TEST_STATIC_BUILD_PATH = os.path.join(TEST_SOURCES_PATH, '_test_build_static') + +# The test project install directories. +TEST_SHARED_INSTALL_PATH = os.path.join(TEST_SOURCES_PATH, + '_test_install_shared') +TEST_STATIC_INSTALL_PATH = os.path.join(TEST_SOURCES_PATH, + '_test_install_static') + +# Show configuration and build output. +VERBOSE = False + + +def cmake_get_available_generators(): + """Returns list of generators available in current CMake executable.""" + result = run_process_and_capture_output(f'{CMAKE} --help') + + if result[0] != 0: + raise Exception(f'cmake --help failed, exit code: {result[0]}\n{result[1]}') + + help_text = result[1].splitlines() + generators_start_index = help_text.index('Generators') + 3 + generators_text = help_text[generators_start_index::] + + generators = [] + for gen in generators_text: + gen = gen.split('=')[0].strip().replace('* ', '') + + if gen and gen[0] != '=': + generators.append(gen) + + return generators + + +def cmake_get_generator(): + """Returns the CMake generator from CMakeCache.txt in the current dir.""" + cmake_cache_file_path = os.path.join(os.getcwd(), 'CMakeCache.txt') + cmake_cache_text = '' + with open(cmake_cache_file_path, 'r') as cmake_cache_file: + cmake_cache_text = cmake_cache_file.read() + + if not cmake_cache_text: + raise FileNotFoundError(f'{cmake_cache_file_path} missing or empty.') + + generator = '' + for line in cmake_cache_text.splitlines(): + if line.startswith('CMAKE_GENERATOR:INTERNAL='): + generator = line.split('=')[1] + + return generator + + +def run_process_and_capture_output(cmd, env=None): + """Runs |cmd| as a child process. + + Returns process exit code and output. + + Args: + cmd: String containing the command to execute. + env: Optional dict of environment variables. + + Returns: + Tuple of exit code and output. + """ + if not cmd: + raise ValueError('run_process_and_capture_output requires cmd argument.') + + if os.name == 'posix': + # On posix systems subprocess.Popen will treat |cmd| as the program name + # when it is passed as a string. Unconditionally split the command so + # callers don't need to care about this detail. + cmd = shlex.split(cmd) + + proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) + stdout = proc.communicate() + return [proc.returncode, stdout[0].decode('utf-8')] + + +def create_output_directories(): + """Creates the build output directores for the test.""" + pathlib.Path(DRACO_SHARED_BUILD_PATH).mkdir(parents=True, exist_ok=True) + pathlib.Path(DRACO_STATIC_BUILD_PATH).mkdir(parents=True, exist_ok=True) + pathlib.Path(TEST_SHARED_BUILD_PATH).mkdir(parents=True, exist_ok=True) + pathlib.Path(TEST_STATIC_BUILD_PATH).mkdir(parents=True, exist_ok=True) + + +def cleanup(): + """Removes the build output directories from the test.""" + shutil.rmtree(DRACO_SHARED_BUILD_PATH) + shutil.rmtree(DRACO_STATIC_BUILD_PATH) + shutil.rmtree(DRACO_SHARED_INSTALL_PATH) + shutil.rmtree(DRACO_STATIC_INSTALL_PATH) + shutil.rmtree(TEST_SHARED_BUILD_PATH) + shutil.rmtree(TEST_STATIC_BUILD_PATH) + shutil.rmtree(TEST_SHARED_INSTALL_PATH) + shutil.rmtree(TEST_STATIC_INSTALL_PATH) + + +def cmake_configure(source_path, cmake_args=None): + """Configures a CMake build.""" + command = f'{CMAKE} {source_path}' + + if CMAKE_GENERATOR: + command += f' -G {CMAKE_GENERATOR}' + + if cmake_args: + for arg in cmake_args: + command += f' {arg}' + + if VERBOSE: + print(f'CONFIGURE command:\n{command}') + + result = run_process_and_capture_output(command) + + if result[0] != 0: + raise Exception(f'CONFIGURE failed!\nexit_code: {result[0]}\n{result[1]}') + + if VERBOSE: + print(f'CONFIGURE result:\nexit_code: {result[0]}\n{result[1]}') + + +def cmake_build(cmake_args=None, build_args=None): + """Runs a CMake build.""" + command = f'{CMAKE} --build .' + + if cmake_args: + for arg in cmake_args: + command += f' {arg}' + + if not build_args: + build_args = [] + + generator = cmake_get_generator() + if generator.endswith('Makefiles'): + build_args.append(f'-j {NUM_PROCESSES}') + elif generator.startswith('Visual'): + build_args.append(f'-m:{NUM_PROCESSES}') + + if build_args: + command += ' --' + for arg in build_args: + command += f' {arg}' + + if VERBOSE: + print(f'BUILD command:\n{command}') + + result = run_process_and_capture_output(f'{command}') + + if result[0] != 0: + raise Exception(f'BUILD failed!\nexit_code: {result[0]}\n{result[1]}') + + if VERBOSE: + print(f'BUILD result:\nexit_code: {result[0]}\n{result[1]}') + + +def run_install_check(install_path): + """Runs the install_check program.""" + cmd = os.path.join(install_path, 'bin', 'install_check') + result = run_process_and_capture_output(cmd) + + if result[0] != 0: + raise Exception( + f'install_check run failed!\nexit_code: {result[0]}\n{result[1]}') + + +def build_and_install_draco(): + """Builds Draco in shared and static configurations.""" + orig_dir = os.getcwd() + + # Build and install Draco in shared library config for the current host + # machine. + os.chdir(DRACO_SHARED_BUILD_PATH) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_SHARED_INSTALL_PATH}') + cmake_args.append('-DBUILD_SHARED_LIBS=ON') + cmake_configure(source_path=DRACO_SOURCES_PATH, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + # Build and install Draco in the static config for the current host machine. + os.chdir(DRACO_STATIC_BUILD_PATH) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_STATIC_INSTALL_PATH}') + cmake_args.append('-DBUILD_SHARED_LIBS=OFF') + cmake_configure(source_path=DRACO_SOURCES_PATH, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + os.chdir(orig_dir) + + +def build_test_project(): + """Builds the test application in shared and static configurations.""" + orig_dir = os.getcwd() + + # Configure the test project in shared mode and build it. + os.chdir(TEST_SHARED_BUILD_PATH) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={TEST_SHARED_INSTALL_PATH}') + cmake_args.append('-DBUILD_SHARED_LIBS=ON') + cmake_configure(source_path=f'{TEST_SOURCES_PATH}', cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + run_install_check(TEST_SHARED_INSTALL_PATH) + + # Configure in static mode and build it. + os.chdir(TEST_STATIC_BUILD_PATH) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={TEST_STATIC_INSTALL_PATH}') + cmake_args.append('-DBUILD_SHARED_LIBS=OFF') + cmake_configure(source_path=f'{TEST_SOURCES_PATH}', cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + run_install_check(TEST_STATIC_INSTALL_PATH) + + os.chdir(orig_dir) + + +def test_draco_install(): + create_output_directories() + build_and_install_draco() + build_test_project() + cleanup() + + +if __name__ == '__main__': + CMAKE_AVAILABLE_GENERATORS = cmake_get_available_generators() + + parser = argparse.ArgumentParser() + parser.add_argument( + '-G', '--generator', help='CMake builds use the specified generator.') + parser.add_argument( + '-v', + '--verbose', + action='store_true', + help='Show configuration and build output.') + args = parser.parse_args() + + if args.generator: + CMAKE_GENERATOR = args.generator + if args.verbose: + VERBOSE = True + + if VERBOSE: + print(f'CMAKE={CMAKE}') + print(f'CMAKE_GENERATOR={CMAKE_GENERATOR}') + print(f'CMAKE_AVAILABLE_GENERATORS={CMAKE_AVAILABLE_GENERATORS}') + print(f'DRACO_SOURCES_PATH={DRACO_SOURCES_PATH}') + print(f'DRACO_SHARED_BUILD_PATH={DRACO_SHARED_BUILD_PATH}') + print(f'DRACO_STATIC_BUILD_PATH={DRACO_STATIC_BUILD_PATH}') + print(f'DRACO_SHARED_INSTALL_PATH={DRACO_SHARED_INSTALL_PATH}') + print(f'DRACO_STATIC_INSTALL_PATH={DRACO_STATIC_INSTALL_PATH}') + print(f'NUM_PROCESSES={NUM_PROCESSES}') + print(f'TEST_SHARED_BUILD_PATH={TEST_SHARED_BUILD_PATH}') + print(f'TEST_STATIC_BUILD_PATH={TEST_STATIC_BUILD_PATH}') + print(f'TEST_SOURCES_PATH={TEST_SOURCES_PATH}') + print(f'VERBOSE={VERBOSE}') + + if CMAKE_GENERATOR and CMAKE_GENERATOR not in CMAKE_AVAILABLE_GENERATORS: + raise ValueError(f'CMake generator unavailable: {CMAKE_GENERATOR}.') + + test_draco_install()