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