|  | #!/usr/bin/env python | 
|  | # ===----------------------------------------------------------------------===## | 
|  | # | 
|  | # 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 | 
|  | # | 
|  | # ===----------------------------------------------------------------------===## | 
|  |  | 
|  | """ | 
|  | Runs an executable on a remote host. | 
|  |  | 
|  | This is meant to be used as an executor when running the LLVM and the Libraries | 
|  | tests on a target. | 
|  | """ | 
|  |  | 
|  | import argparse | 
|  | import os | 
|  | import posixpath | 
|  | import shlex | 
|  | import subprocess | 
|  | import sys | 
|  | import tarfile | 
|  | import tempfile | 
|  | import re | 
|  |  | 
|  |  | 
|  | def ssh(args, command): | 
|  | cmd = ["ssh", "-oBatchMode=yes"] | 
|  | if args.extra_ssh_args is not None: | 
|  | cmd.extend(shlex.split(args.extra_ssh_args)) | 
|  | return cmd + [args.host, command] | 
|  |  | 
|  |  | 
|  | def scp(args, src, dst): | 
|  | cmd = ["scp", "-q", "-oBatchMode=yes"] | 
|  | if args.extra_scp_args is not None: | 
|  | cmd.extend(shlex.split(args.extra_scp_args)) | 
|  | return cmd + [src, "{}:{}".format(args.host, dst)] | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | parser = argparse.ArgumentParser() | 
|  | parser.add_argument("--host", type=str, required=True) | 
|  | parser.add_argument("--execdir", type=str, required=False) | 
|  | parser.add_argument("--extra-ssh-args", type=str, required=False) | 
|  | parser.add_argument("--extra-scp-args", type=str, required=False) | 
|  | parser.add_argument("--codesign_identity", type=str, required=False, default=None) | 
|  | parser.add_argument("--env", type=str, nargs="*", required=False, default=dict()) | 
|  |  | 
|  | # Note: The default value is for the backward compatibility with a hack in | 
|  | # libcxx test suite. | 
|  | # 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 on the remote host. It's also possible | 
|  | # for there to be no such executable, for example in the case of a .sh.cpp | 
|  | # test. | 
|  | parser.add_argument( | 
|  | "--exec-pattern", | 
|  | type=str, | 
|  | required=False, | 
|  | default=".*", | 
|  | help="The name regex pattern of the executables generated by \ | 
|  | a test file. Specifying it allows us to do custom \ | 
|  | processing like codesigning test-executables \ | 
|  | and changing their path when running on \ | 
|  | the remote host. It's also possible for there \ | 
|  | to be no such executable, for example in \ | 
|  | the case of a .sh.cpp test.", | 
|  | ) | 
|  |  | 
|  | parser.add_argument("command", nargs=argparse.ONE_OR_MORE) | 
|  | args = parser.parse_args() | 
|  | commandLine = args.command | 
|  |  | 
|  | execdir = args.execdir | 
|  | if execdir == ".": | 
|  | # Retrieve the exec directory from the command line. | 
|  | execdir, _ = os.path.split(commandLine[0]) | 
|  | if execdir == "": | 
|  | # Get the current directory in that case. | 
|  | execdir = os.getcwd() | 
|  | arcname = os.path.basename(execdir) if execdir else None | 
|  |  | 
|  | # Create a temporary directory where the test will be run. | 
|  | # That is effectively the value of %T on the remote host. | 
|  | tmp = subprocess.check_output( | 
|  | ssh(args, "mktemp -d /tmp/llvm.XXXXXXXXXX"), universal_newlines=True | 
|  | ).strip() | 
|  |  | 
|  | isExecutable = lambda exe: re.match(args.exec_pattern, exe) and os.path.exists(exe) | 
|  | pathOnRemote = lambda file: posixpath.join(tmp, os.path.basename(file)) | 
|  |  | 
|  | remoteCommands = [] | 
|  |  | 
|  | try: | 
|  | # Do any necessary codesigning of test-executables found in the command line. | 
|  | if args.codesign_identity: | 
|  | for exe in filter(isExecutable, commandLine): | 
|  | subprocess.check_call( | 
|  | ["xcrun", "codesign", "-f", "-s", args.codesign_identity, exe], | 
|  | env={}, | 
|  | ) | 
|  |  | 
|  | # tar up the execution directory (which contains everything that's needed | 
|  | # to run the test), and copy the tarball over to the remote host. | 
|  | if execdir: | 
|  | try: | 
|  | tmpTar = tempfile.NamedTemporaryFile(suffix=".tar", delete=False) | 
|  | with tarfile.open(fileobj=tmpTar, mode="w") as tarball: | 
|  | tarball.add(execdir, arcname=arcname) | 
|  |  | 
|  | # Make sure we close the file before we scp it, because accessing | 
|  | # the temporary file while still open doesn't work on Windows. | 
|  | tmpTar.close() | 
|  | remoteTarball = pathOnRemote(tmpTar.name) | 
|  | subprocess.check_call(scp(args, tmpTar.name, remoteTarball)) | 
|  | 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) | 
|  |  | 
|  | # Untar the dependencies in the temporary directory and remove the tarball. | 
|  | remoteCommands.extend( | 
|  | [ | 
|  | "tar -xf {} -C {} --strip-components 1".format(remoteTarball, tmp), | 
|  | "rm {}".format(remoteTarball), | 
|  | ] | 
|  | ) | 
|  | else: | 
|  | # Copy only the files, which are specified in the command line. | 
|  | # Copy them to remote host one by one. | 
|  | for x in commandLine: | 
|  | _, f = os.path.split(x) | 
|  | subprocess.check_call(scp(args, x, pathOnRemote(f))) | 
|  |  | 
|  | # Make sure all executables in the remote command line have 'execute' | 
|  | # permissions on the remote host. The host that compiled the test-executable | 
|  | # might not have a notion of 'executable' permissions. | 
|  | for exe in filter(isExecutable, commandLine): | 
|  | remoteCommands.append("chmod +x {}".format(pathOnRemote(exe))) | 
|  |  | 
|  | # Execute the command through SSH in the temporary directory, with the | 
|  | # correct environment. We tweak the command line to run it on the remote | 
|  | # host by transforming the path of test-executables to their path in the | 
|  | # temporary directory on the remote host. | 
|  | for i, x in enumerate(commandLine): | 
|  | if isExecutable(x): | 
|  | commandLine[i] = pathOnRemote(x) | 
|  | remoteCommands.append("cd {}".format(tmp)) | 
|  | if args.env: | 
|  | remoteCommands.append("export {}".format(" ".join(args.env))) | 
|  | remoteCommands.append(subprocess.list2cmdline(commandLine)) | 
|  |  | 
|  | # Finally, SSH to the remote host and execute all the commands. | 
|  | rc = subprocess.call(ssh(args, " && ".join(remoteCommands))) | 
|  | return rc | 
|  |  | 
|  | finally: | 
|  | # Make sure the temporary directory is removed when we're done. | 
|  | subprocess.check_call(ssh(args, "rm -r {}".format(tmp))) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | exit(main()) |