Repository: pantsbuild/pants

Issue:
Option `fromfile`s are loaded relative to CWD
**Describe the bug**
```terminal
cd src
../pants --no-pantsd peek ::
```

results in `Failed to read template_by_globs in scope 'preamble' from file build-support/preambles/config.yaml: FileNotFoundError(2, 'No such file or directory')`

**Pants version**
`main`

**OS**
N/A

**Additional info**
womp womp



Discussion:

Resolving patch:
diff --git a/src/python/pants/option/parser.py b/src/python/pants/option/parser.py
--- a/src/python/pants/option/parser.py
+++ b/src/python/pants/option/parser.py
@@ -573,14 +573,13 @@ def expand(val_or_str):
                 else:
                     fromfile = val_or_str[1:]
                     try:
-                        with open(fromfile) as fp:
-                            s = fp.read()
-                            if fromfile.endswith(".json"):
-                                return json.loads(s)
-                            elif fromfile.endswith(".yml") or fromfile.endswith(".yaml"):
-                                return yaml.safe_load(s)
-                            else:
-                                return s.strip()
+                        contents = Path(get_buildroot(), fromfile).read_text()
+                        if fromfile.endswith(".json"):
+                            return json.loads(contents)
+                        elif fromfile.endswith(".yml") or fromfile.endswith(".yaml"):
+                            return yaml.safe_load(contents)
+                        else:
+                            return contents.strip()
                     except (OSError, ValueError, yaml.YAMLError) as e:
                         raise FromfileError(
                             f"Failed to read {dest} in {self._scope_str()} from file {fromfile}: {e!r}"


Test patch:
diff --git a/src/python/pants/option/options_test.py b/src/python/pants/option/options_test.py
--- a/src/python/pants/option/options_test.py
+++ b/src/python/pants/option/options_test.py
@@ -9,6 +9,7 @@
 from contextlib import contextmanager
 from enum import Enum
 from functools import partial
+from pathlib import Path
 from textwrap import dedent
 from typing import Any, Callable, Dict, cast
 
@@ -17,6 +18,7 @@
 import yaml
 from packaging.version import Version
 
+from pants.base.build_environment import get_buildroot
 from pants.base.deprecated import CodeRemovedError, warn_or_error
 from pants.base.hash_utils import CoercingEncoder
 from pants.engine.fs import FileContent
@@ -46,7 +48,7 @@
 from pants.option.ranked_value import Rank, RankedValue
 from pants.option.scope import GLOBAL_SCOPE, ScopeInfo
 from pants.option.subsystem import Subsystem
-from pants.util.contextutil import temporary_file, temporary_file_path
+from pants.util.contextutil import pushd, temporary_dir, temporary_file, temporary_file_path
 
 _FAKE_CUR_VERSION = "1.0.0.dev0"
 
@@ -1517,6 +1519,15 @@ def test_fromfile_yaml_trailing_newlines_matter(self) -> None:
             options = self._parse(flags=f"fromfile --{'dictvalue'}=@{fp.name}")
             assert {"a": "multiline\n"} == options.for_scope("fromfile")["dictvalue"]
 
+    def test_fromfile_relative_to_build_root(self) -> None:
+        with temporary_dir(root_dir=get_buildroot()) as tempdir:
+            dirname = tempdir.split("/")[-1]
+            tempfile = Path(tempdir, "config")
+            tempfile.write_text("{'a': 'multiline\\n'}")
+            with pushd(tempdir):
+                options = self._parse(flags=f"fromfile --dictvalue=@{dirname}/config")
+                assert {"a": "multiline\n"} == options.for_scope("fromfile")["dictvalue"]
+
     def test_fromfile_error(self) -> None:
         options = self._parse(flags="fromfile --string=@/does/not/exist")
         with pytest.raises(FromfileError):