Public Access
DEVX-110: feat: centralize venv management in devx.mak
Post-merge / detect-type (push) Successful in 9s
Build Images / detect-type (push) Failing after 13s
Build Images / build-and-push (push) Has been skipped
Post-merge / validate-commit-msg (push) Successful in 10s
Build Images / cleanup (push) Has been skipped
Post-merge / vikunja (push) Successful in 15s
Post-merge / configure-repo (push) Successful in 18s
Post-merge / sync-wiki (push) Successful in 29s
Post-merge / release (push) Successful in 32s
Post-merge / badges (push) Successful in 39s
Post-merge / publish (push) Successful in 17s
Post-merge / detect-type (push) Successful in 9s
Build Images / detect-type (push) Failing after 13s
Build Images / build-and-push (push) Has been skipped
Post-merge / validate-commit-msg (push) Successful in 10s
Build Images / cleanup (push) Has been skipped
Post-merge / vikunja (push) Successful in 15s
Post-merge / configure-repo (push) Successful in 18s
Post-merge / sync-wiki (push) Successful in 29s
Post-merge / release (push) Successful in 32s
Post-merge / badges (push) Successful in 39s
Post-merge / publish (push) Successful in 17s
This commit was merged in pull request #167.
This commit is contained in:
+44
-14
@@ -56,17 +56,57 @@ DEVX_DOCKERFILE_PATHS ?= docker
|
||||
# PIP_INSTALL — helper to run pip with Gitea private PyPI registry configured.
|
||||
# Usage: $(DEVX_PIP_INSTALL) install -e '.[ci,lint]'
|
||||
# CI_GITEA_USERNAME can be set in .env, as an env var, or as a Make variable.
|
||||
# Projects can alias: PIP_INSTALL = $(DEVX_PIP_INSTALL)
|
||||
DEVX_PIP_INSTALL := if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
|
||||
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \
|
||||
_PYPI_USER="$${CI_GITEA_USERNAME:-emil}"; \
|
||||
if [ -n "$$CI_GITEA_TOKEN" ] && [ -n "$$_PYPI_USER" ]; then export PIP_EXTRA_INDEX_URL="https://$$_PYPI_USER:$$CI_GITEA_TOKEN@$(DEVX_GITEA_PYPI_HOST)/api/packages/$(DEVX_GITEA_PYPI_ORG)/pypi/simple/"; fi; \
|
||||
$(DEVX_BIN)/pip
|
||||
|
||||
# ── Virtual environment management ────────────────────────────────────────────
|
||||
#
|
||||
# These targets provide a single, consistent venv setup across all
|
||||
# devx-integrated projects (infra, grm, devx). Each project includes
|
||||
# devx.mak and aliases its local targets to these.
|
||||
#
|
||||
# The venv is a standard .venv directory (no pyenv virtualenv dependency).
|
||||
# pyenv can still be used to install Python 3.12+ but the venv itself
|
||||
# is created with `python3 -m venv .venv`.
|
||||
#
|
||||
# Projects should set these variables BEFORE including devx.mak:
|
||||
# DEVX_VENV — venv directory (default: .venv)
|
||||
# DEVX_BIN — venv bin directory (default: $(DEVX_VENV)/bin)
|
||||
# DEVX_PYTHON — Python executable (default: python3; should be $(DEVX_BIN)/python after setup)
|
||||
#
|
||||
# Common aliases in project Makefiles:
|
||||
# PIP_INSTALL = $(DEVX_PIP_INSTALL)
|
||||
# venv: devx-venv
|
||||
# activate-scripts: devx-activate-scripts
|
||||
# .env: devx-env
|
||||
|
||||
# Create .venv with Python version check (3.12+ required)
|
||||
$(DEVX_VENV)/bin/activate:
|
||||
@python3 -c "import sys; v=sys.version_info; assert v >= (3, 12), f'Python 3.12+ required, found {v.major}.{v.minor}'; print(f'Python {v.major}.{v.minor}.{v.micro} OK')"
|
||||
python3 -m venv $(DEVX_VENV)
|
||||
$(DEVX_BIN)/pip install --upgrade pip setuptools wheel
|
||||
|
||||
# Alias: devx-venv creates the venv (delegates to the activate rule)
|
||||
devx-venv: $(DEVX_VENV)/bin/activate
|
||||
|
||||
# Ensure a venv exists — in CI (no pyenv), creates .venv if missing.
|
||||
# Locally, uses the existing .venv (created by `make setup` or `make devx-venv`).
|
||||
devx-ensure-venv:
|
||||
@if [ ! -f $(DEVX_BIN)/python ]; then \
|
||||
echo "[ensure-venv] Creating $(DEVX_VENV) (no venv found)..."; \
|
||||
python3 -m venv $(DEVX_VENV); \
|
||||
$(DEVX_BIN)/pip install --upgrade pip setuptools wheel; \
|
||||
fi
|
||||
|
||||
.PHONY: devx-create-task devx-create-pr devx-push devx-push-with-pr devx-check-config
|
||||
.PHONY: devx-pr-status devx-pr-logs devx-pr-label devx-pr-review devx-rebase devx-pr-rebase
|
||||
.PHONY: devx-configure-gitea-pypi devx-install-tools devx-install-checkmake devx-checkmake
|
||||
.PHONY: devx-workflow-lint devx-workflow-dryrun devx-workflow-dryrun-safe devx-workflow-check
|
||||
.PHONY: devx-notify-failure devx-install-hooks devx-activate-scripts
|
||||
.PHONY: devx-notify-failure devx-install-hooks devx-activate-scripts devx-venv devx-ensure-venv
|
||||
.PHONY: devx-lint-ruff devx-lint-format devx-typecheck devx-lint-bandit devx-lint-deps devx-lint
|
||||
.PHONY: devx-clean devx-pre-push
|
||||
.PHONY: devx-check-mutable-globals devx-check-dep-docs devx-check-test-coverage devx-check-docs devx-check-test-speed
|
||||
@@ -165,12 +205,6 @@ devx-env:
|
||||
echo "Created .env from .env.example — please edit it with your credentials."; \
|
||||
fi
|
||||
|
||||
# Create Python venv with version check
|
||||
devx-venv:
|
||||
@python3 -c "import sys; v=sys.version_info; assert v >= (3, 12), f'Python 3.12+ required, found {v.major}.{v.minor}'; print(f'Python {v.major}.{v.minor}.{v.micro} OK')"
|
||||
$(DEVX_PYTHON) -m venv $(DEVX_VENV)
|
||||
$(DEVX_BIN)/pip install --upgrade pip setuptools wheel
|
||||
|
||||
# Create activate scripts for shell/fish/zsh
|
||||
devx-activate-scripts:
|
||||
@test -f activate.sh || (echo '#!/usr/bin/env bash' > activate.sh && echo 'source "$$(cd "$$(dirname "$${BASH_SOURCE[0]}")" && pwd)/.venv/bin/activate"' >> activate.sh && chmod +x activate.sh)
|
||||
@@ -266,7 +300,7 @@ devx-lint: devx-lint-ruff devx-lint-format devx-typecheck devx-lint-bandit
|
||||
# ── Testing ───────────────────────────────────────────────────────────────────
|
||||
|
||||
devx-test-unit:
|
||||
@$(DEVX_BIN)/pytest $(DEVX_TEST_PATHS) -v --no-cov
|
||||
@$(DEVX_BIN)/pytest $(DEVX_TEST_PATHS) -q --no-cov
|
||||
|
||||
devx-pytest-cov:
|
||||
@$(DEVX_BIN)/pytest $(DEVX_TEST_PATHS) -v --cov=$(DEVX_COV_PKG) --cov-report=term-missing --cov-fail-under=100
|
||||
@@ -341,12 +375,8 @@ devx-lint-dockerfiles:
|
||||
# devx-setup-ci) — each project defines its own setup-ci target.
|
||||
|
||||
devx-setup-image:
|
||||
@if [ -d /opt/venv ]; then ln -sf /opt/venv $(DEVX_VENV); . $(DEVX_BIN)/activate; \
|
||||
_U="$${CI_GITEA_USERNAME:-emil}"; \
|
||||
if [ -n "$$CI_GITEA_TOKEN" ]; then export PIP_EXTRA_INDEX_URL="https://$$_U:$$CI_GITEA_TOKEN@$(DEVX_GITEA_PYPI_HOST)/api/packages/$(DEVX_GITEA_PYPI_ORG)/pypi/simple/"; fi; \
|
||||
pip install --no-cache-dir -e .$(if $(EXTRAS),[$(EXTRAS)],); \
|
||||
echo "[devx-setup-image] Linked /opt/venv$(if $(EXTRAS), with [$(EXTRAS)],)."; \
|
||||
else echo "[devx-setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
|
||||
@/opt/venv/bin/python -m devx.tools.setup_image --venv $(DEVX_VENV) --extras "$(EXTRAS)" \
|
||||
--gitea-host $(DEVX_GITEA_PYPI_HOST) --gitea-org $(DEVX_GITEA_PYPI_ORG)
|
||||
|
||||
# ── Docker image build / push / cleanup ───────────────────────────────────────
|
||||
#
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Set up the project inside a pre-built CI image.
|
||||
|
||||
CI images (e.g. ``ci-quality:latest``) ship with a Python virtualenv at
|
||||
``/opt/venv`` that already contains the runtime dependencies. This tool
|
||||
links that venv to ``.venv`` in the project root and installs the project
|
||||
itself in editable mode, optionally with extras.
|
||||
|
||||
If ``/opt/venv`` does not exist (local development), falls back to
|
||||
``make setup-ci`` via ``subprocess``.
|
||||
|
||||
Usage::
|
||||
|
||||
python3 -m devx.tools.setup_image # runtime deps only
|
||||
python3 -m devx.tools.setup_image --extras lint # runtime + lint deps
|
||||
python3 -m devx.tools.setup_image --extras ci,lint
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess # nosec B404
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
DEFAULT_VENV = ".venv"
|
||||
OPT_VENV = "/opt/venv"
|
||||
FALLBACK_TARGET = "setup-ci"
|
||||
|
||||
|
||||
def _build_pip_extra_index_url(
|
||||
gitea_host: str,
|
||||
gitea_org: str,
|
||||
username: str,
|
||||
token: str,
|
||||
) -> str:
|
||||
"""Build the PIP_EXTRA_INDEX_URL for the Gitea PyPI registry.
|
||||
|
||||
Returns a URL of the form:
|
||||
https://<user>:<token>@<host>/api/packages/<org>/pypi/simple/
|
||||
"""
|
||||
return f"https://{username}:{token}@{gitea_host}/api/packages/{gitea_org}/pypi/simple/"
|
||||
|
||||
|
||||
def _install_in_image(
|
||||
venv_link: str,
|
||||
opt_venv: str,
|
||||
extras: str,
|
||||
gitea_host: str,
|
||||
gitea_org: str,
|
||||
) -> None:
|
||||
"""Link /opt/venv to .venv, activate it, and pip install the project.
|
||||
|
||||
Sets ``PIP_EXTRA_INDEX_URL`` when ``CI_GITEA_TOKEN`` is available so
|
||||
that private packages from the Gitea PyPI registry can be installed.
|
||||
"""
|
||||
# Symlink /opt/venv → .venv
|
||||
link = Path(venv_link)
|
||||
if link.exists() or link.is_symlink():
|
||||
link.unlink()
|
||||
link.symlink_to(opt_venv)
|
||||
|
||||
# Build pip install command
|
||||
spec = f".[{extras}]" if extras else "."
|
||||
pip_bin = str(Path(venv_link) / "bin" / "pip")
|
||||
cmd = [pip_bin, "install", "--no-cache-dir", "-e", spec]
|
||||
|
||||
env = os.environ.copy()
|
||||
token = env.get("CI_GITEA_TOKEN", "")
|
||||
if token:
|
||||
username = env.get("CI_GITEA_USERNAME", "emil")
|
||||
env["PIP_EXTRA_INDEX_URL"] = _build_pip_extra_index_url(
|
||||
gitea_host,
|
||||
gitea_org,
|
||||
username,
|
||||
token,
|
||||
)
|
||||
|
||||
click.echo(f"[setup-image] Linked {opt_venv}" + (f" with [{extras}]" if extras else "") + ".")
|
||||
subprocess.run(cmd, check=True, env=env) # nosec B603
|
||||
|
||||
|
||||
def _fallback_to_setup_ci() -> None:
|
||||
"""Fall back to ``make setup-ci`` when /opt/venv is not present."""
|
||||
click.echo(f"[setup-image] {OPT_VENV} not found — falling back to {FALLBACK_TARGET}")
|
||||
subprocess.run( # nosec B603, B607
|
||||
["make", FALLBACK_TARGET],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--venv",
|
||||
default=DEFAULT_VENV,
|
||||
show_default=True,
|
||||
help="Path to the local venv symlink (e.g. .venv).",
|
||||
)
|
||||
@click.option(
|
||||
"--opt-venv",
|
||||
default=OPT_VENV,
|
||||
show_default=True,
|
||||
help="Path to the pre-built venv inside the CI image.",
|
||||
)
|
||||
@click.option(
|
||||
"--extras",
|
||||
default="",
|
||||
help="Comma-separated dependency extras (e.g. 'ci,lint'). Empty for runtime only.",
|
||||
)
|
||||
@click.option(
|
||||
"--gitea-host",
|
||||
default="git.oblachno.oblachno.fyi",
|
||||
show_default=True,
|
||||
help="Gitea host for the PyPI registry.",
|
||||
)
|
||||
@click.option(
|
||||
"--gitea-org",
|
||||
default="oblachno-oss",
|
||||
show_default=True,
|
||||
help="Gitea org for the PyPI registry.",
|
||||
)
|
||||
def cli(
|
||||
venv: str,
|
||||
opt_venv: str,
|
||||
extras: str,
|
||||
gitea_host: str,
|
||||
gitea_org: str,
|
||||
) -> None:
|
||||
"""Set up the project using a pre-built CI image venv."""
|
||||
if Path(opt_venv).is_dir():
|
||||
_install_in_image(venv, opt_venv, extras, gitea_host, gitea_org)
|
||||
else:
|
||||
_fallback_to_setup_ci()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
cli() # pragma: no cover
|
||||
Reference in New Issue
Block a user