Skip to content

Commit

Permalink
Adds workdir field for experimental_shell_command and friends (pa…
Browse files Browse the repository at this point in the history
…ntsbuild#17928)

This changes the behaviour of `experimental_shell_command` and `experimental_run_in_sandbox`: processes are now run in the buildroot by default, a `workdir` must be specified explicitly to run in any other directory, including the one where the target is defined.

If `workdir` is set, the output files are returned relative to the `workdir`, as before. There is possibly an argument in favour of adding an `output_prefix` field before stabilising.

Fixes pantsbuild#16807.
  • Loading branch information
Christopher Neugebauer authored Jan 9, 2023
1 parent c676ef9 commit 7360eae
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 21 deletions.
25 changes: 21 additions & 4 deletions src/python/pants/backend/shell/target_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,13 @@ class RunInSandboxSourcesField(MultipleSourcesField):
expected_num_files = 0


class ShellCommandIsInteractiveField(MultipleSourcesField):
# We use this to determine whether this is an interactive process.
alias = "_is_interactive"
uses_source_roots = False
expected_num_files = 0


class RunInSandboxArgumentsField(StringSequenceField):
alias = "args"
default = ()
Expand Down Expand Up @@ -421,10 +428,16 @@ class ShellCommandLogOutputField(BoolField):
help = "Set to true if you want the output from the command logged to the console."


class ShellCommandRunWorkdirField(StringField):
class ShellCommandWorkdirField(StringField):
alias = "workdir"
default = "."
help = "Sets the current working directory of the command, relative to the project root."
default = None
help = softwrap(
"Sets the current working directory of the command, relative to the project root. If not "
"set, use the project root.\n\n"
"To specify the location of the `BUILD` file, use `.`. Values beginning with `.` are "
"relative to the location of the `BUILD` file.\n\n"
"To specify the project/build root, use `/` or the empty string."
)


class ShellCommandTestDependenciesField(ShellCommandExecutionDependenciesField):
Expand Down Expand Up @@ -452,6 +465,7 @@ class ShellCommandTarget(Target):
ShellCommandTimeoutField,
ShellCommandToolsField,
ShellCommandExtraEnvVarsField,
ShellCommandWorkdirField,
EnvironmentField,
)
help = softwrap(
Expand Down Expand Up @@ -494,6 +508,7 @@ class ShellRunInSandboxTarget(Target):
ShellCommandTimeoutField,
ShellCommandToolsField,
ShellCommandExtraEnvVarsField,
ShellCommandWorkdirField,
EnvironmentField,
)
help = softwrap(
Expand Down Expand Up @@ -521,7 +536,8 @@ class ShellCommandRunTarget(Target):
*COMMON_TARGET_FIELDS,
ShellCommandExecutionDependenciesField,
ShellCommandCommandField,
ShellCommandRunWorkdirField,
ShellCommandWorkdirField,
ShellCommandIsInteractiveField,
)
help = softwrap(
"""
Expand Down Expand Up @@ -558,6 +574,7 @@ class ShellCommandTestTarget(Target):
ShellCommandExtraEnvVarsField,
EnvironmentField,
SkipShellCommandTestsField,
ShellCommandWorkdirField,
)
help = softwrap(
"""
Expand Down
41 changes: 31 additions & 10 deletions src/python/pants/backend/shell/util_rules/shell_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@
ShellCommandCommandField,
ShellCommandExecutionDependenciesField,
ShellCommandExtraEnvVarsField,
ShellCommandIsInteractiveField,
ShellCommandLogOutputField,
ShellCommandOutputDependenciesField,
ShellCommandOutputDirectoriesField,
ShellCommandOutputFilesField,
ShellCommandOutputsField,
ShellCommandRunWorkdirField,
ShellCommandSourcesField,
ShellCommandTimeoutField,
ShellCommandToolsField,
ShellCommandWorkdirField,
)
from pants.backend.shell.util_rules.builtin import BASH_BUILTIN_COMMANDS
from pants.base.deprecated import warn_or_error
Expand Down Expand Up @@ -84,7 +85,7 @@ class GenerateFilesFromRunInSandboxRequest(GenerateSourcesRequest):
class ShellCommandProcessRequest:
description: str
interactive: bool
working_directory: str
working_directory: str | None
command: str
timeout: int | None
tools: tuple[str, ...]
Expand All @@ -105,11 +106,13 @@ class ShellCommandProcessFromTargetRequest:
async def _prepare_process_request_from_target(shell_command: Target) -> ShellCommandProcessRequest:
description = f"the `{shell_command.alias}` at `{shell_command.address}`"

interactive = shell_command.has_field(ShellCommandRunWorkdirField)
if interactive:
working_directory = shell_command[ShellCommandRunWorkdirField].value or ""
else:
working_directory = shell_command.address.spec_path
interactive = shell_command.has_field(ShellCommandIsInteractiveField)
working_directory = _parse_working_directory(
shell_command[ShellCommandWorkdirField].value or "", shell_command.address
)

if interactive and not working_directory:
working_directory = "."

command = shell_command[ShellCommandCommandField].value
if not command:
Expand Down Expand Up @@ -240,7 +243,7 @@ async def prepare_process_request_from_target(
class RunShellCommand(RunFieldSet):
required_fields = (
ShellCommandCommandField,
ShellCommandRunWorkdirField,
ShellCommandWorkdirField,
)
run_in_sandbox_behavior = RunInSandboxBehavior.NOT_SUPPORTED

Expand Down Expand Up @@ -324,7 +327,9 @@ async def run_in_sandbox_request(
)
run_field_set: RunFieldSet = field_sets.field_sets[0]

working_directory = shell_command.address.spec_path
working_directory = _parse_working_directory(
shell_command[ShellCommandWorkdirField].value or "", shell_command.address
)

# Must be run in target environment so that the binaries/envvars match the execution
# environment when we actually run the process.
Expand Down Expand Up @@ -456,8 +461,9 @@ async def prepare_shell_command_process(
input_digest = await Get(Digest, MergeDigests([shell_command.input_digest, work_dir]))

if interactive:
_working_directory = working_directory or "."
relpath = os.path.relpath(
working_directory or ".", start="/" if os.path.isabs(working_directory) else "."
_working_directory or ".", start="/" if os.path.isabs(_working_directory) else "."
)
boot_script = f"cd {shlex.quote(relpath)}; " if relpath != "." else ""
else:
Expand Down Expand Up @@ -516,6 +522,21 @@ def _output_at_build_root(process: Process, bash: BashBinary) -> Process:
)


def _parse_working_directory(workdir_in: str, address: Address) -> str:
"""Convert the `workdir` field into something that can be understood by `Process`."""

reldir = address.spec_path

if workdir_in == ".":
return reldir
elif workdir_in.startswith("./"):
return os.path.join(reldir, workdir_in[2:])
elif workdir_in.startswith("/"):
return workdir_in[1:]
else:
return workdir_in


@rule
async def run_shell_command_request(shell_command: RunShellCommand) -> RunRequest:
wrapped_tgt = await Get(
Expand Down
Loading

0 comments on commit 7360eae

Please sign in to comment.