diff --git a/src/BUILD b/src/BUILD index f45f14fd7..8c9711d98 100644 --- a/src/BUILD +++ b/src/BUILD @@ -65,6 +65,7 @@ java_binary( py_library( name = "plantuml_for_python", srcs = ["@score_docs_as_code//src:dummy.py"], + #deps = ["@score_docs_as_code//src/find_runfiles"], data = ["@score_docs_as_code//src:plantuml"], visibility = ["//visibility:public"], ) diff --git a/src/extensions/BUILD b/src/extensions/BUILD index d4db4293c..660a480e5 100644 --- a/src/extensions/BUILD +++ b/src/extensions/BUILD @@ -20,4 +20,5 @@ py_library( srcs = ["@score_docs_as_code//src/extensions:score_plantuml.py"], imports = ["."], visibility = ["//visibility:public"], + deps = ["@score_docs_as_code//src/find_runfiles"], ) diff --git a/src/extensions/score_metamodel/BUILD b/src/extensions/score_metamodel/BUILD index 30392ca00..285942fcb 100644 --- a/src/extensions/score_metamodel/BUILD +++ b/src/extensions/score_metamodel/BUILD @@ -49,7 +49,10 @@ py_library( imports = ["."], visibility = ["//visibility:public"], # TODO: Figure out if all requirements are needed or if we can break it down a bit - deps = all_requirements + ["@score_docs_as_code//src/helper_lib"], + deps = all_requirements + [ + "@score_docs_as_code//src/find_runfiles", + "@score_docs_as_code//src/helper_lib", + ], ) score_py_pytest( diff --git a/src/extensions/score_metamodel/external_needs.py b/src/extensions/score_metamodel/external_needs.py index 0c48a56a8..5ebe34841 100644 --- a/src/extensions/score_metamodel/external_needs.py +++ b/src/extensions/score_metamodel/external_needs.py @@ -12,9 +12,7 @@ # ******************************************************************************* import json -import os import subprocess -import sys from dataclasses import dataclass from pathlib import Path @@ -23,6 +21,8 @@ from sphinx.util import logging from sphinx_needs.needsfile import NeedsList +from src.find_runfiles import get_runfiles_dir + logger = logging.getLogger(__name__) @@ -138,7 +138,7 @@ def temp(self: NeedsList): def get_external_needs_source(external_needs_source: str) -> list[ExternalNeedsSource]: - bazel = external_needs_source or os.getenv("RUNFILES_DIR") + bazel = external_needs_source or get_runfiles_dir() if bazel: external_needs = parse_external_needs_sources_from_DATA(external_needs_source) @@ -149,25 +149,10 @@ def get_external_needs_source(external_needs_source: str) -> list[ExternalNeedsS def add_external_needs_json(e: ExternalNeedsSource, config: Config): - json_file = f"{e.bazel_module}+/{e.target}/_build/needs/needs.json" - if r := os.getenv("RUNFILES_DIR"): - logger.debug("Using runfiles to determine external needs JSON file.") - fixed_json_file = Path(r) / json_file - else: - logger.debug( - "Running outside bazel. " - + "Determining git root for external needs JSON file." - ) - git_root = Path.cwd().resolve() - while not (git_root / ".git").exists(): - git_root = git_root.parent - if git_root == Path("/"): - sys.exit("Could not find git root.") - logger.debug(f"Git root found: {git_root}") - fixed_json_file = git_root / "bazel-bin" / "ide_support.runfiles" / json_file - - logger.debug(f"Fixed JSON file path: {json_file} -> {fixed_json_file}") - json_file = fixed_json_file + json_file_raw = f"{e.bazel_module}+/{e.target}/_build/needs/needs.json" + r = get_runfiles_dir() + json_file = r / json_file_raw + logger.debug(f"Fixed JSON file path: {json_file_raw} -> {json_file}") try: needs_json_data = json.loads(Path(json_file).read_text(encoding="utf-8")) # pyright: ignore[reportAny] @@ -192,11 +177,11 @@ def add_external_needs_json(e: ExternalNeedsSource, config: Config): def add_external_docs_sources(e: ExternalNeedsSource, config: Config): # Note that bazel does NOT write the files under e.target! # {e.bazel_module}+ matches the original git layout! - if r := os.getenv("RUNFILES_DIR"): - docs_source_path = Path(r) / f"{e.bazel_module}+" - else: + r = get_runfiles_dir() + if "ide_support.runfiles" in str(r): logger.error("Combo builds are currently only supported with Bazel.") return + docs_source_path = Path(r) / f"{e.bazel_module}+" if "collections" not in config: config.collections = {} diff --git a/src/extensions/score_plantuml.py b/src/extensions/score_plantuml.py index c452315e5..dca734610 100644 --- a/src/extensions/score_plantuml.py +++ b/src/extensions/score_plantuml.py @@ -24,48 +24,48 @@ In addition it sets common PlantUML options, like output to svg_obj. """ -import os -import sys from pathlib import Path from sphinx.application import Sphinx from sphinx.util import logging -logger = logging.getLogger(__name__) - +from src.find_runfiles import get_runfiles_dir -def get_runfiles_dir() -> Path: - if r := os.getenv("RUNFILES_DIR"): - # Runfiles are only available when running in Bazel. - # bazel build and bazel run are both supported. - # i.e. `bazel build //:docs` and `bazel run //:docs`. - logger.debug("Using runfiles to determine plantuml path.") +logger = logging.getLogger(__name__) - runfiles_dir = Path(r) - else: - # The only way to land here is when running from within the virtual - # environment created by the `:ide_support` rule in the BUILD file. - # i.e. esbonio or manual sphinx-build execution within the virtual - # environment. - # We'll still use the plantuml binary from the bazel build. - # But we need to find it first. - logger.debug("Running outside bazel.") - - git_root = Path.cwd().resolve() - while not (git_root / ".git").exists(): - git_root = git_root.parent - if git_root == Path("/"): - sys.exit("Could not find git root.") - - runfiles_dir = git_root / "bazel-bin" / "ide_support.runfiles" - - if not runfiles_dir.exists(): - sys.exit( - f"Could not find runfiles_dir at {runfiles_dir}. " - "Have a look at README.md for instructions on how to build docs." - ) - return runfiles_dir +# def get_runfiles_dir() -> Path: +# if r := os.getenv("RUNFILES_DIR"): +# # Runfiles are only available when running in Bazel. +# # bazel build and bazel run are both supported. +# # i.e. `bazel build //:docs` and `bazel run //:docs`. +# logger.debug("Using runfiles to determine plantuml path.") +# +# runfiles_dir = Path(r) +# +# else: +# # The only way to land here is when running from within the virtual +# # environment created by the `:ide_support` rule in the BUILD file. +# # i.e. esbonio or manual sphinx-build execution within the virtual +# # environment. +# # We'll still use the plantuml binary from the bazel build. +# # But we need to find it first. +# logger.debug("Running outside bazel.") +# +# git_root = Path.cwd().resolve() +# while not (git_root / ".git").exists(): +# git_root = git_root.parent +# if git_root == Path("/"): +# sys.exit("Could not find git root.") +# +# runfiles_dir = git_root / "bazel-bin" / "ide_support.runfiles" +# +# if not runfiles_dir.exists(): +# sys.exit( +# f"Could not find runfiles_dir at {runfiles_dir}. " +# "Have a look at README.md for instructions on how to build docs." +# ) +# return runfiles_dir def find_correct_path(runfiles: Path) -> Path: diff --git a/src/find_runfiles/__init__.py b/src/find_runfiles/__init__.py index e46620042..cd4d25082 100644 --- a/src/find_runfiles/__init__.py +++ b/src/find_runfiles/__init__.py @@ -27,14 +27,17 @@ def _log_debug(message: str): print(message) -def find_git_root() -> Path: - # TODO: is __file__ ever resolved into the bazel cache directories? - # Then this function will not work! +def find_git_root(starting_path: Path | None = None) -> Path: + # 1. Highest priority: Bazel environment variable workspace = os.getenv("BUILD_WORKSPACE_DIRECTORY") if workspace: return Path(workspace) - for parent in Path(__file__).resolve().parents: + # 2. Traversal logic: use starting_path (for tests) or __file__ (for prod) + # We resolve it to ensure we aren't dealing with symlinks in the bazel cache + current: Path = (starting_path or Path(__file__)).resolve() + + for parent in current.parents: if (parent / ".git").exists(): return parent @@ -44,74 +47,73 @@ def find_git_root() -> Path: ) -def get_runfiles_dir_impl( - cwd: Path, - conf_dir: Path, - env_runfiles: Path | None, - git_root: Path, -) -> Path: - """Functional (and therefore testable) logic to determine the runfiles directory.""" - - _log_debug( - f"get_runfiles_dir_impl(\n cwd={cwd},\n conf_dir={conf_dir},\n" - f" env_runfiles={env_runfiles},\n git_root={git_root}\n)" - ) - - if env_runfiles: +def get_runfiles_dir() -> Path: + """Runfiles directory relative to conf.py""" + if r := os.getenv("RUNFILES_DIR"): # Runfiles are only available when running in Bazel. - # Both `bazel build` and `bazel run` are supported. + # bazel build and bazel run are both supported. # i.e. `bazel build //:docs` and `bazel run //:docs`. - _log_debug("Using env[RUNFILES_DIR] to find the runfiles...") - - if env_runfiles.is_absolute() and "bazel-out" in env_runfiles.parts: - # In case of `bazel run` it will point to the global cache directory, - # which has a new hash every time. And it's not pretty. - # However, `bazel-out` is a symlink to that same cache directory! - try: - idx = env_runfiles.parts.index("bazel-out") - runfiles_dir = git_root.joinpath(*env_runfiles.parts[idx:]) - _log_debug(f"Made runfiles dir pretty: {runfiles_dir}") - except ValueError: - sys.exit("Could not find bazel-out in runfiles path.") - else: - runfiles_dir = git_root / env_runfiles + logger.debug("Using runfiles to determine plantuml path.") + + runfiles_dir = Path(r) else: # The only way to land here is when running from within the virtual - # environment created by the `:ide_support` rule. + # environment created by the `:ide_support` rule in the BUILD file. # i.e. esbonio or manual sphinx-build execution within the virtual # environment. - _log_debug("Running outside bazel.") - - # TODO: "process-docs" is in SOURCE_DIR!! - runfiles_dir = git_root / "bazel-bin" / "process-docs" / "ide_support.runfiles" - - return runfiles_dir - - -def get_runfiles_dir() -> Path: - """Runfiles directory relative to conf.py""" + # We'll still use the plantuml binary from the bazel build. + # But we need to find it first. + logger.debug("Running outside bazel.") - # FIXME CONF_DIRECTORY is our invention. When running from esbonio, this is not - # set. It seems to provide app.confdir instead... - conf_dir = os.getenv("CONF_DIRECTORY") - assert conf_dir + git_root = Path.cwd().resolve() + while not (git_root / ".git").exists(): + git_root = git_root.parent + if git_root == Path("/"): + sys.exit("Could not find git root.") - env_runfiles = os.getenv("RUNFILES_DIR") + runfiles_dir = git_root / "bazel-bin" / "ide_support.runfiles" - runfiles = Path( - get_runfiles_dir_impl( - cwd=Path(os.getcwd()), - conf_dir=Path(conf_dir), - env_runfiles=Path(env_runfiles) if env_runfiles else None, - git_root=find_git_root(), - ) - ) - - if not runfiles.exists(): + if not runfiles_dir.exists(): sys.exit( - f"Could not find runfiles at {runfiles}. Have a look at " - "README.md for instructions on how to build docs." + f"Could not find runfiles_dir at {runfiles_dir}. " + "Have a look at README.md for instructions on how to build docs." ) + return runfiles_dir - return runfiles + # _log_debug( + # f"get_runfiles_dir_impl(\n cwd={cwd},\n " + # f" env_runfiles={env_runfiles},\n git_root={git_root}\n)" + # ) + # + # if env_runfiles: + # # Runfiles are only available when running in Bazel. + # # Both `bazel build` and `bazel run` are supported. + # # i.e. `bazel build //:docs` and `bazel run //:docs`. + # _log_debug("Using env[RUNFILES_DIR] to find the runfiles...") + # + # if env_runfiles.is_absolute() and "bazel-out" in env_runfiles.parts: + # # In case of `bazel run` it will point to the global cache directory, + # # which has a new hash every time. And it's not pretty. + # # However, `bazel-out` is a symlink to that same cache directory! + # try: + # idx = env_runfiles.parts.index("bazel-out") + # runfiles_dir = git_root.joinpath(*env_runfiles.parts[idx:]) + # _log_debug(f"Made runfiles dir pretty: {runfiles_dir}") + # except ValueError: + # sys.exit("Could not find bazel-out in runfiles path.") + # else: + # runfiles_dir = git_root / env_runfiles + # + # else: + # # The only way to land here is when running from within the virtual + # # environment created by the `:ide_support` rule. + # # i.e. esbonio or manual sphinx-build execution within the virtual + # # environment. + # _log_debug("Running outside bazel.") + # + # # TODO: "process-docs" is in SOURCE_DIR!! + # runfiles_dir = git_root / "bazel-bin" / + # "process-docs" / "ide_support.runfiles" + # + # return runfiles_dir diff --git a/src/find_runfiles/test_find_runfiles.py b/src/find_runfiles/test_find_runfiles.py index 97d73d84b..0a04dd8ff 100644 --- a/src/find_runfiles/test_find_runfiles.py +++ b/src/find_runfiles/test_find_runfiles.py @@ -12,82 +12,98 @@ # ******************************************************************************* from pathlib import Path -# TODO: why is there an __init__.py file in tooling? -from src import find_runfiles - - -def get_runfiles_dir_impl( - cwd: str, conf_dir: str, env_runfiles: str | None, git_root: str -): - return str( - find_runfiles.get_runfiles_dir_impl( - cwd=Path(cwd), - conf_dir=Path(conf_dir), - env_runfiles=Path(env_runfiles) if env_runfiles else None, - git_root=Path(git_root), - ) - ) +import pytest +from src import find_runfiles # Assuming the new file is named find_runfiles.py -def test_run_incremental(): - """bazel run //process-docs:incremental""" - # in incremental.py: - assert get_runfiles_dir_impl( - cwd="/home/vscode/.cache/bazel/_bazel_vscode/6084288f00f33db17acb4220ce8f1999/execroot/_main/bazel-out/k8-fastbuild/bin/process-docs/incremental.runfiles/_main", - conf_dir="process-docs", - env_runfiles="/home/vscode/.cache/bazel/_bazel_vscode/6084288f00f33db17acb4220ce8f1999/execroot/_main/bazel-out/k8-fastbuild/bin/process-docs/incremental.runfiles", - git_root="/workspaces/process", - ) == ( - "/workspaces/process/bazel-out/k8-fastbuild/bin/process-docs/" - "incremental.runfiles" - ) +## --- Helpers to simulate environments --- - # in conf.py: - assert get_runfiles_dir_impl( - cwd="/workspaces/process/process-docs", - conf_dir="process-docs", - env_runfiles="/home/vscode/.cache/bazel/_bazel_vscode/6084288f00f33db17acb4220ce8f1999/execroot/_main/bazel-out/k8-fastbuild/bin/process-docs/incremental.runfiles", - git_root="/workspaces/process", - ) == ( - "/workspaces/process/bazel-out/k8-fastbuild/bin/process-docs/" - "incremental.runfiles" - ) +def setup_mock_repo(tmp_path: Path) -> Path: + """Creates a dummy .git directory and returns the path.""" + repo: Path = tmp_path / "workspaces" / "process" + repo.mkdir(parents=True) + (repo / ".git").mkdir() + return repo -def test_build_incremental_and_exec_it(): - """bazel build //process-docs:incremental && bazel-bin/process-docs/incremental""" - assert ( - get_runfiles_dir_impl( - cwd="/workspaces/process/process-docs", - conf_dir="process-docs", - env_runfiles="bazel-bin/process-docs/incremental.runfiles", - git_root="/workspaces/process", - ) - == "/workspaces/process/bazel-bin/process-docs/incremental.runfiles" - ) + +## --- Tests --- -def test_esbonio_old(): - """Observed with esbonio 0.x""" - assert ( - get_runfiles_dir_impl( - cwd="/workspaces/process/process-docs", - conf_dir="process-docs", - env_runfiles=None, - git_root="/workspaces/process", - ) - == "/workspaces/process/bazel-bin/process-docs/ide_support.runfiles" +def test_run_incremental_pretty_path( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """ + Simulates: bazel run //process-docs:incremental + Logic: Uses RUNFILES_DIR environment variable. + """ + git_root: Path = setup_mock_repo(tmp_path) + runfiles_path: Path = ( + git_root / "bazel-out/k8-fastbuild/bin/process-docs/incremental.runfiles" ) + runfiles_path.mkdir(parents=True) + # In the new logic, get_runfiles_dir() returns the env var Path if it exists + monkeypatch.setenv("RUNFILES_DIR", str(runfiles_path)) -def test3(): - # docs named differently, just to make sure nothing is hardcoded - # bazel run //other-docs:incremental - assert get_runfiles_dir_impl( - cwd="/workspaces/process/other-docs", - conf_dir="other-docs", - env_runfiles="/home/vscode/.cache/bazel/_bazel_vscode/6084288f00f33db17acb4220ce8f1999/execroot/_main/bazel-out/k8-fastbuild/bin/other-docs/incremental.runfiles", - git_root="/workspaces/process", - ) == ( - "/workspaces/process/bazel-out/k8-fastbuild/bin/other-docs/incremental.runfiles" - ) + result: Path = find_runfiles.get_runfiles_dir() + assert result == runfiles_path + assert result.exists() + + +def test_build_incremental_and_exec_it( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """ + Simulates: bazel build //process-docs:incremental && + bazel-bin/process-docs/incremental + """ + git_root: Path = setup_mock_repo(tmp_path) + bin_runfiles: Path = git_root / "bazel-bin/process-docs/incremental.runfiles" + bin_runfiles.mkdir(parents=True) + + monkeypatch.setenv("RUNFILES_DIR", str(bin_runfiles)) + + result: Path = find_runfiles.get_runfiles_dir() + assert result == bin_runfiles + + +def test_outside_bazel_ide_support( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """ + Simulates: Running outside bazel (e.g., Esbonio/Sphinx). + Logic: Falls back to git_root / "bazel-bin" / "ide_support.runfiles" + """ + git_root: Path = setup_mock_repo(tmp_path) + # The new logic uses Path.cwd().resolve() to find .git + monkeypatch.chdir(git_root) + monkeypatch.delenv("RUNFILES_DIR", raising=False) + + # Create the expected fallback path + expected_path: Path = git_root / "bazel-bin" / "ide_support.runfiles" + expected_path.mkdir(parents=True) + + result: Path = find_runfiles.get_runfiles_dir() + assert result == expected_path + + +def test_find_git_root_via_traversal( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Tests find_git_root by walking up the tree from a specific path.""" + # Create the fake repo: /tmp/.../workspaces/process/.git + git_root: Path = setup_mock_repo(tmp_path) + monkeypatch.delenv("BUILD_WORKSPACE_DIRECTORY", raising=False) + + # Create a deep subdirectory and a "fake" script file inside it + sub_dir: Path = git_root / "some" / "deep" / "path" + sub_dir.mkdir(parents=True) + fake_script: Path = sub_dir / "tool.py" + fake_script.touch() + + # Pass the fake script path so the function starts searching from there + result: Path = find_runfiles.find_git_root(starting_path=fake_script) + + assert result == git_root + assert (result / ".git").exists()