1037e7968Spatrick#!/usr/bin/env python 2037e7968Spatrick#===----------------------------------------------------------------------===## 3037e7968Spatrick# 4037e7968Spatrick# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 5037e7968Spatrick# See https://llvm.org/LICENSE.txt for license information. 6037e7968Spatrick# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 7037e7968Spatrick# 8037e7968Spatrick#===----------------------------------------------------------------------===## 9037e7968Spatrick 10037e7968Spatrick""" 11037e7968SpatrickRuns an executable on a remote host. 12037e7968Spatrick 13037e7968SpatrickThis is meant to be used as an executor when running the C++ Standard Library 14037e7968Spatrickconformance test suite. 15037e7968Spatrick""" 16037e7968Spatrick 17037e7968Spatrickimport argparse 18037e7968Spatrickimport os 19037e7968Spatrickimport posixpath 20*76d0caaeSpatrickimport shlex 21037e7968Spatrickimport subprocess 22037e7968Spatrickimport sys 23037e7968Spatrickimport tarfile 24037e7968Spatrickimport tempfile 25037e7968Spatrick 26*76d0caaeSpatrickfrom shlex import quote as cmd_quote 27*76d0caaeSpatrick 28*76d0caaeSpatrickdef ssh(args, command): 29*76d0caaeSpatrick cmd = ['ssh', '-oBatchMode=yes'] 30*76d0caaeSpatrick if args.extra_ssh_args is not None: 31*76d0caaeSpatrick cmd.extend(shlex.split(args.extra_ssh_args)) 32*76d0caaeSpatrick return cmd + [args.host, command] 33*76d0caaeSpatrick 34*76d0caaeSpatrick 35*76d0caaeSpatrickdef scp(args, src, dst): 36*76d0caaeSpatrick cmd = ['scp', '-q', '-oBatchMode=yes'] 37*76d0caaeSpatrick if args.extra_scp_args is not None: 38*76d0caaeSpatrick cmd.extend(shlex.split(args.extra_scp_args)) 39*76d0caaeSpatrick return cmd + [src, '{}:{}'.format(args.host, dst)] 40*76d0caaeSpatrick 41037e7968Spatrick 42037e7968Spatrickdef main(): 43037e7968Spatrick parser = argparse.ArgumentParser() 44037e7968Spatrick parser.add_argument('--host', type=str, required=True) 45037e7968Spatrick parser.add_argument('--execdir', type=str, required=True) 46*76d0caaeSpatrick parser.add_argument('--tempdir', type=str, required=False, default='/tmp') 47*76d0caaeSpatrick parser.add_argument('--extra-ssh-args', type=str, required=False) 48*76d0caaeSpatrick parser.add_argument('--extra-scp-args', type=str, required=False) 49037e7968Spatrick parser.add_argument('--codesign_identity', type=str, required=False, default=None) 50037e7968Spatrick parser.add_argument('--env', type=str, nargs='*', required=False, default=dict()) 51*76d0caaeSpatrick parser.add_argument("command", nargs=argparse.ONE_OR_MORE) 52*76d0caaeSpatrick args = parser.parse_args() 53*76d0caaeSpatrick commandLine = args.command 54037e7968Spatrick 55037e7968Spatrick # Create a temporary directory where the test will be run. 56037e7968Spatrick # That is effectively the value of %T on the remote host. 57*76d0caaeSpatrick tmp = subprocess.check_output(ssh(args, 'mktemp -d {}/libcxx.XXXXXXXXXX'.format(args.tempdir)), universal_newlines=True).strip() 58037e7968Spatrick 59037e7968Spatrick # HACK: 60037e7968Spatrick # If an argument is a file that ends in `.tmp.exe`, assume it is the name 61037e7968Spatrick # of an executable generated by a test file. We call these test-executables 62037e7968Spatrick # below. This allows us to do custom processing like codesigning test-executables 63037e7968Spatrick # and changing their path when running on the remote host. It's also possible 64037e7968Spatrick # for there to be no such executable, for example in the case of a .sh.cpp 65037e7968Spatrick # test. 66037e7968Spatrick isTestExe = lambda exe: exe.endswith('.tmp.exe') and os.path.exists(exe) 67037e7968Spatrick pathOnRemote = lambda file: posixpath.join(tmp, os.path.basename(file)) 68037e7968Spatrick 69037e7968Spatrick try: 70037e7968Spatrick # Do any necessary codesigning of test-executables found in the command line. 71037e7968Spatrick if args.codesign_identity: 72037e7968Spatrick for exe in filter(isTestExe, commandLine): 73037e7968Spatrick subprocess.check_call(['xcrun', 'codesign', '-f', '-s', args.codesign_identity, exe], env={}) 74037e7968Spatrick 75037e7968Spatrick # tar up the execution directory (which contains everything that's needed 76037e7968Spatrick # to run the test), and copy the tarball over to the remote host. 77037e7968Spatrick try: 78037e7968Spatrick tmpTar = tempfile.NamedTemporaryFile(suffix='.tar', delete=False) 79037e7968Spatrick with tarfile.open(fileobj=tmpTar, mode='w') as tarball: 80037e7968Spatrick tarball.add(args.execdir, arcname=os.path.basename(args.execdir)) 81037e7968Spatrick 82037e7968Spatrick # Make sure we close the file before we scp it, because accessing 83037e7968Spatrick # the temporary file while still open doesn't work on Windows. 84037e7968Spatrick tmpTar.close() 85037e7968Spatrick remoteTarball = pathOnRemote(tmpTar.name) 86*76d0caaeSpatrick subprocess.check_call(scp(args, tmpTar.name, remoteTarball)) 87037e7968Spatrick finally: 88037e7968Spatrick # Make sure we close the file in case an exception happens before 89037e7968Spatrick # we've closed it above -- otherwise close() is idempotent. 90037e7968Spatrick tmpTar.close() 91037e7968Spatrick os.remove(tmpTar.name) 92037e7968Spatrick 93037e7968Spatrick # Untar the dependencies in the temporary directory and remove the tarball. 94037e7968Spatrick remoteCommands = [ 95037e7968Spatrick 'tar -xf {} -C {} --strip-components 1'.format(remoteTarball, tmp), 96037e7968Spatrick 'rm {}'.format(remoteTarball) 97037e7968Spatrick ] 98037e7968Spatrick 99037e7968Spatrick # Make sure all test-executables in the remote command line have 'execute' 100037e7968Spatrick # permissions on the remote host. The host that compiled the test-executable 101037e7968Spatrick # might not have a notion of 'executable' permissions. 102037e7968Spatrick for exe in map(pathOnRemote, filter(isTestExe, commandLine)): 103037e7968Spatrick remoteCommands.append('chmod +x {}'.format(exe)) 104037e7968Spatrick 105037e7968Spatrick # Execute the command through SSH in the temporary directory, with the 106037e7968Spatrick # correct environment. We tweak the command line to run it on the remote 107037e7968Spatrick # host by transforming the path of test-executables to their path in the 108037e7968Spatrick # temporary directory on the remote host. 109037e7968Spatrick commandLine = (pathOnRemote(x) if isTestExe(x) else x for x in commandLine) 110037e7968Spatrick remoteCommands.append('cd {}'.format(tmp)) 111037e7968Spatrick if args.env: 112*76d0caaeSpatrick remoteCommands.append('export {}'.format(cmd_quote(' '.join(args.env)))) 113037e7968Spatrick remoteCommands.append(subprocess.list2cmdline(commandLine)) 114037e7968Spatrick 115037e7968Spatrick # Finally, SSH to the remote host and execute all the commands. 116*76d0caaeSpatrick rc = subprocess.call(ssh(args, ' && '.join(remoteCommands))) 117037e7968Spatrick return rc 118037e7968Spatrick 119037e7968Spatrick finally: 120037e7968Spatrick # Make sure the temporary directory is removed when we're done. 121*76d0caaeSpatrick subprocess.check_call(ssh(args, 'rm -r {}'.format(tmp))) 122037e7968Spatrick 123037e7968Spatrick 124037e7968Spatrickif __name__ == '__main__': 125037e7968Spatrick exit(main()) 126