Repository: pantsbuild/pants

Issue:
`root_output_directory` value is awkward when chaining `experimental_shell_command`/`experimental_run_in_sandbox` targets
per @huonw in #18157:

> 
> > This addresses the ../../../docs.html example listed in @huonw's https://github.com/pantsbuild/pants/issues/17616#issuecomment-1378329146 on https://github.com/pantsbuild/pants/issues/17616: The correct incantation would allow running in the working directory of the script, and the docs.html would be at the digest root automatically.
> >
> > @huonw if you're game, I'd love some verification that this does what you want.
> 
> (Thanks @chrisjrn for waiting while I was on leave.)
> 
> I had a play with this. I think the new working directory default is a good change 👍
> 
> However, I found I got kinda confused for two reasons:
> 
> 1. the new `root_output_directory` and its default
> 2. behaviour when `output_files` isn't found, but that's not directly related to the change here, so I've filed https://github.com/pantsbuild/pants/issues/18183
> 
> ---
> 
> For 1, I think there's tension between:
> 
> 1. "internal" commands, like `npm ci` (`:node_modules` in the case study in https://github.com/pantsbuild/pants/issues/17616#issuecomment-1378329146).
> 2. archive/external ones.
>  
> In particular, with the change here, it seems like `npm ci` etc. will be installing to `$CHROOT/path/to/target/node_modules` and then moving to `$CHROOT/node_modules` for later commands that use it. `npm` happens to handle this fine (it'll traverse parents), but this seems rife for confusion (e.g. commands that don't traverse parents) and conflicts (e.g. if there's two commands that have their own "internal"/"private" `node_modules` (aka resolves), and both of those are pulled into the same sandbox 💥 ). 
> 
> Proposal: Maybe `root_output_directory` could continue to default to `/`, but a user can set it to `"."` (or whatever) to do relocation, if they want?

I'm fine with this default, and will make this change.


Discussion:

Resolving patch:
diff --git a/src/python/pants/backend/shell/target_types.py b/src/python/pants/backend/shell/target_types.py
--- a/src/python/pants/backend/shell/target_types.py
+++ b/src/python/pants/backend/shell/target_types.py
@@ -462,14 +462,14 @@ class RunShellCommandWorkdirField(ShellCommandWorkdirField):
 
 class ShellCommandOutputRootDirField(StringField):
     alias = "root_output_directory"
-    default = "."
+    default = "/"
     help = softwrap(
         "Adjusts the location of files output by this command, when consumed as a dependency.\n\n"
-        "`.` or values beginning with `./` are relative to the location of the `BUILD` file.\n\n"
-        "`.` or values beginning with `./` are relative to the value of `workdir`.\n\n"
-        "To specify the build root, use `/` or the empty string.\n\n"
-        "Values that do not begin with `.` or `/` are relative to the build root.\n\n"
-        "All files output by "
+        "Values are relative to the build root, except in the following cases:\n\n"
+        "* `.` specifies the location of the `BUILD` file.\n"
+        "* Values beginning with `./` are relative to the location of the `BUILD` file.\n"
+        "* `/` or the empty string specifies the build root.\n"
+        "* Values beginning with `/` are also relative to the build root."
     )
 
 


Test patch:
diff --git a/src/python/pants/backend/shell/util_rules/shell_command_test.py b/src/python/pants/backend/shell/util_rules/shell_command_test.py
--- a/src/python/pants/backend/shell/util_rules/shell_command_test.py
+++ b/src/python/pants/backend/shell/util_rules/shell_command_test.py
@@ -134,6 +134,7 @@ def test_sources_and_files(rule_runner: RuleRunner) -> None:
                   output_files=["message.txt"],
                   output_directories=["res"],
                   command="./script.sh",
+                  root_output_directory=".",
                 )
 
                 files(
@@ -177,6 +178,7 @@ def test_quotes_command(rule_runner: RuleRunner) -> None:
                   tools=["echo", "tee"],
                   command='echo "foo bar" | tee out.log',
                   output_files=["out.log"],
+                  root_output_directory=".",
                 )
                 """
             ),
@@ -200,7 +202,6 @@ def test_chained_shell_commands(rule_runner: RuleRunner) -> None:
                   tools=["echo"],
                   output_files=["../msg"],
                   command="echo 'shell_command:a' > ../msg",
-                  root_output_directory="/",
                 )
                 """
             ),
@@ -212,7 +213,6 @@ def test_chained_shell_commands(rule_runner: RuleRunner) -> None:
                   output_files=["../msg"],
                   command="echo 'shell_command:b' >> ../msg",
                   execution_dependencies=["src/a:msg"],
-                  root_output_directory="/",
                 )
                 """
             ),
@@ -339,7 +339,8 @@ def test_shell_command_masquerade_as_a_files_target(rule_runner: RuleRunner) ->
                   name="content-gen",
                   command="echo contents > contents.txt",
                   tools=["echo"],
-                  output_files=["contents.txt"]
+                  output_files=["contents.txt"],
+                  root_output_directory=".",
                 )
                 """
             ),
@@ -744,6 +745,7 @@ def test_run_runnable_in_sandbox(rule_runner: RuleRunner) -> None:
                   name="run_fruitcake",
                   runnable=":fruitcake",
                   output_files=["fruitcake.txt"],
+                  root_output_directory=".",
                 )
                 """
             ),
@@ -814,6 +816,7 @@ def test_run_in_sandbox_capture_stdout_err(rule_runner: RuleRunner) -> None:
                   runnable=":fruitcake",
                   stdout="stdout",
                   stderr="stderr",
+                  root_output_directory=".",
                 )
                 """
             ),
@@ -840,7 +843,6 @@ def test_relative_directories(rule_runner: RuleRunner) -> None:
                   tools=["echo"],
                   command='echo foosh > ../foosh.txt',
                   output_files=["../foosh.txt"],
-                  root_output_directory="/",
                 )
                 """
             ),
@@ -864,7 +866,6 @@ def test_relative_directories_2(rule_runner: RuleRunner) -> None:
                   tools=["echo"],
                   command='echo foosh > ../newdir/foosh.txt',
                   output_files=["../newdir/foosh.txt"],
-                  root_output_directory="/",
                 )
                 """
             ),
@@ -888,7 +889,6 @@ def test_cannot_escape_build_root(rule_runner: RuleRunner) -> None:
                   tools=["echo"],
                   command='echo foosh > ../../invalid.txt',
                   output_files=["../../invalid.txt"],
-                  root_output_directory="/",
                 )
                 """
             ),
@@ -938,7 +938,6 @@ def test_working_directory_special_values(
                   runnable=":fruitcake",
                   output_files=["fruitcake.txt"],
                   workdir="{workdir}",
-                  root_output_directory="/",
                 )
                 """
             ),