xref: /llvm-project/libcxx/utils/ssh.py (revision 21f8bc25adba762ab06e26a7dd50da78fcd17528)
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