Repository: pantsbuild/pants

Issue:
ResolveError: Directory '{mydir}' does not contain any BUILD files (when Dockerizing packages)
**Describe the bug**

Created a repo at https://github.com/sureshjoshi/pantsbuild-14031 to help illustrate this problem. 

Essentially, I use custom output paths for my .pex files, and while testing out the `docker_image` target, I noticed some of my components fail with the error 

> ResolveError: Directory 'backend' does not contain any BUILD files

After a lot of debugging, I only ran into this problem when my output folders were common to multiple `pex_binary` targets. 

For example, in the repo above, I have 3 identical projects (A, B, C) - where they only differ by the `pex_binary` `output_path` (and this location updated in the associated Dockerfile), and one of the projects refuses to compile.

As per the README in the repo:

```bash
# Should create a pex at dist/backend/projecta/projecta.pex
# Docker image created successfully as projecta-container:latest
./pants package backend/projecta::

# Should create a pex at dist/backend.projectc/projectc.pex
# Docker image created successfully as projectc-container:latest
./pants package backend/projectc::
```

```bash
# Should create a pex at dist/backend/projectb.pex
./pants package backend/projectb:projectb

# FAILS: With ResolveError
./pants package backend/projectb:projectb-container 
```

So, the difference above is that Project C uses no `output_path` and uses the dot-syntax for the dist folder. ProjectA places the pex file under a `backend/projecta` directory. The failing ProjectB places the pex file directly under `backend`.

This isn't a big issue, and easily worked around, and I'm guessing it has to do with namespacing or module/package semantics, but it's just a weird problem that is difficult to debug based on the error message.

**Pants version**

- 2.8.0
- 2.9.0rc1

**OS**

macOS 12.1
Untested on Linux



Discussion:
@kaos : Any thoughts here?
We had a holiday in Sweden yesterday, and I have another day off today, so will be a while before I can take a 👀 .

👍🏽  for the repro project, most excellent, thank you.


Resolving patch:
diff --git a/src/python/pants/backend/docker/util_rules/dependencies.py b/src/python/pants/backend/docker/util_rules/dependencies.py
--- a/src/python/pants/backend/docker/util_rules/dependencies.py
+++ b/src/python/pants/backend/docker/util_rules/dependencies.py
@@ -3,6 +3,7 @@
 
 from pants.backend.docker.subsystems.dockerfile_parser import DockerfileInfo, DockerfileInfoRequest
 from pants.backend.docker.target_types import DockerDependenciesField
+from pants.base.specs import AddressSpecs, MaybeEmptySiblingAddresses
 from pants.core.goals.package import PackageFieldSet
 from pants.engine.addresses import Addresses, UnparsedAddressInputs
 from pants.engine.rules import Get, collect_rules, rule
@@ -22,18 +23,28 @@ class InjectDockerDependencies(InjectDependenciesRequest):
 
 @rule
 async def inject_docker_dependencies(request: InjectDockerDependencies) -> InjectedDependencies:
-    """Inspects COPY instructions in the Dockerfile for references to known targets."""
+    """Inspects COPY instructions in the Dockerfile for references to known packagable targets."""
     dockerfile_info = await Get(
         DockerfileInfo, DockerfileInfoRequest(request.dependencies_field.address)
     )
 
-    targets = await Get(
-        Targets,
+    # Parse all putative target addresses.
+    putative_addresses = await Get(
+        Addresses,
         UnparsedAddressInputs(
             dockerfile_info.putative_target_addresses,
             owning_address=dockerfile_info.address,
         ),
     )
+
+    # Get the target for those addresses that are known.
+    directories = {address.spec_path for address in putative_addresses}
+    all_addresses = await Get(Addresses, AddressSpecs(map(MaybeEmptySiblingAddresses, directories)))
+    targets = await Get(
+        Targets, Addresses((address for address in putative_addresses if address in all_addresses))
+    )
+
+    # Only keep those targets that we can "package".
     package = await Get(FieldSetsPerTarget, FieldSetsPerTargetRequest(PackageFieldSet, targets))
     referenced_targets = (
         field_sets[0].address for field_sets in package.collection if len(field_sets) > 0


Test patch:
diff --git a/src/python/pants/backend/docker/util_rules/docker_build_context_test.py b/src/python/pants/backend/docker/util_rules/docker_build_context_test.py
--- a/src/python/pants/backend/docker/util_rules/docker_build_context_test.py
+++ b/src/python/pants/backend/docker/util_rules/docker_build_context_test.py
@@ -286,7 +286,7 @@ def test_packaged_pex_path(rule_runner: RuleRunner) -> None:
     rule_runner.write_files(
         {
             "src/docker/BUILD": """docker_image(dependencies=["src/python/proj/cli:bin"])""",
-            "src/docker/Dockerfile": """FROM python""",
+            "src/docker/Dockerfile": """FROM python:3.8""",
             "src/python/proj/cli/BUILD": """pex_binary(name="bin", entry_point="main.py")""",
             "src/python/proj/cli/main.py": """print("cli main")""",
         }
@@ -593,3 +593,38 @@ def test_create_docker_build_context() -> None:
     assert dict(context.build_env.environment) == {"ENVNAME": "value2"}
     assert context.dockerfile == "test/Dockerfile"
     assert context.stages == ("base", "dev", "prod")
+
+
+def test_pex_custom_output_path_issue14031(rule_runner: RuleRunner) -> None:
+    rule_runner.write_files(
+        {
+            "project/test/BUILD": dedent(
+                """\
+                pex_binary(
+                  name="test",
+                  entry_point="main.py",
+                  output_path="project/test.pex",
+                )
+
+                docker_image(
+                  name="test-image",
+                  dependencies=[":test"],
+                )
+                """
+            ),
+            "project/test/main.py": "print('Hello')",
+            "project/test/Dockerfile": dedent(
+                """\
+                FROM python:3.8
+                COPY project/test.pex .
+                CMD ["./test.pex"]
+                """
+            ),
+        }
+    )
+
+    assert_build_context(
+        rule_runner,
+        Address("project/test", target_name="test-image"),
+        expected_files=["project/test/Dockerfile", "project/test.pex"],
+    )