Repository: pantsbuild/pants

Issue:
Target in subproject fails with relative dependency
# Problem
using the `--suproject_roots` option, it is possible to reference targets from a completely separate pants project found within the current project. All required dependencies are properly resolved when the targets within that subproject specify complete addresses, but fails when relative addresses are used to reference targets in the same build file (eg `dependencies=[':other_target']` ). In the former case, it appears to look for the relative target using the root of the subproject rather than the path to the BUILD file in which that the relative target is specified.

# Example
given the following structure
```
├── pants
├── pants.ini
└── src
    └── python
        └── my_project
            ├── BUILD
            └── my_sub_project
                └── src
                    └── python
                        └── BUILD
```
where
```python
# src/python/my_project/BUILD
python_library(
    dependencies=[
        'src/python/my_project/my_sub_project/src/python:sub_target_1',
    ],
)
```
and
```python
# src/python/my_project/my_sub_project/src/python/BUILD
python_library(
    name='sub_target_1',
    dependencies=[
        ':sub_target_2',
    ]
)
python_library(
    name='sub_target_2',
)
```
and
```ini
# pants.ini
[GLOBAL]
pants_version: 1.4.0rc1

subproject_roots = [
	'src/python/my_project/my_sub_project',
	]
```
The following command produces an unexpected error
 ```
./pants dependencies src/python/my_project
Exception caught: (<class 'pants.build_graph.address_lookup_error.AddressLookupError'>)

Exception message: Directory "Dir(path='src/python/my_project/my_sub_project')" does not contain build files.
```
# Workaround
Currently, if we update the subproject such that:
```python
# src/python/my_project/my_sub_project/src/python/BUILD
python_library(
    name='sub_target_1',
    dependencies=[
        'src/python:sub_target_2',
    ]
)
python_library(
    name='sub_target_2',
)
```
then the command works as expected
```
./pants dependencies src/python/my_project
src/python/my_project:my_project
src/python/my_project/my_sub_project/src/python:sub_target_1
src/python/my_project/my_sub_project/src/python:sub_target_2
```


Discussion:
Thanks for the report!

Resolving patch:
diff --git a/src/python/pants/build_graph/address.py b/src/python/pants/build_graph/address.py
--- a/src/python/pants/build_graph/address.py
+++ b/src/python/pants/build_graph/address.py
@@ -8,7 +8,7 @@
 from builtins import object
 from collections import namedtuple
 
-from pants.util.dirutil import longest_dir_prefix
+from pants.util.dirutil import fast_relpath, longest_dir_prefix
 from pants.util.strutil import strip_prefix
 
 
@@ -73,12 +73,13 @@ def prefix_subproject(spec_path):
 
   spec_parts = spec.rsplit(':', 1)
   if len(spec_parts) == 1:
-    spec_path = prefix_subproject(normalize_absolute_refs(spec_parts[0]))
+    default_target_spec = spec_parts[0]
+    spec_path = prefix_subproject(normalize_absolute_refs(default_target_spec))
     target_name = os.path.basename(spec_path)
   else:
     spec_path, target_name = spec_parts
-    if not spec_path and not subproject and relative_to:
-      spec_path = relative_to
+    if not spec_path and relative_to:
+      spec_path = fast_relpath(relative_to, subproject) if subproject else relative_to
     spec_path = prefix_subproject(normalize_absolute_refs(spec_path))
 
   return spec_path, target_name


Test patch:
diff --git a/tests/python/pants_test/build_graph/test_address.py b/tests/python/pants_test/build_graph/test_address.py
--- a/tests/python/pants_test/build_graph/test_address.py
+++ b/tests/python/pants_test/build_graph/test_address.py
@@ -111,19 +111,46 @@ def parse(spec, relative_to):
                         subproject_roots=[
                           'subprojectA',
                           'path/to/subprojectB',
-                        ])[0]
+                        ])
 
     # Ensure that a spec in subprojectA is determined correctly.
-    subprojectA_spec = parse('src/python/alib', 'subprojectA/src/python')
-    self.assertEqual(subprojectA_spec, 'subprojectA/src/python/alib')
+    spec_path, target_name = parse('src/python/alib', 'subprojectA/src/python')
+    self.assertEqual('subprojectA/src/python/alib', spec_path)
+    self.assertEqual('alib', target_name)
+
+    spec_path, target_name = parse('src/python/alib:jake', 'subprojectA/src/python/alib')
+    self.assertEqual('subprojectA/src/python/alib', spec_path)
+    self.assertEqual('jake', target_name)
+
+    spec_path, target_name = parse(':rel', 'subprojectA/src/python/alib')
+    self.assertEqual('subprojectA/src/python/alib', spec_path)
+    self.assertEqual('rel', target_name)
 
     # Ensure that a spec in subprojectB, which is more complex, is correct.
-    subprojectB_spec = parse('src/python/blib', 'path/to/subprojectB/src/python')
-    self.assertEqual(subprojectB_spec, 'path/to/subprojectB/src/python/blib')
+    spec_path, target_name = parse('src/python/blib', 'path/to/subprojectB/src/python')
+    self.assertEqual('path/to/subprojectB/src/python/blib', spec_path)
+    self.assertEqual('blib', target_name)
+
+    spec_path, target_name = parse('src/python/blib:jane', 'path/to/subprojectB/src/python/blib')
+    self.assertEqual('path/to/subprojectB/src/python/blib', spec_path)
+    self.assertEqual('jane', target_name)
+
+    spec_path, target_name = parse(':rel', 'path/to/subprojectB/src/python/blib')
+    self.assertEqual('path/to/subprojectB/src/python/blib', spec_path)
+    self.assertEqual('rel', target_name)
 
     # Ensure that a spec in the parent project is not mapped.
-    parent_spec = parse('src/python/parent', 'src/python')
-    self.assertEqual(parent_spec, 'src/python/parent')
+    spec_path, target_name = parse('src/python/parent', 'src/python')
+    self.assertEqual('src/python/parent', spec_path)
+    self.assertEqual('parent', target_name)
+
+    spec_path, target_name = parse('src/python/parent:george', 'src/python')
+    self.assertEqual('src/python/parent', spec_path)
+    self.assertEqual('george', target_name)
+
+    spec_path, target_name = parse(':rel', 'src/python/parent')
+    self.assertEqual('src/python/parent', spec_path)
+    self.assertEqual('rel', target_name)
 
 
 class BaseAddressTest(unittest.TestCase):