diff --git a/libcxx/test/configs/libcxx-trunk-bundled.cfg.in b/libcxx/test/configs/libcxx-trunk-bundled.cfg.in new file mode 100644 --- /dev/null +++ b/libcxx/test/configs/libcxx-trunk-bundled.cfg.in @@ -0,0 +1,54 @@ +@AUTO_GEN_COMMENT@ + +LIBCXX_ROOT = "@LIBCXX_SOURCE_DIR@" +INSTALL_ROOT = "@CMAKE_BINARY_DIR@" +COMPILER = "@CMAKE_CXX_COMPILER@" +EXEC_ROOT = "@LIBCXX_BINARY_DIR@" +CMAKE_OSX_SYSROOT = "@CMAKE_OSX_SYSROOT@"; + +import os +import pipes +import site +import sys +site.addsitedir(os.path.join(LIBCXX_ROOT, 'utils')) +import libcxx.test.features +import libcxx.test.format +import libcxx.test.newconfig +import libcxx.test.params + +# Configure basic properties of the test suite +config.name = 'libcxx-trunk-bundled' +config.test_source_root = os.path.join(LIBCXX_ROOT, 'test') +config.test_format = libcxx.test.format.CxxStandardLibraryTest() +config.recursiveExpansionLimit = 10 +config.test_exec_root = EXEC_ROOT + +# Configure basic substitutions +bundlePy = os.path.join(LIBCXX_ROOT, 'utils', 'bundle.py') +config.substitutions.append(('%{cxx}', COMPILER)) +config.substitutions.append(('%{flags}', + '-isysroot {}'.format(CMAKE_OSX_SYSROOT) if CMAKE_OSX_SYSROOT else '' +)) +config.substitutions.append(('%{compile_flags}', + '-nostdinc++ -isystem {} -I {}'.format( + os.path.join(INSTALL_ROOT, 'include', 'c++', 'v1'), + os.path.join(LIBCXX_ROOT, 'test', 'support')) +)) +config.substitutions.append(('%{link_flags}', + '-nostdlib++ {} {}'.format( + os.path.join(INSTALL_ROOT, 'lib', 'libc++.a'), + os.path.join(INSTALL_ROOT, 'lib', 'libc++abi.a')) +)) +config.substitutions.append(('%{exec}', + '{} {} --execdir %T --bundle %t.bundle -- '.format( + pipes.quote(sys.executable), + pipes.quote(bundlePy)) +)) + +# Add parameters and features to the config +libcxx.test.newconfig.configure( + libcxx.test.params.DEFAULT_PARAMETERS, + libcxx.test.features.DEFAULT_FEATURES, + config, + lit_config +) diff --git a/libcxx/utils/bundle.py b/libcxx/utils/bundle.py new file mode 100644 --- /dev/null +++ b/libcxx/utils/bundle.py @@ -0,0 +1,126 @@ +#===----------------------------------------------------------------------===## +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +#===----------------------------------------------------------------------===## + +""" +Bundles an executable along with its dependencies into a directory suitable +for running an individual test at a later time. +""" + +import argparse +import os +import subprocess +import sys +import tarfile +import tempfile + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--bundle', type=str, required=True) + parser.add_argument('--codesign_identity', type=str, required=False, default=None) + parser.add_argument('--dependencies', type=str, nargs='*', required=False, default=[]) + parser.add_argument('--env', type=str, nargs='*', required=False, default=dict()) + (args, remaining) = parser.parse_known_args(sys.argv[1:]) + + if len(remaining) < 2: + sys.stderr.write('Missing actual commands to run') + return 1 + + commandLine = remaining[1:] # Skip the '--' + + # HACK: + # If an argument is a file that ends in `.tmp.exe`, assume it is the name + # of an executable generated by a test file. We call these test-executables + # below. This allows us to do custom processing like codesigning test-executables + # and changing their path when running from the bundle. It's also possible + # for there to be no such executable, for example in the case of a .sh.cpp + # test. + isTestExe = lambda exe: exe.endswith('.tmp.exe') and os.path.exists(exe) + pathInBundle = lambda file: '${{TMP}}/{}'.format(os.path.basename(file)) + + # Do any necessary codesigning of test-executables found in the command line. + if args.codesign_identity: + for exe in filter(isTestExe, commandLine): + subprocess.check_call(['xcrun', 'codesign', '-f', '-s', args.codesign_identity, exe], env={}) + + # Ensure the test dependencies exist, tar them up and get an ASCII-encoding + # of the tarball so we can embed it in a shell script (see below). + try: + tmpTar = tempfile.NamedTemporaryFile(suffix='.tar', delete=False) + with tarfile.open(fileobj=tmpTar, mode='w') as tarball: + for dep in args.dependencies: + if not os.path.exists(dep): + sys.stderr.write('Missing file or directory "{}" marked as a dependency of a test'.format(dep)) + return 1 + tarball.add(dep, arcname=os.path.basename(dep)) + + # Make sure we close the file before we encode it, because accessing + # the temporary file while still open doesn't work on Windows. + tmpTar.close() + encodedTarball = subprocess.check_output(['base64', '-i', tmpTar.name]).strip() + finally: + # Make sure we close the file in case an exception happens before + # we've closed it above -- otherwise close() is idempotent. + tmpTar.close() + os.remove(tmpTar.name) + + # Start writing the shell script commands. + commands = [ + '#! /bin/sh', + 'set -e', + 'TMP="$(mktemp -d /tmp/libcxx.XXXXXXXXXX)"' + ] + + # Get the tarball by decoding the end of the shell script, which embeds the + # tarball at the end of itself. Then, untar the dependencies in the temporary + # directory, and remove the intermediate tarball that was extracted from the + # script in the process. + commands += [ + 'tail -n +$[ $(grep -n "^BEGIN_TARBALL" "${0}" | cut -d ":" -f 1) + 1 ] "${0}" | base64 --decode -o "${TMP}/.temporary.tar"', + 'tar -xf "${TMP}/.temporary.tar" -C "${TMP}"', + 'rm "${TMP}/.temporary.tar"' + ] + + # Make sure all test-executables in the bundle have 'execute' permissions + # on the host where the bundle is run. The host that compiled the + # test-executable might not have a notion of 'executable' permissions. + for exe in map(pathInBundle, filter(isTestExe, commandLine)): + commands.append('chmod +x {}'.format(exe)) + + # Execute the command in the temporary directory, with the correct + # environment. We tweak the command line to run it from the bundle + # by transforming the path of test-executables to their path in the + # temporary directory, where we know they have been untarred above. + commands += [ + 'cd "${TMP}"', + 'export {}'.format(' '.join(args.env)), + ' '.join(pathInBundle(x) if isTestExe(x) else x for x in commandLine) + ] + + # Make sure the temporary directory is removed when we're done. + commands += [ + 'rm -r "${TMP}"' + ] + + # The tarball itself is embedded at the end of the script so that it's + # sufficient to copy the script to get all dependencies. It's simpler + # than having to carry around a separate tarball. + commands += [ + 'exit 0', # Make sure the script never executes the embedded tarball + 'BEGIN_TARBALL', + encodedTarball + ] + + # Write the actual script and make it executable. + with open(args.bundle, 'w') as script: + script.write('\n'.join(commands)) + subprocess.check_call(['chmod', '+x', args.bundle]) + + +if __name__ == '__main__': + exit(main())