Repository: pantsbuild/pants

Issue:
2.20 changes `adhoc_tool` to resolve target specs in `execution_dependencies` relative to `runnable`, not target
**Describe the bug**

It seems 2.20 changes behaviour for how the dependency target specs are resolved in a BUILD file like:

```python
file(name="f", source="f.txt")

adhoc_tool(
    name="tool",
    runnable="a:cat",
    args=["f.txt"],
    execution_dependencies=[":f"],
    log_output=True,
)
```

If that's in `b/`, then the `execution_dependencies` field refers to:

- 2.19: `b:f`
- 2.20: `a:f`

This is a regression/breakage that may require users to update code, so, if it's a desirable change, we'd _preferably_ not do it without warning/deprecation (although it is experimental, so maybe we decide it's worth it)... but it seems undesirable?

Reproducer:

```shell
cd $(mktemp -d)

cat > pants.toml <<EOF
[GLOBAL]
pants_version = "2.20.0a0"

backend_packages = [
  "pants.backend.experimental.adhoc",
]
EOF

mkdir -p a b

cat > a/BUILD <<EOF
system_binary(name="cat", binary_name="cat")
EOF

cat > b/BUILD <<EOF
file(name="f", source="f.txt")

adhoc_tool(
    name="tool",
    runnable="a:cat",
    args=["f.txt"],
    execution_dependencies=[":f"],
    log_output=True,
)
EOF

echo "contents of f.txt here" > b/f.txt

# OKAY: successfully cats the file, and exports
PANTS_VERSION=2.19.0 pants export-codegen b:tool

# BUG: 'ResolveError: The address a:f from the `execution_dependencies` from the target a:cat does not exist.'
pants export-codegen b:tool
```



**Pants version**
2.20.0a0

This appears to be be a regression from 2.20.0.dev3 (works like 2.19) to 2.20.0.dev4 (has this new behaviour).

- https://github.com/pantsbuild/pants/releases/tag/release_2.20.0.dev4
- https://github.com/pantsbuild/pants/compare/release_2.20.0.dev3...release_2.20.0.dev4


**OS**

macOS

**Additional info**
N/A


Discussion:

Resolving patch:
diff --git a/src/python/pants/core/util_rules/adhoc_process_support.py b/src/python/pants/core/util_rules/adhoc_process_support.py
--- a/src/python/pants/core/util_rules/adhoc_process_support.py
+++ b/src/python/pants/core/util_rules/adhoc_process_support.py
@@ -411,7 +411,7 @@ async def create_tool_runner(
         Get(
             ResolvedExecutionDependencies,
             ResolveExecutionDependenciesRequest(
-                address=runnable_address,
+                address=request.target.address,
                 execution_dependencies=request.execution_dependencies,
                 runnable_dependencies=request.runnable_dependencies,
             ),


Test patch:
diff --git a/src/python/pants/backend/adhoc/adhoc_tool_test.py b/src/python/pants/backend/adhoc/adhoc_tool_test.py
--- a/src/python/pants/backend/adhoc/adhoc_tool_test.py
+++ b/src/python/pants/backend/adhoc/adhoc_tool_test.py
@@ -268,3 +268,37 @@ def test_env_vars(rule_runner: PythonRuleRunner) -> None:
         Address("src", target_name="envvars"),
         expected_contents={"out.log": f"{envvar_value}\n"},
     )
+
+
+def test_execution_dependencies_and_runnable_dependencies(rule_runner: PythonRuleRunner) -> None:
+    file_contents = "example contents"
+
+    rule_runner.write_files(
+        {
+            # put the runnable in its own directory, so we're sure that the dependencies are
+            # resolved relative to the adhoc_tool target
+            "a/BUILD": """system_binary(name="bash", binary_name="bash")""",
+            "b/BUILD": dedent(
+                """
+                system_binary(name="renamed_cat", binary_name="cat")
+                files(name="f", sources=["f.txt"])
+
+                adhoc_tool(
+                  name="deps",
+                  runnable="a:bash",
+                  args=["-c", "renamed_cat f.txt"],
+                  execution_dependencies=[":f"],
+                  runnable_dependencies=[":renamed_cat"],
+                  stdout="stdout",
+                )
+                """
+            ),
+            "b/f.txt": file_contents,
+        }
+    )
+
+    assert_adhoc_tool_result(
+        rule_runner,
+        Address("b", target_name="deps"),
+        expected_contents={"b/stdout": file_contents},
+    )
diff --git a/src/python/pants/backend/adhoc/code_quality_tool_integration_test.py b/src/python/pants/backend/adhoc/code_quality_tool_integration_test.py
--- a/src/python/pants/backend/adhoc/code_quality_tool_integration_test.py
+++ b/src/python/pants/backend/adhoc/code_quality_tool_integration_test.py
@@ -10,6 +10,8 @@
     CodeQualityToolUnsupportedGoalError,
     base_rules,
 )
+from pants.backend.adhoc.run_system_binary import rules as run_system_binary_rules
+from pants.backend.adhoc.target_types import SystemBinaryTarget
 from pants.backend.python import register as register_python
 from pants.backend.python.target_types import PythonSourceTarget
 from pants.core.goals.fix import Fix
@@ -35,6 +37,7 @@ def make_rule_runner(*cfgs: CodeQualityToolRuleBuilder):
         *core_rules(),
         *process.rules(),
         *register_python.rules(),
+        *run_system_binary_rules(),
         *base_rules(),
     ]
     for cfg in cfgs:
@@ -45,6 +48,7 @@ def make_rule_runner(*cfgs: CodeQualityToolRuleBuilder):
             CodeQualityToolTarget,
             FileTarget,
             PythonSourceTarget,
+            SystemBinaryTarget,
         ],
         rules=rules,
     )
@@ -276,3 +280,48 @@ def test_several_formatters():
     assert "badtogood made changes" in res.stderr
     assert "underscoreit" not in res.stderr
     assert "goodcode = 50\ngoodcode = 100\n" == rule_runner.read_file("do_not_underscore.py")
+
+
+def test_execution_dependencies_and_runnable_dependencies():
+    input_contents = "input contents\n"
+    output_contents = "original output contents"
+    # confirm that comparing these strings confirms that the test behaved as expected
+    assert input_contents != output_contents
+
+    cfg = CodeQualityToolRuleBuilder(
+        goal="fmt", target="b:overwriter", name="Overwriter", scope="overwriter"
+    )
+
+    # Formatter that copies the `b/input.txt` file over `b/output.txt` file via bash (relying on
+    # there being only one file, and a workaround for #19103 meaning we can't use `cp` as a system
+    # binary)
+    rule_runner = make_rule_runner(cfg)
+    rule_runner.write_files(
+        {
+            # put the runnable in its own directory, so we're sure that the dependencies are
+            # resolved relative to the code_quality_tool target
+            "a/BUILD": """system_binary(name="bash", binary_name="bash")""",
+            "b/BUILD": dedent(
+                """
+                system_binary(name="renamed_cat", binary_name="cat")
+                file(name="input", source="input.txt")
+                file(name="output", source="output.txt")
+
+                code_quality_tool(
+                    name="overwriter",
+                    runnable="a:bash",
+                    args=["-c", "renamed_cat b/input.txt > $1", "ignored"],
+                    execution_dependencies=[":input"],
+                    runnable_dependencies=[":renamed_cat"],
+                    file_glob_include=["**/*.txt"],
+                )
+                """
+            ),
+            "b/input.txt": input_contents,
+            "b/output.txt": output_contents,
+        }
+    )
+    res = rule_runner.run_goal_rule(Fmt, args=["b/output.txt"])
+    assert res.exit_code == 0
+    assert "overwriter made changes" in res.stderr
+    assert input_contents == rule_runner.read_file("b/output.txt")