mirror of
https://github.com/chatmail/relay.git
synced 2026-05-10 16:04:37 +00:00
refactor: extract sdist build into util with lock-based idempotency
Move _build_chatmaild from deployers.py into util.py as build_chatmaild_sdist() with fcntl-based file locking so parallel deploys do not race on the sdist. The build is called once from run_cmd() before pyinfra starts; deployers.py now only calls get_chatmaild_sdist() to locate the pre-built archive. Add test_build_chatmaild_sdist and test_get_chatmaild_sdist_errors.
This commit is contained in:
@@ -34,6 +34,7 @@ from .sshexec import (
|
||||
resolve_host_from_ssh_config,
|
||||
resolve_key_from_ssh_config,
|
||||
)
|
||||
from .util import build_chatmaild_sdist
|
||||
from .www import main as webdev_main
|
||||
|
||||
#
|
||||
@@ -120,6 +121,9 @@ def run_cmd(args, out):
|
||||
env["CHATMAIL_WEBSITE_ONLY"] = "True" if args.website_only else ""
|
||||
env["CHATMAIL_DISABLE_MAIL"] = "True" if args.disable_mail else ""
|
||||
env["CHATMAIL_REQUIRE_IROH"] = "True" if require_iroh else ""
|
||||
|
||||
if not args.website_only:
|
||||
build_chatmaild_sdist()
|
||||
if not args.dns_check_disabled:
|
||||
env["CHATMAIL_ADDR_V4"] = remote_data.get("A") or ""
|
||||
env["CHATMAIL_ADDR_V6"] = remote_data.get("AAAA") or ""
|
||||
|
||||
@@ -3,9 +3,6 @@ Chat Mail pyinfra deploy.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
|
||||
@@ -18,7 +15,7 @@ from pyinfra.facts.systemd import SystemdEnabled
|
||||
from pyinfra.operations import apt, files, pip, server, systemd
|
||||
|
||||
from cmdeploy.cmdeploy import Out
|
||||
from cmdeploy.util import get_version_string
|
||||
from cmdeploy.util import get_chatmaild_sdist, get_version_string
|
||||
|
||||
from .acmetool import AcmetoolDeployer
|
||||
from .basedeploy import (
|
||||
@@ -55,20 +52,6 @@ class Port(FactBase):
|
||||
return output[0]
|
||||
|
||||
|
||||
def _build_chatmaild(dist_dir) -> None:
|
||||
dist_dir = Path(dist_dir).resolve()
|
||||
if dist_dir.exists():
|
||||
shutil.rmtree(dist_dir)
|
||||
dist_dir.mkdir()
|
||||
subprocess.check_output(
|
||||
[sys.executable, "-m", "build", "-n"]
|
||||
+ ["--sdist", "chatmaild", "--outdir", str(dist_dir)]
|
||||
)
|
||||
entries = list(dist_dir.iterdir())
|
||||
assert len(entries) == 1
|
||||
return entries[0]
|
||||
|
||||
|
||||
def remove_legacy_artifacts():
|
||||
if not has_systemd():
|
||||
return
|
||||
@@ -84,7 +67,7 @@ def remove_legacy_artifacts():
|
||||
|
||||
def _install_remote_venv_with_chatmaild() -> None:
|
||||
remove_legacy_artifacts()
|
||||
dist_file = _build_chatmaild(dist_dir=Path("chatmaild/dist"))
|
||||
dist_file = get_chatmaild_sdist()
|
||||
remote_base_dir = "/usr/local/lib/chatmaild"
|
||||
remote_dist_file = f"{remote_base_dir}/dist/{dist_file.name}"
|
||||
remote_venv_dir = f"{remote_base_dir}/venv"
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
from cmdeploy.util import collapse, get_git_hash, get_version_string, shell
|
||||
import pytest
|
||||
|
||||
from cmdeploy.util import (
|
||||
build_chatmaild_sdist,
|
||||
collapse,
|
||||
get_chatmaild_sdist,
|
||||
get_git_hash,
|
||||
get_version_string,
|
||||
shell,
|
||||
)
|
||||
|
||||
|
||||
def test_collapse():
|
||||
@@ -51,3 +60,28 @@ def test_git_helpers_with_commits_and_diffs(tmp_path):
|
||||
new_hash = get_git_hash(root=tmp_path)
|
||||
assert new_hash != git_hash
|
||||
assert get_version_string(root=tmp_path) == new_hash
|
||||
|
||||
|
||||
def test_build_chatmaild_sdist(tmp_path):
|
||||
dist_dir = tmp_path / "dist"
|
||||
|
||||
# First call builds the sdist
|
||||
result = build_chatmaild_sdist(dist_dir)
|
||||
assert result.name.endswith(".tar.gz")
|
||||
assert result.stat().st_size > 0
|
||||
|
||||
# Second call is idempotent — returns the same file, no rebuild
|
||||
mtime = result.stat().st_mtime
|
||||
result2 = build_chatmaild_sdist(dist_dir)
|
||||
assert result2 == result
|
||||
assert result2.stat().st_mtime == mtime
|
||||
|
||||
|
||||
def test_get_chatmaild_sdist_errors(tmp_path):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
get_chatmaild_sdist(tmp_path / "nonexistent")
|
||||
|
||||
empty = tmp_path / "empty"
|
||||
empty.mkdir()
|
||||
with pytest.raises(FileNotFoundError):
|
||||
get_chatmaild_sdist(empty)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Shared utility functions for cmdeploy."""
|
||||
|
||||
import fcntl
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
@@ -15,11 +17,11 @@ def collapse(text):
|
||||
|
||||
Handy for writing shell commands across multiple lines::
|
||||
|
||||
cmd = collapse(f\"""
|
||||
cmd = collapse(f\"\"\"
|
||||
cmdeploy run
|
||||
--config {ct.ini}
|
||||
--ssh-host {ct.domain}
|
||||
\""")
|
||||
\"\"\")
|
||||
"""
|
||||
return textwrap.dedent(text).replace("\n", " ").strip()
|
||||
|
||||
@@ -67,3 +69,42 @@ def get_version_string(root=None):
|
||||
if git_diff:
|
||||
return f"{git_hash}\n{git_diff}"
|
||||
return git_hash
|
||||
|
||||
|
||||
def _chatmaild_default_dist_dir():
|
||||
return _project_root() / "chatmaild" / "dist"
|
||||
|
||||
|
||||
def build_chatmaild_sdist(dist_dir=None):
|
||||
"""Build the chatmaild sdist if not already present (idempotent, process-safe)."""
|
||||
|
||||
if dist_dir is None:
|
||||
dist_dir = _chatmaild_default_dist_dir()
|
||||
dist_dir = Path(dist_dir).resolve()
|
||||
dist_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
lockfile = dist_dir.parent / ".dist.lock"
|
||||
with open(lockfile, "w") as fh:
|
||||
fcntl.flock(fh, fcntl.LOCK_EX)
|
||||
existing = [p for p in dist_dir.iterdir() if p.suffix == ".gz"]
|
||||
if existing:
|
||||
return existing[0]
|
||||
subprocess.check_output(
|
||||
[sys.executable, "-m", "build", "-n"]
|
||||
+ ["--sdist", "chatmaild", "--outdir", str(dist_dir)],
|
||||
cwd=str(_project_root()),
|
||||
)
|
||||
return get_chatmaild_sdist(dist_dir)
|
||||
|
||||
|
||||
def get_chatmaild_sdist(dist_dir=None):
|
||||
"""Return the path to the pre-built chatmaild sdist."""
|
||||
if dist_dir is None:
|
||||
dist_dir = _chatmaild_default_dist_dir()
|
||||
|
||||
entries = list(Path(dist_dir).iterdir())
|
||||
if len(entries) == 0:
|
||||
raise FileNotFoundError(f"dist directory is empty: {dist_dir}")
|
||||
if len(entries) > 1:
|
||||
raise ValueError(f"expected one file in {dist_dir}, found {len(entries)}")
|
||||
return entries[0]
|
||||
|
||||
Reference in New Issue
Block a user