Public Access
DEVX-110: feat: extract docker-login, tofu-ops, check-deps, install-tofu to Python tools
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 21s
Build Images / detect-type (push) Successful in 41s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / release (push) Successful in 43s
Post-merge / sync-wiki (push) Successful in 47s
Post-merge / badges (push) Successful in 51s
Post-merge / publish (push) Successful in 22s
Build Images / build-and-push (push) Successful in 3m26s
Build Images / cleanup (push) Successful in 3m20s
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 21s
Build Images / detect-type (push) Successful in 41s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / release (push) Successful in 43s
Post-merge / sync-wiki (push) Successful in 47s
Post-merge / badges (push) Successful in 51s
Post-merge / publish (push) Successful in 22s
Build Images / build-and-push (push) Successful in 3m26s
Build Images / cleanup (push) Successful in 3m20s
This commit was merged in pull request #168.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that required development tools are present.
|
||||
|
||||
Verifies the availability of core tools (tofu, docker, checkmake, Python
|
||||
3.12+ in the venv) and prints warnings or errors for missing ones.
|
||||
|
||||
Usage::
|
||||
|
||||
python3 -m devx.tools.check_deps
|
||||
python3 -m devx.tools.check_deps --venv .venv
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess # nosec B404
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from devx.i18n import _
|
||||
|
||||
REQUIRED_TOOLS = ["tofu", "docker"]
|
||||
OPTIONAL_TOOLS = ["checkmake"]
|
||||
PYTHON_MIN_VERSION = (3, 12)
|
||||
|
||||
|
||||
def _check_tool(name: str, *, optional: bool = False) -> bool:
|
||||
"""Check if a tool is on PATH. Returns True if found."""
|
||||
found = shutil.which(name) is not None
|
||||
if found:
|
||||
return True
|
||||
level = "WARN" if optional else "ERROR"
|
||||
click.echo(
|
||||
_("{level}: {tool} not found.{hint}", level=level, tool=name, hint=""),
|
||||
err=True,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _check_python_version(venv_bin: Path) -> None:
|
||||
"""Check that the venv Python is >= 3.12."""
|
||||
python_bin = venv_bin / "python"
|
||||
if not python_bin.exists():
|
||||
click.echo(
|
||||
_("WARN: .venv not found. Run 'make setup-venv' to create it."),
|
||||
err=True,
|
||||
)
|
||||
return
|
||||
result = subprocess.run( # nosec B603
|
||||
[str(python_bin), "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
click.echo(_("WARN: Could not determine Python version in .venv."), err=True)
|
||||
return
|
||||
version_str = result.stdout.strip().split()[-1] if result.stdout else ""
|
||||
try:
|
||||
major, minor = int(version_str.split(".")[0]), int(version_str.split(".")[1])
|
||||
except (IndexError, ValueError):
|
||||
click.echo(_("WARN: Could not parse Python version '{version}'.", version=version_str), err=True)
|
||||
return
|
||||
if (major, minor) < PYTHON_MIN_VERSION:
|
||||
click.echo(
|
||||
_(
|
||||
"WARN: .venv has Python {version}, but >={req} is required.",
|
||||
version=version_str,
|
||||
req=f"{PYTHON_MIN_VERSION[0]}.{PYTHON_MIN_VERSION[1]}",
|
||||
),
|
||||
err=True,
|
||||
)
|
||||
return
|
||||
click.echo(_("[check-deps] Virtualenv .venv ready (Python {version}).", version=version_str))
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--venv", default=".venv", show_default=True, help="Path to the virtual environment.")
|
||||
@click.option("--checkmake-bin", default=None, help="Path to checkmake binary (fallback if not on PATH).")
|
||||
def cli(venv: str, checkmake_bin: str | None) -> None:
|
||||
"""Verify that required development tools are present."""
|
||||
click.echo("[check-deps] Verifying tools...")
|
||||
|
||||
all_required = True
|
||||
for tool in REQUIRED_TOOLS:
|
||||
if not _check_tool(tool):
|
||||
all_required = False
|
||||
|
||||
for tool in OPTIONAL_TOOLS:
|
||||
if not _check_tool(tool, optional=True):
|
||||
if checkmake_bin and Path(checkmake_bin).exists():
|
||||
click.echo(f" {tool}: found at {checkmake_bin}")
|
||||
else:
|
||||
click.echo(" Run 'make install-checkmake' to install the Makefile linter.")
|
||||
|
||||
_check_python_version(Path(venv) / "bin")
|
||||
|
||||
if not all_required:
|
||||
raise click.ClickException("Required tools missing.")
|
||||
click.echo("[check-deps] All core tools present.")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
cli() # pragma: no cover
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Docker registry login helper.
|
||||
|
||||
Handles login to Docker registries (Gitea, Docker Hub) with credential
|
||||
loading from environment variables. Supports required and optional modes.
|
||||
|
||||
Usage::
|
||||
|
||||
python3 -m devx.tools.docker_login --registry git.oblachno.oblachno.fyi \\
|
||||
--token-env CI_GITEA_TOKEN --username-env CI_GITEA_USERNAME \\
|
||||
--default-username emil
|
||||
|
||||
python3 -m devx.tools.docker_login --registry docker.io \\
|
||||
--token-env DOCKER_HUB_TOKEN --username-env DOCKER_HUB_USERNAME --optional
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess # nosec B404
|
||||
|
||||
import click
|
||||
|
||||
from devx.i18n import _
|
||||
|
||||
|
||||
def docker_login(
|
||||
registry: str,
|
||||
username: str,
|
||||
token: str,
|
||||
*,
|
||||
suppress_failure: bool = False,
|
||||
) -> bool:
|
||||
"""Log in to a Docker registry.
|
||||
|
||||
Returns True on success, False on failure.
|
||||
If ``suppress_failure`` is True, prints a warning instead of raising.
|
||||
"""
|
||||
cmd = ["docker", "login", registry, "-u", username, "-p", token]
|
||||
result = subprocess.run( # nosec B603
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
if suppress_failure:
|
||||
click.echo(
|
||||
_("[docker-login] Login to {registry} failed (continuing).", registry=registry),
|
||||
err=True,
|
||||
)
|
||||
return False
|
||||
raise click.ClickException(
|
||||
_("Login to {registry} failed: {error}", registry=registry, error=result.stderr.strip()),
|
||||
)
|
||||
click.echo(_("[docker-login] Logged in to {registry}.", registry=registry))
|
||||
return True
|
||||
|
||||
|
||||
def _resolve_credentials(
|
||||
token_env: str,
|
||||
username_env: str,
|
||||
default_username: str | None,
|
||||
) -> tuple[str | None, str | None]:
|
||||
"""Resolve credentials from environment variables.
|
||||
|
||||
Returns (username, token) or (None, None) if token is not set.
|
||||
"""
|
||||
import os
|
||||
|
||||
token = os.environ.get(token_env, "")
|
||||
if not token:
|
||||
return None, None
|
||||
username = os.environ.get(username_env, "") or (default_username or "")
|
||||
return username, token
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--registry", required=True, help="Docker registry URL (e.g. docker.io, git.example.com).")
|
||||
@click.option("--token-env", required=True, help="Environment variable name for the auth token.")
|
||||
@click.option("--username-env", required=True, help="Environment variable name for the username.")
|
||||
@click.option(
|
||||
"--default-username",
|
||||
default=None,
|
||||
help="Default username if the env var is not set.",
|
||||
)
|
||||
@click.option(
|
||||
"--optional",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Skip silently if token is not set instead of raising.",
|
||||
)
|
||||
@click.option(
|
||||
"--suppress-failure",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Continue on login failure instead of raising (prints warning).",
|
||||
)
|
||||
def cli(
|
||||
registry: str,
|
||||
token_env: str,
|
||||
username_env: str,
|
||||
default_username: str | None,
|
||||
optional: bool,
|
||||
suppress_failure: bool,
|
||||
) -> None:
|
||||
"""Log in to a Docker registry using credentials from environment variables."""
|
||||
username, token = _resolve_credentials(token_env, username_env, default_username)
|
||||
if token is None:
|
||||
if optional:
|
||||
click.echo(_("[docker-login] Skipping {registry} (token {env} not set).", registry=registry, env=token_env))
|
||||
return
|
||||
raise click.ClickException(
|
||||
_("{env} is not set. Set it in your .env file or pass it as an environment variable.", env=token_env),
|
||||
)
|
||||
if not username:
|
||||
raise click.ClickException(
|
||||
_("{env} is not set. Set it in your .env file.", env=username_env),
|
||||
)
|
||||
docker_login(registry, username, token, suppress_failure=suppress_failure)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
cli() # pragma: no cover
|
||||
@@ -42,6 +42,8 @@ TEA_VERSION = "0.14.1"
|
||||
|
||||
HADOLINT_VERSION = "2.12.0"
|
||||
|
||||
TOFU_VERSION = "1.12.3"
|
||||
|
||||
|
||||
def _arch() -> str:
|
||||
"""Return the architecture string used by release assets (delegates to shared utility)."""
|
||||
@@ -174,7 +176,27 @@ def install_hadolint() -> bool:
|
||||
return True
|
||||
|
||||
|
||||
TOOL_NAMES = ["actionlint", "git-cliff", "act_runner", "tea", "hadolint"]
|
||||
def install_tofu() -> bool:
|
||||
"""Install OpenTofu if not already present. Returns True if installed/skipped.
|
||||
|
||||
Downloads the official release tarball from GitHub and extracts the
|
||||
``tofu`` binary to ``~/.local/bin``.
|
||||
"""
|
||||
if _is_installed("tofu"):
|
||||
click.echo("tofu: already installed")
|
||||
return True
|
||||
arch = _arch()
|
||||
os_name = platform.system().lower()
|
||||
url = (
|
||||
f"https://github.com/opentofu/opentofu/releases/download/"
|
||||
f"v{TOFU_VERSION}/tofu_{TOFU_VERSION}_{os_name}_{arch}.tar.gz"
|
||||
)
|
||||
dest = _download_and_extract_tarball(url, "tofu")
|
||||
click.echo(f"tofu: installed to {dest}")
|
||||
return True
|
||||
|
||||
|
||||
TOOL_NAMES = ["actionlint", "git-cliff", "act_runner", "tea", "hadolint", "tofu"]
|
||||
|
||||
|
||||
def _install_tool(name: str) -> bool:
|
||||
@@ -189,6 +211,8 @@ def _install_tool(name: str) -> bool:
|
||||
return install_tea()
|
||||
if name == "hadolint":
|
||||
return install_hadolint()
|
||||
if name == "tofu":
|
||||
return install_tofu()
|
||||
raise click.ClickException(f"Unknown tool: {name}")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""OpenTofu operations: init and validate across directories.
|
||||
|
||||
Handles initialization and validation of OpenTofu configurations across
|
||||
multiple directories (modules + environments). Supports CI mode with
|
||||
``-backend=false`` to avoid state backend access.
|
||||
|
||||
Usage::
|
||||
|
||||
python3 -m devx.tools.tofu_ops init --env staging
|
||||
python3 -m devx.tools.tofu_ops validate
|
||||
python3 -m devx.tools.tofu_ops validate --ci
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess # nosec B404
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from devx.i18n import _
|
||||
|
||||
DEFAULT_ENV_DIRS = ["tofu/environments/{env}", "tofu/environments/dns"]
|
||||
DEFAULT_VALIDATE_DIRS = [
|
||||
"tofu/modules/hetzner-vm",
|
||||
"tofu/modules/hetzner-network",
|
||||
"tofu/environments/staging",
|
||||
"tofu/environments/production",
|
||||
"tofu/environments/dns",
|
||||
]
|
||||
|
||||
|
||||
def _run_tofu(cmd: list[str], cwd: Path) -> None:
|
||||
"""Run a tofu command in the given directory, raising on failure."""
|
||||
click.echo(f" -> {cwd}")
|
||||
result = subprocess.run( # nosec B603, B607
|
||||
cmd,
|
||||
cwd=str(cwd),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise click.ClickException(
|
||||
_("tofu command failed in {dir}: {error}", dir=cwd, error=result.stderr.strip()),
|
||||
)
|
||||
|
||||
|
||||
def tofu_init(env: str, root: str = ".", extra_dirs: list[str] | None = None) -> None:
|
||||
"""Run ``tofu init`` in the environment directory and DNS directory.
|
||||
|
||||
Args:
|
||||
env: Environment name (e.g. staging, production).
|
||||
root: Repository root directory.
|
||||
extra_dirs: Additional directory patterns to initialize.
|
||||
"""
|
||||
root_path = Path(root)
|
||||
dirs = [d.format(env=env) for d in (extra_dirs or DEFAULT_ENV_DIRS)]
|
||||
for dir_pattern in dirs:
|
||||
dir_path = root_path / dir_pattern
|
||||
if dir_path.is_dir():
|
||||
click.echo(f"[tofu-init] Initializing {dir_path}...")
|
||||
_run_tofu(["tofu", "init"], dir_path)
|
||||
click.echo("[tofu-init] Done.")
|
||||
|
||||
|
||||
def tofu_validate(
|
||||
root: str = ".",
|
||||
dirs: list[str] | None = None,
|
||||
ci: bool = False,
|
||||
) -> None:
|
||||
"""Run ``tofu validate`` in all OpenTofu directories.
|
||||
|
||||
In CI mode, runs ``tofu init -backend=false`` before validate to avoid
|
||||
state backend access.
|
||||
|
||||
Args:
|
||||
root: Repository root directory.
|
||||
dirs: List of directory paths to validate (relative to root).
|
||||
ci: If True, use CI mode with -backend=false.
|
||||
"""
|
||||
root_path = Path(root)
|
||||
target_dirs = dirs or DEFAULT_VALIDATE_DIRS
|
||||
mode = "ci" if ci else "validate"
|
||||
click.echo(f"[tofu-{mode}] Validating OpenTofu configurations...")
|
||||
for dir_rel in target_dirs:
|
||||
dir_path = root_path / dir_rel
|
||||
if not dir_path.is_dir():
|
||||
continue
|
||||
if ci:
|
||||
_run_tofu(["tofu", "init", "-backend=false", "-input=false"], dir_path)
|
||||
_run_tofu(["tofu", "validate"], dir_path)
|
||||
click.echo(f"[tofu-{mode}] All configurations valid.")
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli() -> None:
|
||||
"""OpenTofu operations."""
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--env", required=True, help="Environment name (staging, production).")
|
||||
@click.option("--root", default=".", help="Repository root directory.")
|
||||
def init(env: str, root: str) -> None:
|
||||
"""Initialize OpenTofu in an environment."""
|
||||
tofu_init(env, root)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--root", default=".", help="Repository root directory.")
|
||||
@click.option("--ci", is_flag=True, default=False, help="CI mode: use -backend=false.")
|
||||
def validate(root: str, ci: bool) -> None:
|
||||
"""Validate OpenTofu configurations."""
|
||||
tofu_validate(root, ci=ci)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
cli() # pragma: no cover
|
||||
@@ -3094,5 +3094,109 @@
|
||||
"pl": " - {count} standard labels verified",
|
||||
"ru": " - {count} standard labels verified",
|
||||
"zh": " - {count} standard labels verified"
|
||||
},
|
||||
"[check-deps] Virtualenv .venv ready (Python {version}).": {
|
||||
"en": "[check-deps] Virtualenv .venv ready (Python {version}).",
|
||||
"bg": "[check-deps] Виртуална среда .venv готова (Python {version}).",
|
||||
"de": "[check-deps] Virtuelle Umgebung .venv bereit (Python {version}).",
|
||||
"pl": "[check-deps] Środowisko wirtualne .venv gotowe (Python {version}).",
|
||||
"ru": "[check-deps] Виртуальное окружение .venv готово (Python {version}).",
|
||||
"zh": "[check-deps] 虚拟环境 .venv 已就绪 (Python {version})。"
|
||||
},
|
||||
"{level}: {tool} not found.{hint}": {
|
||||
"en": "{level}: {tool} not found.{hint}",
|
||||
"bg": "{level}: {tool} не е намерен.{hint}",
|
||||
"de": "{level}: {tool} nicht gefunden.{hint}",
|
||||
"pl": "{level}: {tool} nie znaleziono.{hint}",
|
||||
"ru": "{level}: {tool} не найден.{hint}",
|
||||
"zh": "{level}: 未找到 {tool}。{hint}"
|
||||
},
|
||||
"WARN: Could not determine Python version in .venv.": {
|
||||
"en": "WARN: Could not determine Python version in .venv.",
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: Не може да се определи версията на Python в .venv.",
|
||||
"de": "WARNUNG: Python-Version in .venv konnte nicht bestimmt werden.",
|
||||
"pl": "OSTRZEŻENIE: Nie można określić wersji Python w .venv.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: Не удалось определить версию Python в .venv.",
|
||||
"zh": "警告: 无法确定 .venv 中的 Python 版本。"
|
||||
},
|
||||
"WARN: Could not parse Python version '{version}'.": {
|
||||
"en": "WARN: Could not parse Python version '{version}'.",
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: Не може да се анализира версията на Python '{version}'.",
|
||||
"de": "WARNUNG: Python-Version '{version}' konnte nicht analysiert werden.",
|
||||
"pl": "OSTRZEŻENIE: Nie można przeanalizować wersji Python '{version}'.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: Не удалось разобрать версию Python '{version}'.",
|
||||
"zh": "警告: 无法解析 Python 版本 '{version}'。"
|
||||
},
|
||||
"WARN: .venv not found. Run 'make setup-venv' to create it.": {
|
||||
"en": "WARN: .venv not found. Run 'make setup-venv' to create it.",
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: .venv не е намерен. Изпълнете 'make setup-venv' за създаване.",
|
||||
"de": "WARNUNG: .venv nicht gefunden. Führen Sie 'make setup-venv' aus, um es zu erstellen.",
|
||||
"pl": "OSTRZEŻENIE: Nie znaleziono .venv. Uruchom 'make setup-venv', aby utworzyć.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: .venv не найден. Выполните 'make setup-venv' для создания.",
|
||||
"zh": "警告: 未找到 .venv。运行 'make setup-venv' 来创建。"
|
||||
},
|
||||
"[docker-login] Logged in to {registry}.": {
|
||||
"en": "[docker-login] Logged in to {registry}.",
|
||||
"bg": "[docker-login] Влязъл в {registry}.",
|
||||
"de": "[docker-login] Angemeldet bei {registry}.",
|
||||
"pl": "[docker-login] Zalogowano do {registry}.",
|
||||
"ru": "[docker-login] Выполнен вход в {registry}.",
|
||||
"zh": "[docker-login] 已登录到 {registry}。"
|
||||
},
|
||||
"[docker-login] Login to {registry} failed (continuing).": {
|
||||
"en": "[docker-login] Login to {registry} failed (continuing).",
|
||||
"bg": "[docker-login] Влизането в {registry} не успя (продължава).",
|
||||
"de": "[docker-login] Anmeldung bei {registry} fehlgeschlagen (wird fortgesetzt).",
|
||||
"pl": "[docker-login] Logowanie do {registry} nie powiodło się (kontynuowanie).",
|
||||
"ru": "[docker-login] Ошибка входа в {registry} (продолжаем).",
|
||||
"zh": "[docker-login] 登录 {registry} 失败(继续)。"
|
||||
},
|
||||
"[docker-login] Skipping {registry} (token {env} not set).": {
|
||||
"en": "[docker-login] Skipping {registry} (token {env} not set).",
|
||||
"bg": "[docker-login] Пропускане на {registry} (токен {env} не е зададен).",
|
||||
"de": "[docker-login] {registry} übersprungen (Token {env} nicht gesetzt).",
|
||||
"pl": "[docker-login] Pomijanie {registry} (token {env} nie ustawiony).",
|
||||
"ru": "[docker-login] Пропуск {registry} (токен {env} не задан).",
|
||||
"zh": "[docker-login] 跳过 {registry}(未设置令牌 {env})。"
|
||||
},
|
||||
"{env} is not set. Set it in your .env file.": {
|
||||
"en": "{env} is not set. Set it in your .env file.",
|
||||
"bg": "{env} не е зададен. Задайте го във вашия .env файл.",
|
||||
"de": "{env} ist nicht gesetzt. Setzen Sie es in Ihrer .env-Datei.",
|
||||
"pl": "{env} nie jest ustawiony. Ustaw go w pliku .env.",
|
||||
"ru": "{env} не задан. Установите его в файле .env.",
|
||||
"zh": "{env} 未设置。请在 .env 文件中设置。"
|
||||
},
|
||||
"{env} is not set. Set it in your .env file or pass it as an environment variable.": {
|
||||
"en": "{env} is not set. Set it in your .env file or pass it as an environment variable.",
|
||||
"bg": "{env} не е зададен. Задайте го във вашия .env файл или го подайте като променлива на средата.",
|
||||
"de": "{env} ist nicht gesetzt. Setzen Sie es in Ihrer .env-Datei oder übergeben Sie es als Umgebungsvariable.",
|
||||
"pl": "{env} nie jest ustawiony. Ustaw go w pliku .env lub przekaż jako zmienną środowiskową.",
|
||||
"ru": "{env} не задан. Установите его в файле .env или передайте как переменную окружения.",
|
||||
"zh": "{env} 未设置。请在 .env 文件中设置或作为环境变量传递。"
|
||||
},
|
||||
"Login to {registry} failed: {error}": {
|
||||
"en": "Login to {registry} failed: {error}",
|
||||
"bg": "Влизането в {registry} не успя: {error}",
|
||||
"de": "Anmeldung bei {registry} fehlgeschlagen: {error}",
|
||||
"pl": "Logowanie do {registry} nie powiodło się: {error}",
|
||||
"ru": "Ошибка входа в {registry}: {error}",
|
||||
"zh": "登录 {registry} 失败: {error}"
|
||||
},
|
||||
"tofu command failed in {dir}: {error}": {
|
||||
"en": "tofu command failed in {dir}: {error}",
|
||||
"bg": "командата tofu не успя в {dir}: {error}",
|
||||
"de": "tofu-Befehl fehlgeschlagen in {dir}: {error}",
|
||||
"pl": "polecenie tofu nie powiodło się w {dir}: {error}",
|
||||
"ru": "команда tofu не удалась в {dir}: {error}",
|
||||
"zh": "tofu 命令在 {dir} 中失败: {error}"
|
||||
},
|
||||
"WARN: .venv has Python {version}, but >={req} is required.": {
|
||||
"en": "WARN: .venv has Python {version}, but >={req} is required.",
|
||||
"bg": "ПРЕДУПРЕЖДЕНИЕ: .venv има Python {version}, но се изисква >={req}.",
|
||||
"de": "WARNUNG: .venv hat Python {version}, aber >={req} ist erforderlich.",
|
||||
"pl": "OSTRZEŻENIE: .venv ma Python {version}, ale wymagane jest >={req}.",
|
||||
"ru": "ПРЕДУПРЕЖДЕНИЕ: в .venv установлен Python {version}, но требуется >={req}.",
|
||||
"zh": "警告: .venv 的 Python 版本为 {version},但要求 >={req}。"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user