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