1*4d6fc14bSjoerg#!/usr/bin/env python 2*4d6fc14bSjoerg#===----------------------------------------------------------------------===## 3*4d6fc14bSjoerg# 4*4d6fc14bSjoerg# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 5*4d6fc14bSjoerg# See https://llvm.org/LICENSE.txt for license information. 6*4d6fc14bSjoerg# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 7*4d6fc14bSjoerg# 8*4d6fc14bSjoerg#===----------------------------------------------------------------------===## 9*4d6fc14bSjoerg 10*4d6fc14bSjoerg""" 11*4d6fc14bSjoergRuns an executable on a remote host. 12*4d6fc14bSjoerg 13*4d6fc14bSjoergThis is meant to be used as an executor when running the C++ Standard Library 14*4d6fc14bSjoergconformance test suite. 15*4d6fc14bSjoerg""" 16*4d6fc14bSjoerg 17*4d6fc14bSjoergimport argparse 18*4d6fc14bSjoergimport os 19*4d6fc14bSjoergimport posixpath 20*4d6fc14bSjoergimport shlex 21*4d6fc14bSjoergimport subprocess 22*4d6fc14bSjoergimport sys 23*4d6fc14bSjoergimport tarfile 24*4d6fc14bSjoergimport tempfile 25*4d6fc14bSjoerg 26*4d6fc14bSjoergtry: 27*4d6fc14bSjoerg from shlex import quote as cmd_quote 28*4d6fc14bSjoergexcept ImportError: 29*4d6fc14bSjoerg # for Python 2 compatibility 30*4d6fc14bSjoerg from pipes import quote as cmd_quote 31*4d6fc14bSjoerg 32*4d6fc14bSjoergdef ssh(args, command): 33*4d6fc14bSjoerg cmd = ['ssh', '-oBatchMode=yes'] 34*4d6fc14bSjoerg if args.extra_ssh_args is not None: 35*4d6fc14bSjoerg cmd.extend(shlex.split(args.extra_ssh_args)) 36*4d6fc14bSjoerg return cmd + [args.host, command] 37*4d6fc14bSjoerg 38*4d6fc14bSjoerg 39*4d6fc14bSjoergdef scp(args, src, dst): 40*4d6fc14bSjoerg cmd = ['scp', '-q', '-oBatchMode=yes'] 41*4d6fc14bSjoerg if args.extra_scp_args is not None: 42*4d6fc14bSjoerg cmd.extend(shlex.split(args.extra_scp_args)) 43*4d6fc14bSjoerg return cmd + [src, '{}:{}'.format(args.host, dst)] 44*4d6fc14bSjoerg 45*4d6fc14bSjoerg 46*4d6fc14bSjoergdef main(): 47*4d6fc14bSjoerg parser = argparse.ArgumentParser() 48*4d6fc14bSjoerg parser.add_argument('--host', type=str, required=True) 49*4d6fc14bSjoerg parser.add_argument('--execdir', type=str, required=True) 50*4d6fc14bSjoerg parser.add_argument('--tempdir', type=str, required=False, default='/tmp') 51*4d6fc14bSjoerg parser.add_argument('--extra-ssh-args', type=str, required=False) 52*4d6fc14bSjoerg parser.add_argument('--extra-scp-args', type=str, required=False) 53*4d6fc14bSjoerg parser.add_argument('--codesign_identity', type=str, required=False, default=None) 54*4d6fc14bSjoerg parser.add_argument('--env', type=str, nargs='*', required=False, default=dict()) 55*4d6fc14bSjoerg parser.add_argument("command", nargs=argparse.ONE_OR_MORE) 56*4d6fc14bSjoerg args = parser.parse_args() 57*4d6fc14bSjoerg commandLine = args.command 58*4d6fc14bSjoerg 59*4d6fc14bSjoerg # Create a temporary directory where the test will be run. 60*4d6fc14bSjoerg # That is effectively the value of %T on the remote host. 61*4d6fc14bSjoerg tmp = subprocess.check_output(ssh(args, 'mktemp -d {}/libcxx.XXXXXXXXXX'.format(args.tempdir)), universal_newlines=True).strip() 62*4d6fc14bSjoerg 63*4d6fc14bSjoerg # HACK: 64*4d6fc14bSjoerg # If an argument is a file that ends in `.tmp.exe`, assume it is the name 65*4d6fc14bSjoerg # of an executable generated by a test file. We call these test-executables 66*4d6fc14bSjoerg # below. This allows us to do custom processing like codesigning test-executables 67*4d6fc14bSjoerg # and changing their path when running on the remote host. It's also possible 68*4d6fc14bSjoerg # for there to be no such executable, for example in the case of a .sh.cpp 69*4d6fc14bSjoerg # test. 70*4d6fc14bSjoerg isTestExe = lambda exe: exe.endswith('.tmp.exe') and os.path.exists(exe) 71*4d6fc14bSjoerg pathOnRemote = lambda file: posixpath.join(tmp, os.path.basename(file)) 72*4d6fc14bSjoerg 73*4d6fc14bSjoerg try: 74*4d6fc14bSjoerg # Do any necessary codesigning of test-executables found in the command line. 75*4d6fc14bSjoerg if args.codesign_identity: 76*4d6fc14bSjoerg for exe in filter(isTestExe, commandLine): 77*4d6fc14bSjoerg subprocess.check_call(['xcrun', 'codesign', '-f', '-s', args.codesign_identity, exe], env={}) 78*4d6fc14bSjoerg 79*4d6fc14bSjoerg # tar up the execution directory (which contains everything that's needed 80*4d6fc14bSjoerg # to run the test), and copy the tarball over to the remote host. 81*4d6fc14bSjoerg try: 82*4d6fc14bSjoerg tmpTar = tempfile.NamedTemporaryFile(suffix='.tar', delete=False) 83*4d6fc14bSjoerg with tarfile.open(fileobj=tmpTar, mode='w') as tarball: 84*4d6fc14bSjoerg tarball.add(args.execdir, arcname=os.path.basename(args.execdir)) 85*4d6fc14bSjoerg 86*4d6fc14bSjoerg # Make sure we close the file before we scp it, because accessing 87*4d6fc14bSjoerg # the temporary file while still open doesn't work on Windows. 88*4d6fc14bSjoerg tmpTar.close() 89*4d6fc14bSjoerg remoteTarball = pathOnRemote(tmpTar.name) 90*4d6fc14bSjoerg subprocess.check_call(scp(args, tmpTar.name, remoteTarball)) 91*4d6fc14bSjoerg finally: 92*4d6fc14bSjoerg # Make sure we close the file in case an exception happens before 93*4d6fc14bSjoerg # we've closed it above -- otherwise close() is idempotent. 94*4d6fc14bSjoerg tmpTar.close() 95*4d6fc14bSjoerg os.remove(tmpTar.name) 96*4d6fc14bSjoerg 97*4d6fc14bSjoerg # Untar the dependencies in the temporary directory and remove the tarball. 98*4d6fc14bSjoerg remoteCommands = [ 99*4d6fc14bSjoerg 'tar -xf {} -C {} --strip-components 1'.format(remoteTarball, tmp), 100*4d6fc14bSjoerg 'rm {}'.format(remoteTarball) 101*4d6fc14bSjoerg ] 102*4d6fc14bSjoerg 103*4d6fc14bSjoerg # Make sure all test-executables in the remote command line have 'execute' 104*4d6fc14bSjoerg # permissions on the remote host. The host that compiled the test-executable 105*4d6fc14bSjoerg # might not have a notion of 'executable' permissions. 106*4d6fc14bSjoerg for exe in map(pathOnRemote, filter(isTestExe, commandLine)): 107*4d6fc14bSjoerg remoteCommands.append('chmod +x {}'.format(exe)) 108*4d6fc14bSjoerg 109*4d6fc14bSjoerg # Execute the command through SSH in the temporary directory, with the 110*4d6fc14bSjoerg # correct environment. We tweak the command line to run it on the remote 111*4d6fc14bSjoerg # host by transforming the path of test-executables to their path in the 112*4d6fc14bSjoerg # temporary directory on the remote host. 113*4d6fc14bSjoerg commandLine = (pathOnRemote(x) if isTestExe(x) else x for x in commandLine) 114*4d6fc14bSjoerg remoteCommands.append('cd {}'.format(tmp)) 115*4d6fc14bSjoerg if args.env: 116*4d6fc14bSjoerg remoteCommands.append('export {}'.format(cmd_quote(' '.join(args.env)))) 117*4d6fc14bSjoerg remoteCommands.append(subprocess.list2cmdline(commandLine)) 118*4d6fc14bSjoerg 119*4d6fc14bSjoerg # Finally, SSH to the remote host and execute all the commands. 120*4d6fc14bSjoerg rc = subprocess.call(ssh(args, ' && '.join(remoteCommands))) 121*4d6fc14bSjoerg return rc 122*4d6fc14bSjoerg 123*4d6fc14bSjoerg finally: 124*4d6fc14bSjoerg # Make sure the temporary directory is removed when we're done. 125*4d6fc14bSjoerg subprocess.check_call(ssh(args, 'rm -r {}'.format(tmp))) 126*4d6fc14bSjoerg 127*4d6fc14bSjoerg 128*4d6fc14bSjoergif __name__ == '__main__': 129*4d6fc14bSjoerg exit(main()) 130