mirror of https://github.com/mongodb/mongo
225 lines
7.5 KiB
Python
225 lines
7.5 KiB
Python
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
sys.path.append("bazel")
|
|
from bazelisk import get_bazel_path, make_bazel_cmd
|
|
|
|
# WARNING: this file is imported from outside of any virtual env so the main import block MUST NOT
|
|
# import any third-party non-std libararies. Libraries needed when running as a script can be
|
|
# conditionally imported below.
|
|
|
|
# Get relative imports to work when the package is not installed on the PYTHONPATH.
|
|
if __name__ == "__main__" and __package__ is None:
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(os.path.realpath(__file__)))))
|
|
|
|
|
|
SUPPORTED_VERSIONS = "v5"
|
|
|
|
|
|
class MongoToolchainError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class MongoToolchainNotFoundError(MongoToolchainError):
|
|
def __init__(self, extra_msg: str):
|
|
super().__init__(extra_msg)
|
|
|
|
|
|
class ToolNotFoundError(MongoToolchainError):
|
|
def __init__(self, tool: str, extra_msg: str | None):
|
|
msg = f"Couldn't find {tool} in mongo toolchain"
|
|
msg = msg + f": {extra_msg}" if extra_msg is not None else msg
|
|
super().__init__(msg)
|
|
|
|
|
|
class MongoToolchain:
|
|
def __init__(self, root_dir_path: Path):
|
|
self._root_dir = root_dir_path
|
|
|
|
def get_tool_path(self, tool: str) -> str:
|
|
path = self._get_bin_dir_path() / tool
|
|
if not path.exists():
|
|
raise ToolNotFoundError(tool, f"{path} does not exist")
|
|
if not path.is_file() or not os.access(path, os.X_OK):
|
|
raise ToolNotFoundError(tool, f"{path} is not an executable")
|
|
return str(path)
|
|
|
|
def check_exists(self) -> None:
|
|
for directory in (
|
|
self._root_dir,
|
|
self._get_bin_dir_path(),
|
|
self._get_include_dir_path(),
|
|
self._get_lib_dir_path(),
|
|
):
|
|
if not directory.is_dir():
|
|
raise MongoToolchainNotFoundError(f"{directory} is not a directory")
|
|
|
|
def get_root_dir(self) -> str:
|
|
return str(self._root_dir)
|
|
|
|
def get_bin_dir(self) -> str:
|
|
return str(self._get_bin_dir_path())
|
|
|
|
def get_include_dir(self) -> str:
|
|
return str(self._get_include_dir_path())
|
|
|
|
def get_lib_dir(self) -> str:
|
|
return str(self._get_include_dir_path())
|
|
|
|
def _get_bin_dir_path(self) -> Path:
|
|
return self._root_dir / "bin"
|
|
|
|
def _get_include_dir_path(self) -> Path:
|
|
return self._root_dir / "include"
|
|
|
|
def _get_lib_dir_path(self) -> Path:
|
|
return self._root_dir / "lib"
|
|
|
|
|
|
def _execute_bazel(argv):
|
|
bazel_cmd = make_bazel_cmd(get_bazel_path(), argv)
|
|
cmd = f"{bazel_cmd['exec']} {' '.join(bazel_cmd['args'])}"
|
|
for i in range(5):
|
|
try:
|
|
return subprocess.check_output(
|
|
cmd,
|
|
env=bazel_cmd["env"],
|
|
shell=True,
|
|
text=True,
|
|
).strip()
|
|
except subprocess.CalledProcessError as e:
|
|
print(
|
|
f"Failed to execute bazel command: `{e.cmd}` exited with code {e.returncode}, retrying in 60s..."
|
|
)
|
|
time.sleep(60)
|
|
if i == 4:
|
|
raise e
|
|
|
|
|
|
def _fetch_bazel_toolchain(version: str) -> None:
|
|
try:
|
|
_execute_bazel(
|
|
["build", "--bes_backend=", "--bes_results_url=", f"@mongo_toolchain_{version}//:all"]
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
raise MongoToolchainNotFoundError(
|
|
f"Failed to fetch bazel toolchain: `{e.cmd}` exited with code {e.returncode}"
|
|
)
|
|
|
|
|
|
def _get_bazel_execroot() -> Path:
|
|
try:
|
|
execroot_str = _execute_bazel(["info", "execution_root"])
|
|
except subprocess.CalledProcessError as e:
|
|
raise MongoToolchainNotFoundError(
|
|
f"Couldn't find bazel execroot: `{e.cmd}` exited with code {e.returncode}"
|
|
)
|
|
return Path(execroot_str)
|
|
|
|
|
|
def _get_bazel_toolchain_path(version: str) -> Path:
|
|
return _get_bazel_execroot() / "external" / f"mongo_toolchain_{version}" / version
|
|
|
|
|
|
def _get_toolchain_from_path(path: str | Path) -> MongoToolchain:
|
|
toolchain = MongoToolchain(Path(path).resolve())
|
|
toolchain.check_exists()
|
|
return toolchain
|
|
|
|
|
|
def _get_bazel_toolchain(version: str) -> MongoToolchain:
|
|
path = _get_bazel_toolchain_path(version)
|
|
if not path.exists():
|
|
_fetch_bazel_toolchain(version)
|
|
if not path.is_dir():
|
|
raise MongoToolchainNotFoundError(
|
|
f"Couldn't find bazel toolchain: {path} is not a directory"
|
|
)
|
|
return _get_toolchain_from_path(path)
|
|
|
|
|
|
def _get_installed_toolchain_path(version: str) -> Path:
|
|
return Path("/opt/mongodbtoolchain") / version
|
|
|
|
|
|
def _get_installed_toolchain(version: str):
|
|
return _get_toolchain_from_path(_get_installed_toolchain_path(version))
|
|
|
|
|
|
def get_mongo_toolchain(
|
|
*, version: str | None = None, from_bazel: bool | None = None
|
|
) -> MongoToolchain:
|
|
# When running under bazel this environment variable will be set and will point to the
|
|
# toolchain the target was configured to use. It can also be set manually to override
|
|
# a script's selection of toolchain.
|
|
toolchain_path = os.environ.get("MONGO_TOOLCHAIN_PATH", None)
|
|
if toolchain_path is not None:
|
|
return _get_toolchain_from_path(toolchain_path)
|
|
|
|
# If no version given, look in the environment or default to v5.
|
|
if version is None:
|
|
version = os.environ.get("MONGO_TOOLCHAIN_VERSION", "v5")
|
|
assert version is not None
|
|
if version not in SUPPORTED_VERSIONS:
|
|
raise MongoToolchainNotFoundError(f"Unknown toolchain version {version}")
|
|
|
|
# If from_bazel is unspecified, let's query from bazel where querying toolchain
|
|
# version is supported since v5.
|
|
def _parse_from_bazel_envvar(value: str) -> bool:
|
|
v = value.lower()
|
|
if v in ("true", "1"):
|
|
return True
|
|
elif v in ("false", "0"):
|
|
return False
|
|
else:
|
|
raise ValueError(f"Invalid value {value} for MONGO_TOOLCHAIN_FROM_BAZEL")
|
|
|
|
if from_bazel is None:
|
|
from_bazel_value = os.environ.get("MONGO_TOOLCHAIN_FROM_BAZEL", "true")
|
|
from_bazel = _parse_from_bazel_envvar(from_bazel_value)
|
|
|
|
if from_bazel:
|
|
return _get_bazel_toolchain(version)
|
|
return _get_installed_toolchain(version)
|
|
|
|
|
|
def try_get_mongo_toolchain(*args, **kwargs) -> MongoToolchain | None:
|
|
try:
|
|
return get_mongo_toolchain(*args, **kwargs)
|
|
except MongoToolchainError:
|
|
return None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# See comment on main import block
|
|
import typer
|
|
from typing_extensions import Annotated
|
|
|
|
_app = typer.Typer(add_completion=False)
|
|
|
|
@_app.command()
|
|
def main(
|
|
tool: Annotated[Optional[str], typer.Argument()] = None,
|
|
version: Annotated[Optional[str], typer.Option("--version")] = None,
|
|
from_bazel: Annotated[Optional[bool], typer.Option("--bazel/--no-bazel")] = None,
|
|
):
|
|
"""
|
|
Prints the path to tools in the mongo toolchain or the toolchain's root directory (which
|
|
should contain bin/, include/, and so on).
|
|
If MONGO_TOOLCHAIN_PATH is set in the environment, it overrides all options to this script.
|
|
Otherwise, MONGO_TOOLCHAIN_VERSION is a lower-precedence alternative to --version and
|
|
MONGO_TOOLCHAIN_FROM_BAZEL is a lower-precedence alternative to --bazel/--no-bazel.
|
|
None of these are required, and if none are given, the default configuration will be used.
|
|
"""
|
|
toolchain = get_mongo_toolchain(version=version, from_bazel=from_bazel)
|
|
if tool is not None:
|
|
print(toolchain.get_tool_path(tool))
|
|
else:
|
|
print(toolchain.get_root_dir())
|
|
|
|
_app()
|