diff --git a/bazel/labgrid/util/target.py b/bazel/labgrid/util/target.py index 5ef2a148a64e0d1f8a3379b5f2f2134fdf6ac984..d2d45a9715c9f3db254367ef781b141dcad0917b 100644 --- a/bazel/labgrid/util/target.py +++ b/bazel/labgrid/util/target.py @@ -41,6 +41,49 @@ class Subprocess: ) +class _TemporaryDirectory: + """Acts like a PurePath with utility functions""" + + def __init__(self, target, root, execroot, tools): + self._target = target + self._root = root + self._execroot = execroot + self._tools = tools + + def mkdir(self, path): + if path.is_absolute(): + make_path = path + else: + make_path = self._execroot / path + + self._target.run( + (f"{self._tools / 'mkdir'}", "-p", f"{path}"), check=True, text=True + ) + + def __getattr__(self, attr): + """Exposes attrs of execroot""" + try: + return getattr(self, attr) + except AttributeError: + pass + + try: + return getattr(self._execroot, attr) + except AttributeError: + raise AttributeError(f"'{type(self)}' object has no attribute '{attr}'") + + def __truediv__(self, path): + return self._execroot / path + + def __str__(self): + return str(self._execroot) + + def remove(self): + self._target.run( + (f"{self._tools / 'rm'}", "-rf", f"{self._root}"), check=True, text=True + ) + + @contextmanager def TemporaryDirectory( shell: CommandProtocol, @@ -48,6 +91,7 @@ def TemporaryDirectory( mktemp: Path, rm: Path, mkdir: Path, + mv: Path, ) -> Iterator[PurePath]: # Get a unique temporary directory, avoiding concurrent `TemporaryDirectory` managers unique = f".{getnode()}.{get_native_id()}" @@ -56,20 +100,25 @@ def TemporaryDirectory( result = target.run((f"./{unique}", "-d"), check=True, text=True) root = PurePath(result.stdout.rstrip()) - # Transfer some basic tools + # Transfer tools and make execroot and tools dir transfer.put(f"{mkdir}", f"{root / 'mkdir'}") - transfer.put(f"{rm}", f"{root / 'rm'}") - # Create the execution root execroot = root / "execroot" + tools = root / "tools" target.run((f"{root / 'mkdir'}", f"{execroot}"), check=True, text=True) + target.run((f"{root / 'mkdir'}", f"{tools}"), check=True, text=True) + + transfer.put(f"{mv}", f"{tools / 'mv'}") + transfer.put(f"{rm}", f"{tools / 'rm'}") + target.run( + (f"{tools / 'mv'}", f"{root / 'mkdir'}", f"{tools / 'mkdir'}"), + check=True, + text=True, + ) - # Remove unneeded tools - target.run((f"{root / 'rm'}", f"{root / 'mkdir'}"), check=True, text=True) - target.run((f"{root / 'rm'}", f"{unique}"), check=True, text=True) + temp = _TemporaryDirectory(target, root, execroot, tools) - # Clean up the root directory when complete try: - yield execroot + yield temp finally: - target.run((f"{root / 'rm'}", "-rf", f"{root}"), check=True, text=True) + temp.remove() diff --git a/e2e/localhost/BUILD.bazel b/e2e/localhost/BUILD.bazel index 51fad29e7812b22c90c1169483b4482f3c1d87a6..cbe36398df37abe778d09af5674be8c4e056f1c6 100644 --- a/e2e/localhost/BUILD.bazel +++ b/e2e/localhost/BUILD.bazel @@ -68,6 +68,30 @@ labgrid_genrule( tools = ["@rules_labgrid//labgrid/run"], ) +labgrid_genrule( + name = "hello-and-put-file-with-remote", + srcs = [ + ":hello.txt", + "@ape//ape:cat", + ], + outs = ["run-and-put-file-with-remote.actual"], + cmd = "$(location @rules_labgrid//labgrid/run) --put $(location :hello.txt):hello.txt $(location @ape//ape:cat) hello.txt > $@", + platform = "@rules_labgrid//labgrid/platform:localhost", + tools = ["@rules_labgrid//labgrid/run"], +) + +labgrid_genrule( + name = "hello-and-put-file-without-remote", + srcs = [ + ":hello.txt", + "@ape//ape:cat", + ], + outs = ["run-and-put-file-without-remote.actual"], + cmd = "$(location @rules_labgrid//labgrid/run) --put $(location :hello.txt) $(location @ape//ape:cat) $(location :hello.txt) > $@", + platform = "@rules_labgrid//labgrid/platform:localhost", + tools = ["@rules_labgrid//labgrid/run"], +) + alias( name = "run-and-transfer-to-nested-d.actual", actual = "nested/run-and-transfer-to-d.actual", @@ -111,6 +135,8 @@ labgrid_run_binary( "run-and-transfer-to-ruledir", "run-and-transfer-to-d", "run-and-transfer-to-nested-d", + "run-and-put-file-without-remote", + "run-and-put-file-with-remote", "run-with-env", "run-binary", ) diff --git a/labgrid/run/BUILD.bazel b/labgrid/run/BUILD.bazel index d8722d31f8cc35c33f5b21194718f55f69c65393..f55b312e8bd86d5a162809093c45249620c0afb2 100644 --- a/labgrid/run/BUILD.bazel +++ b/labgrid/run/BUILD.bazel @@ -1,34 +1,28 @@ load("@rules_labgrid//labgrid/config:transition.bzl", "labgrid_config_transition") load("@rules_python//python:defs.bzl", "py_binary") -labgrid_config_transition( - name = "mktemp", - srcs = ["@ape//ape:mktemp"], -) - -labgrid_config_transition( - name = "mkdir", - srcs = ["@ape//ape:mkdir"], -) +TOOLS = [ + "mktemp", + "mkdir", + "rm", + "env", + "mv", +] -labgrid_config_transition( - name = "rm", - srcs = ["@ape//ape:rm"], -) - -labgrid_config_transition( - name = "env", - srcs = ["@ape//ape:env"], -) +[ + labgrid_config_transition( + name = tool, + srcs = ["@ape//ape:{}".format(tool)], + ) + for tool in TOOLS +] py_binary( name = "run", srcs = ["run.py"], data = [ - ":env", - ":mkdir", - ":mktemp", - ":rm", + ":{}".format(tool) + for tool in TOOLS ], tags = ["manual"], visibility = ["//visibility:public"], diff --git a/labgrid/run/run.py b/labgrid/run/run.py index fcbb4ba5b7ac9ffe15340b2920fca7d5b84f36a7..60395a744b1a3f790d1f47a2bf302efea027e619 100644 --- a/labgrid/run/run.py +++ b/labgrid/run/run.py @@ -54,6 +54,7 @@ class Tools: rm: Path = runfile("ape/ape/assimilate/rm.ape/rm") mkdir: Path = runfile("ape/ape/assimilate/mkdir.ape/mkdir") env: Path = runfile("ape/ape/assimilate/env.ape/env") + mv: Path = runfile("ape/ape/assimilate/mv.ape/mv") def resolve(value: str) -> Path: @@ -214,6 +215,14 @@ def arguments(prsr: ArgumentParser) -> None: action="append", default=[], ) + prsr.add_argument( + "--put", + help="Transfer files to the target before execution. Relative paths are resolved to the execution root on both the local and remote. `LOCAL` performs environment variable substitution. `REMOTE` can be omitted, which will use the same path for remote and local.", + type=put, + metavar="LOCAL[:REMOTE]", + action="append", + default=[], + ) prsr.add_argument( "--log-level", action=LogLevelAction, @@ -225,7 +234,7 @@ def arguments(prsr: ArgumentParser) -> None: ) -def get(value: str) -> Get: +def get(value: str) -> FileTransfer: # FIXME: Extract `BazelArgumentParser` to handle quoting remote, found, local = value.removeprefix("'").removesuffix("'").partition(":") optional = remote.endswith("?") @@ -235,17 +244,27 @@ def get(value: str) -> Get: local = Template(local).substitute(environ) else: local = remote - return Get(PurePath(remote), PurePath(local), optional) + return FileTransfer(PurePath(remote), PurePath(local), optional) + + +def put(value: str) -> FileTransfer: + # FIXME: Extract `BazelArgumentParser` to handle quoting + local, found, remote = value.removeprefix("'").removesuffix("'").partition(":") + if found: + local = Template(local).substitute(environ) + else: + remote = local + return FileTransfer(PurePath(remote), PurePath(local), False) @dataclass(frozen=True) -class Get: +class FileTransfer: remote: PurePath local: PurePath optional: bool - def resolve(self, remote: PurePath) -> Get: - return Get( + def resolve(self, remote: PurePath) -> FileTransfer: + return FileTransfer( remote=self.join(remote, self.remote), local=self.join(Path.cwd(), self.local), optional=self.optional, @@ -264,7 +283,8 @@ def run( *arguments: str, config: Path, state: State, - get: Iterator[Get], + get: Iterator[FileTransfer], + put: Iterator[FileTransfer], env: Iterator[str], log_level: int, tools: Tools = Tools(), @@ -291,7 +311,12 @@ def run( shell = target.get_driver("CommandProtocol") transfer = target.get_driver("FileTransferProtocol") with TemporaryDirectory( - shell, transfer, tools.mktemp, tools.rm, tools.mkdir + shell, + transfer, + tools.mktemp, + tools.rm, + tools.mkdir, + tools.mv, ) as temp: # Transfer the provided program over to the device remote = temp / program.name @@ -310,6 +335,12 @@ def run( # Transfer 'env' binary over to the device transfer.put(f"{tools.env}", f"{temp / tools.env.name}") + # Transfer user defined files + for unresolved in put: + resolved = unresolved.resolve(temp) + temp.mkdir(resolved.remote.parent) + transfer.put(resolved.local, resolved.remote) + # Construct the CLI using env, the env vars and then the program and args. cmd = join((f"{remote}", *arguments)) cmd = f"cd {temp} && {temp / tools.env.name} {' '.join(env)} {cmd}" @@ -350,6 +381,7 @@ def main(exe: Path, *args: str) -> int: state=parsed.state, config=parsed.config, get=parsed.get, + put=parsed.put, env=parsed.env, log_level=parsed.log_level, )