1f998e0d6SLouis Dionne#!/usr/bin/env python 207e46252SLouis Dionne# ===----------------------------------------------------------------------===## 307e46252SLouis Dionne# 407e46252SLouis Dionne# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 507e46252SLouis Dionne# See https://llvm.org/LICENSE.txt for license information. 607e46252SLouis Dionne# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 707e46252SLouis Dionne# 807e46252SLouis Dionne# ===----------------------------------------------------------------------===## 907e46252SLouis Dionne 1007e46252SLouis Dionne""" 1107e46252SLouis DionneRuns an executable on a remote host. 1207e46252SLouis Dionne 1307e46252SLouis DionneThis is meant to be used as an executor when running the C++ Standard Library 1407e46252SLouis Dionneconformance test suite. 1507e46252SLouis Dionne""" 1607e46252SLouis Dionne 1707e46252SLouis Dionneimport argparse 1807e46252SLouis Dionneimport os 19fee0026fSSergej Jaskiewiczimport posixpath 2004f908b9SAlex Richardsonimport shlex 2107e46252SLouis Dionneimport subprocess 2207e46252SLouis Dionneimport sys 2392e563bcSLouis Dionneimport tarfile 2492e563bcSLouis Dionneimport tempfile 2507e46252SLouis Dionne 2607e46252SLouis Dionne 2707e46252SLouis Dionnedef main(): 2807e46252SLouis Dionne parser = argparse.ArgumentParser() 297bfaa0f0STobias Hieta parser.add_argument("--host", type=str, required=True) 307bfaa0f0STobias Hieta parser.add_argument("--execdir", type=str, required=True) 317bfaa0f0STobias Hieta parser.add_argument("--tempdir", type=str, required=False, default="/tmp") 327bfaa0f0STobias Hieta parser.add_argument("--extra-ssh-args", type=str, required=False) 337bfaa0f0STobias Hieta parser.add_argument("--extra-scp-args", type=str, required=False) 347bfaa0f0STobias Hieta parser.add_argument("--codesign_identity", type=str, required=False, default=None) 357bfaa0f0STobias Hieta parser.add_argument("--env", type=str, nargs="*", required=False, default=[]) 3677d8ce5bSLouis Dionne parser.add_argument("--prepend_env", type=str, nargs="*", required=False, default=[]) 3777d8ce5bSLouis Dionne parser.add_argument("-v", "--verbose", action='store_true') 383980e895SAlex Richardson parser.add_argument("command", nargs=argparse.ONE_OR_MORE) 393980e895SAlex Richardson args = parser.parse_args() 403980e895SAlex Richardson commandLine = args.command 4107e46252SLouis Dionne 421ce70139SLouis Dionne def ssh(command): 431ce70139SLouis Dionne cmd = ["ssh", "-oBatchMode=yes"] 441ce70139SLouis Dionne if args.extra_ssh_args is not None: 451ce70139SLouis Dionne cmd.extend(shlex.split(args.extra_ssh_args)) 461ce70139SLouis Dionne return cmd + [args.host, command] 471ce70139SLouis Dionne 481ce70139SLouis Dionne def scp(src, dst): 491ce70139SLouis Dionne cmd = ["scp", "-q", "-oBatchMode=yes"] 501ce70139SLouis Dionne if args.extra_scp_args is not None: 511ce70139SLouis Dionne cmd.extend(shlex.split(args.extra_scp_args)) 521ce70139SLouis Dionne return cmd + [src, "{}:{}".format(args.host, dst)] 531ce70139SLouis Dionne 5477d8ce5bSLouis Dionne def runCommand(command, *args_, **kwargs): 5577d8ce5bSLouis Dionne if args.verbose: 562ab31b6bSLouis Dionne print(f"$ {' '.join(command)}", file=sys.stderr) 5777d8ce5bSLouis Dionne return subprocess.run(command, *args_, **kwargs) 5877d8ce5bSLouis Dionne 5964acef38SLouis Dionne # Create a temporary directory where the test will be run. 601fc5010dSLouis Dionne # That is effectively the value of %T on the remote host. 6177d8ce5bSLouis Dionne tmp = runCommand( 621ce70139SLouis Dionne ssh("mktemp -d {}/libcxx.XXXXXXXXXX".format(args.tempdir)), 637bfaa0f0STobias Hieta universal_newlines=True, 6477d8ce5bSLouis Dionne check=True, 65*21f8bc25SLouis Dionne capture_output=True, 66*21f8bc25SLouis Dionne stdin=subprocess.DEVNULL 6777d8ce5bSLouis Dionne ).stdout.strip() 680489d39eSLouis Dionne 690489d39eSLouis Dionne # HACK: 700489d39eSLouis Dionne # If an argument is a file that ends in `.tmp.exe`, assume it is the name 710489d39eSLouis Dionne # of an executable generated by a test file. We call these test-executables 720489d39eSLouis Dionne # below. This allows us to do custom processing like codesigning test-executables 730489d39eSLouis Dionne # and changing their path when running on the remote host. It's also possible 740489d39eSLouis Dionne # for there to be no such executable, for example in the case of a .sh.cpp 750489d39eSLouis Dionne # test. 767bfaa0f0STobias Hieta isTestExe = lambda exe: exe.endswith(".tmp.exe") and os.path.exists(exe) 7792e563bcSLouis Dionne pathOnRemote = lambda file: posixpath.join(tmp, os.path.basename(file)) 780489d39eSLouis Dionne 7964acef38SLouis Dionne try: 800489d39eSLouis Dionne # Do any necessary codesigning of test-executables found in the command line. 810489d39eSLouis Dionne if args.codesign_identity: 820489d39eSLouis Dionne for exe in filter(isTestExe, commandLine): 83d2b71c7aSLouis Dionne codesign = ["codesign", "-f", "-s", args.codesign_identity, exe] 84*21f8bc25SLouis Dionne runCommand(codesign, env={}, check=True, stdin=subprocess.DEVNULL) 850489d39eSLouis Dionne 861fc5010dSLouis Dionne # tar up the execution directory (which contains everything that's needed 871fc5010dSLouis Dionne # to run the test), and copy the tarball over to the remote host. 88b00a874bSLouis Dionne try: 897bfaa0f0STobias Hieta tmpTar = tempfile.NamedTemporaryFile(suffix=".tar", delete=False) 907bfaa0f0STobias Hieta with tarfile.open(fileobj=tmpTar, mode="w") as tarball: 911fc5010dSLouis Dionne tarball.add(args.execdir, arcname=os.path.basename(args.execdir)) 9292e563bcSLouis Dionne 93b00a874bSLouis Dionne # Make sure we close the file before we scp it, because accessing 94b00a874bSLouis Dionne # the temporary file while still open doesn't work on Windows. 95b00a874bSLouis Dionne tmpTar.close() 9692e563bcSLouis Dionne remoteTarball = pathOnRemote(tmpTar.name) 97*21f8bc25SLouis Dionne runCommand(scp(tmpTar.name, remoteTarball), check=True, stdin=subprocess.DEVNULL) 98b00a874bSLouis Dionne finally: 99b00a874bSLouis Dionne # Make sure we close the file in case an exception happens before 100b00a874bSLouis Dionne # we've closed it above -- otherwise close() is idempotent. 101b00a874bSLouis Dionne tmpTar.close() 102b00a874bSLouis Dionne os.remove(tmpTar.name) 10392e563bcSLouis Dionne 10492e563bcSLouis Dionne # Untar the dependencies in the temporary directory and remove the tarball. 10592e563bcSLouis Dionne remoteCommands = [ 1067bfaa0f0STobias Hieta "tar -xf {} -C {} --strip-components 1".format(remoteTarball, tmp), 1077bfaa0f0STobias Hieta "rm {}".format(remoteTarball), 10892e563bcSLouis Dionne ] 10907e46252SLouis Dionne 1100489d39eSLouis Dionne # Make sure all test-executables in the remote command line have 'execute' 1110489d39eSLouis Dionne # permissions on the remote host. The host that compiled the test-executable 1120489d39eSLouis Dionne # might not have a notion of 'executable' permissions. 11392e563bcSLouis Dionne for exe in map(pathOnRemote, filter(isTestExe, commandLine)): 1147bfaa0f0STobias Hieta remoteCommands.append("chmod +x {}".format(exe)) 11507e46252SLouis Dionne 11607e46252SLouis Dionne # Execute the command through SSH in the temporary directory, with the 1170489d39eSLouis Dionne # correct environment. We tweak the command line to run it on the remote 1180489d39eSLouis Dionne # host by transforming the path of test-executables to their path in the 1191fc5010dSLouis Dionne # temporary directory on the remote host. 1205eb8d45aSLouis Dionne commandLine = (pathOnRemote(x) if isTestExe(x) else x for x in commandLine) 1217bfaa0f0STobias Hieta remoteCommands.append("cd {}".format(tmp)) 122ba3bddb6SMartin Storsjö 123ba3bddb6SMartin Storsjö if args.prepend_env: 124ba3bddb6SMartin Storsjö # We can't sensibly know the original value of the env vars 125ba3bddb6SMartin Storsjö # in order to prepend to them, so just overwrite these variables. 126ba3bddb6SMartin Storsjö args.env.extend(args.prepend_env) 127ba3bddb6SMartin Storsjö 128d98b9a41SLouis Dionne if args.env: 1291ce70139SLouis Dionne env = list(map(shlex.quote, args.env)) 1307bfaa0f0STobias Hieta remoteCommands.append("export {}".format(" ".join(args.env))) 131d98b9a41SLouis Dionne remoteCommands.append(subprocess.list2cmdline(commandLine)) 13292e563bcSLouis Dionne 13392e563bcSLouis Dionne # Finally, SSH to the remote host and execute all the commands. 134*21f8bc25SLouis Dionne # Make sure to forward stdin to the process so that the test suite 135*21f8bc25SLouis Dionne # can pipe stuff into the executor. 1361ce70139SLouis Dionne rc = runCommand(ssh(" && ".join(remoteCommands))).returncode 13764acef38SLouis Dionne return rc 13807e46252SLouis Dionne 13964acef38SLouis Dionne finally: 14064acef38SLouis Dionne # Make sure the temporary directory is removed when we're done. 1411ce70139SLouis Dionne runCommand(ssh("rm -r {}".format(tmp)), check=True) 14207e46252SLouis Dionne 14307e46252SLouis Dionne 1447bfaa0f0STobias Hietaif __name__ == "__main__": 14507e46252SLouis Dionne exit(main()) 146