Skip to content

Commit 3da4be2

Browse files
support setting the working directory on interactive process (#22802)
We've run into a few cases where setting the working directory on an InteractiveProcess would be useful, in particular for rules are tests (ie TestRequest). In order to support debugging we convert the setup process to InteractiveProcess but since that drops the working directory you're unable to utilize it currently. As a workaround we currently write a bash script into the sandbox like ``` cd ${DIR} && exec {COMMAND} ``` This will help clean that up This is also a pre-requisite to supporting setting the working dir in the Run goal (small thread here https://pantsbuild.slack.com/archives/C01CQHVDMMW/p1707448457657149) I'll handle that in a separate pr though
1 parent 66941f5 commit 3da4be2

File tree

4 files changed

+70
-2
lines changed

4 files changed

+70
-2
lines changed

docs/notes/2.31.x.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ Please provide feedback on this backend [here](https://github.com/pantsbuild/pan
8787

8888
Pants no longer supports loading `pkg_resources`-style namespace packages for plugins. Instead, just use ["native namespace packages"](https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages) as per [PEP 420](https://peps.python.org/pep-0420/).
8989

90+
Allow `InteractiveProcess` to set the working directory relative to the sandbox or workspace. Existing usages of `InteractiveProcess.from_process` will now respect the working directory if its set on the Process, which may be a breaking change depending on the use case. Two existing rules `twine_upload` for python package uploads and `test_shell_command_interactively` for shell command testing with the `--debug` flag will now honor the working directory if set on the Process.
91+
9092
#### nFPM backend
9193

9294
Added a new rule to help in-repo plugins implement the `inject_nfpm_package_fields(InjectNfpmPackageFieldsRequest) -> InjectedNfpmPackageFields` polymorphic rule. The `get_package_field_sets_for_nfpm_content_file_deps` rule (in the `pants.backend.nfpm.util_rules.contents` module) collects selected `PackageFieldSet`s from the contents of an `nfpm_*_package` so that the packages can be analyzed to inject things like package requirements.

src/python/pants/engine/process.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ def __init__(
441441
append_only_caches: Mapping[str, str] | None = None,
442442
immutable_input_digests: Mapping[str, Digest] | None = None,
443443
keep_sandboxes: KeepSandboxes = KeepSandboxes.never,
444+
working_directory: str | None = None,
444445
) -> None:
445446
"""Request to run a subprocess in the foreground, similar to subprocess.run().
446447
@@ -463,6 +464,7 @@ def __init__(
463464
input_digest=input_digest,
464465
append_only_caches=append_only_caches,
465466
immutable_input_digests=immutable_input_digests,
467+
working_directory=working_directory,
466468
),
467469
)
468470
object.__setattr__(self, "run_in_workspace", run_in_workspace)
@@ -489,6 +491,7 @@ def from_process(
489491
append_only_caches=process.append_only_caches,
490492
immutable_input_digests=process.immutable_input_digests,
491493
keep_sandboxes=keep_sandboxes,
494+
working_directory=process.working_directory,
492495
)
493496

494497

src/python/pants/engine/process_test.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from __future__ import annotations
55

6+
import os
67
import textwrap
78
from pathlib import Path
89

@@ -311,6 +312,55 @@ def test_interactive_process_inputs(rule_runner: RuleRunner, run_in_workspace: b
311312
}
312313

313314

315+
@pytest.mark.parametrize("working_directory", [None, "foo", "foo/bar"])
316+
@pytest.mark.parametrize("run_in_workspace", [True, False])
317+
def test_interactive_process_working_directory(
318+
rule_runner: RuleRunner,
319+
working_directory: str,
320+
run_in_workspace: bool,
321+
) -> None:
322+
# Test that interactive processes can run in a working directory, and that the
323+
# working directory is correctly resolved relative to the sandbox root or workspace root.
324+
rule_runner.write_files(
325+
{
326+
"test.txt": "workspace test.txt",
327+
"foo/test.txt": "workspace foo/test.txt",
328+
"foo/bar/test.txt": "workspace foo/bar/test.txt",
329+
}
330+
)
331+
input_digest = rule_runner.make_snapshot(
332+
{
333+
"test.txt": "chroot test.txt",
334+
"foo/test.txt": "chroot foo/test.txt",
335+
"foo/bar/test.txt": "chroot foo/bar/test.txt",
336+
}
337+
).digest
338+
339+
process = InteractiveProcess(
340+
argv=["/bin/bash", "-c", "pwd && cat test.txt"],
341+
input_digest=input_digest,
342+
working_directory=working_directory,
343+
run_in_workspace=run_in_workspace,
344+
)
345+
346+
with mock_console(rule_runner.options_bootstrapper) as (_, stdio_reader):
347+
result = rule_runner.run_interactive_process(process)
348+
stdout = stdio_reader.get_stdout()
349+
stderr = stdio_reader.get_stderr()
350+
assert result.exit_code == 0, (
351+
f"Process failed with exit code {result.exit_code}.\nstdout: {stdout}\nstderr: {stderr}"
352+
)
353+
354+
lines = stdout.splitlines()
355+
assert len(lines) == 2
356+
if working_directory is not None:
357+
assert lines[0].endswith(working_directory)
358+
359+
expected_prefix = "workspace" if run_in_workspace else "chroot"
360+
expected_path = os.path.join(*(s for s in (working_directory, "test.txt") if s))
361+
assert f"{expected_prefix} {expected_path}" in stdout
362+
363+
314364
def test_workspace_execution_support() -> None:
315365
rule_runner = RuleRunner(
316366
rules=[

src/rust/engine/src/intrinsics/interactive_process.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,22 @@ pub async fn interactive_process_inner(
133133
};
134134

135135
let mut command = process::Command::new(program_name);
136-
if !run_in_workspace {
137-
command.current_dir(tempdir.path());
136+
137+
let cwd = match (&process.working_directory, run_in_workspace) {
138+
(Some(working_directory), true) => Some(
139+
current_dir()
140+
.map_err(|e| format!("Could not detect current working directory: {e}"))?
141+
.join(working_directory),
142+
),
143+
(Some(working_directory), false) => Some(tempdir.path().join(working_directory)),
144+
(None, false) => Some(tempdir.path().to_owned()),
145+
(None, true) => None,
146+
};
147+
148+
if let Some(cwd) = cwd {
149+
command.current_dir(cwd);
138150
}
151+
139152
for arg in process.argv[1..].iter() {
140153
command.arg(arg);
141154
}

0 commit comments

Comments
 (0)