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
This commit is contained in:
Tom Finegan 2022-02-02 16:05:53 -08:00 committed by GitHub
parent 110a1ec83a
commit 9fd0a50a6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 410 additions and 2 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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 <vector>
#include "draco/core/decoder_buffer.h"
int main(int /*argc*/, char** /*argv*/) {
std::vector<char> empty_buffer;
draco::DecoderBuffer buffer;
buffer.Init(empty_buffer.data(), empty_buffer.size());
return 0;
}

View File

@ -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()