From 1f0dd2549f258729657efbeb4d4c9f9d2766c95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 23 Nov 2025 21:20:37 +0000 Subject: [PATCH 1/8] feat: enable debmagic to build itself via debuild --- debian/control | 10 +++++++-- debian/debmagic.1 | 43 ++++++++++++++++++++++++++++++++++++ debian/rules | 11 ++++++--- pyproject.toml | 3 ++- src/debmagic/_build_stage.py | 1 + src/debmagic/_module/dh.py | 25 +++++++++------------ src/debmagic/_package.py | 9 +++++--- src/debmagic/_preset.py | 2 ++ 8 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 debian/debmagic.1 diff --git a/debian/control b/debian/control index a36695f..14c0759 100644 --- a/debian/control +++ b/debian/control @@ -5,10 +5,15 @@ Uploaders: Jonas Jelten , Build-Depends: debhelper-compat (= 13), + dh-python, + python3-all, + pybuild-plugin-pyproject, + python3-setuptools, python3-debian Rules-Requires-Root: no X-Style: black Standards-Version: 4.7.2 +X-Python-Version: >= 3.12 Homepage: https://github.com/SFTtech/debmagic Vcs-Git: https://github.com/SFTtech/debmagic.git Vcs-Browser: https://github.com/SFTtech/debmagic @@ -17,7 +22,8 @@ Package: debmagic Architecture: all Depends: python3-debian, + ${misc:Depends}, ${python3:Depends} Multi-Arch: foreign -Description: TODO - TODO +Description: Debian build instructions written in Python. + Explicit is better than implicit. diff --git a/debian/debmagic.1 b/debian/debmagic.1 new file mode 100644 index 0000000..4550f3e --- /dev/null +++ b/debian/debmagic.1 @@ -0,0 +1,43 @@ +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. +.TH DEBMAGIC "1" "November 2025" "debmagic 0.1.0" "User Commands" +.SH NAME +debmagic \- manual page for debmagic 0.1.0 +.SH DESCRIPTION +usage: debmagic [\-h] [\-\-version] {help,version,debuild,build} ... +.PP +Debmagic +.SS "positional arguments:" +.IP +{help,version,debuild,build} +.TP +help +Show this help page and exit +.TP +version +Print the version information and exit +.TP +debuild +Simply run debuild in the current working directory +.TP +build +Buidl a debian package with the selected +containerization driver +.SS "options:" +.TP +\fB\-h\fR, \fB\-\-help\fR +show this help message and exit +.TP +\fB\-\-version\fR +show program's version number and exit +.SH "SEE ALSO" +The full documentation for +.B debmagic +is maintained as a Texinfo manual. If the +.B info +and +.B debmagic +programs are properly installed at your site, the command +.IP +.B info debmagic +.PP +should give you access to the complete manual. diff --git a/debian/rules b/debian/rules index 863a4e6..05d1894 100755 --- a/debian/rules +++ b/debian/rules @@ -6,8 +6,13 @@ from pathlib import Path repo_root = Path(__file__).parent.parent / "src" sys.path.append(str(repo_root)) -from debmagic.v1 import package, python +from debmagic.v0 import package +from debmagic.v0 import dh as dh_mod -package( - preset=python, +dh = dh_mod.Preset(dh_args=["--with", "python3", "--buildsystem=pybuild"]) + +pkg = package( + preset=[dh], ) + +pkg.pack() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 56ed6e9..327f6e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,8 @@ name = "debmagic" version = "0.1.0" description = "build debian packages" -license = { file = "LICENSE" } +license = "GPL-2.0-or-later" +license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.12" classifiers = ["Programming Language :: Python :: 3"] diff --git a/src/debmagic/_build_stage.py b/src/debmagic/_build_stage.py index a7fd29d..64aad14 100644 --- a/src/debmagic/_build_stage.py +++ b/src/debmagic/_build_stage.py @@ -2,6 +2,7 @@ class BuildStage(StrEnum): + clean = "clean" prepare = "prepare" configure = "configure" build = "build" diff --git a/src/debmagic/_module/dh.py b/src/debmagic/_module/dh.py index bf58875..c64e34e 100644 --- a/src/debmagic/_module/dh.py +++ b/src/debmagic/_module/dh.py @@ -32,11 +32,9 @@ class DHSequenceID(StrEnum): class Preset(PresetBase): - def __init__(self, dh_invocation: str | None = None): - if not dh_invocation: - dh_invocation = "dh" - self._dh_invocation: str = dh_invocation - self._overrides: dict[str, DHOverride] = {} + def __init__(self, dh_args: list[str] | None = None): + self._dh_args: list[str] = dh_args or [] + self._overrides: dict[str, DHOverride] = dict() self._initialized = False # debmagic's stages, with matching commands from the dh sequence @@ -52,7 +50,7 @@ def __init__(self, dh_invocation: str | None = None): def initialize(self, src_pkg: SourcePackage) -> None: # get all steps the dh sequence would do - self._populate_stages(self._dh_invocation, base_dir=src_pkg.base_dir) + self._populate_stages(self._dh_args, base_dir=src_pkg.base_dir) self._initialized = True def clean(self, build: Build): @@ -97,7 +95,7 @@ def _run_dh_seq_cmds(self, build: Build, seq_cmds: list[str]) -> None: else: build.cmd(cmd, cwd=build.source_dir) - def _populate_stages(self, dh_invocation: str, base_dir: Path) -> None: + def _populate_stages(self, dh_args: list[str], base_dir: Path) -> None: """ split up the dh sequences into debmagic's stages. this involves guessing, since dh only has "build" (=configure, build, test) @@ -106,10 +104,10 @@ def _populate_stages(self, dh_invocation: str, base_dir: Path) -> None: if you have a better idea how to map dh sequences to debmagic's stages, please tell us. """ ## clean, which is 1:1 fortunately - self._clean_seq = self._get_dh_seq(base_dir, dh_invocation, DHSequenceID.clean) + self._clean_seq = self._get_dh_seq(base_dir, dh_args, DHSequenceID.clean) ## untangle "build" to configure & build & test - build_seq_raw = self._get_dh_seq(base_dir, dh_invocation, DHSequenceID.build) + build_seq_raw = self._get_dh_seq(base_dir, dh_args, DHSequenceID.build) if build_seq_raw[-1] != "create-stamp debian/debhelper-build-stamp": raise RuntimeError("build stamp creation line missing from dh build sequence") build_seq = build_seq_raw[:-1] # remove that stamp line @@ -128,9 +126,9 @@ def _populate_stages(self, dh_invocation: str, base_dir: Path) -> None: self._test_seq = build_seq[auto_test_idx:] ## untangle "binary" to install & package - install_seq = self._get_dh_seq(base_dir, dh_invocation, DHSequenceID.install) + install_seq = self._get_dh_seq(base_dir, dh_args, DHSequenceID.install) self._install_seq = list_strip_head(install_seq, build_seq_raw) - binary_seq = self._get_dh_seq(base_dir, dh_invocation, DHSequenceID.binary) + binary_seq = self._get_dh_seq(base_dir, dh_args, DHSequenceID.binary) self._package_seq = list_strip_head(binary_seq, install_seq) # register all sequence items for validity checks @@ -147,9 +145,8 @@ def _populate_stages(self, dh_invocation: str, base_dir: Path) -> None: cmd_id = cmd[0] self._seq_ids.add(cmd_id) - def _get_dh_seq(self, base_dir: Path, dh_invocation: str, seq: DHSequenceID) -> list[str]: - dh_base_cmd = shlex.split(dh_invocation) - cmd = [*dh_base_cmd, str(seq), "--no-act"] + def _get_dh_seq(self, base_dir: Path, dh_args: list[str], seq: DHSequenceID) -> list[str]: + cmd = ["dh", str(seq), "--no-act", *dh_args] proc = run_cmd(cmd, cwd=base_dir, capture_output=True, text=True) lines = proc.stdout.splitlines() return [line.strip() for line in lines] diff --git a/src/debmagic/_package.py b/src/debmagic/_package.py index 3c4e20e..039705f 100644 --- a/src/debmagic/_package.py +++ b/src/debmagic/_package.py @@ -43,7 +43,9 @@ def _parse_args(custom_functions: dict[str, CustomFunction] = {}): common_cli = argparse.ArgumentParser(add_help=False) common_cli.add_argument( - "--dry-run", action="store_true", help="don't actually run anything that changes the system/package state" + "--dry-run", + action="store_true", + help="don't actually run anything that changes the system/package state", ) # debian-required "targets": @@ -186,7 +188,7 @@ def pack(self): flags=self.buildflags, parallel=multiprocessing.cpu_count(), # TODO prefix=Path("/usr"), # TODO - dry_run=args.__dict__.get("dry_run"), # TODO + dry_run=args.dry_run, ) match args.operation: @@ -228,7 +230,8 @@ def pack(self): func_args = {k: vars(args)[k] for k in func.args.keys()} func.fun(**func_args) else: - cli.print_help(f"call to unknown operation {args.operation!r}") + print(f"call to unknown operation {args.operation!r}\n") + cli.print_help() cli.exit(1) diff --git a/src/debmagic/_preset.py b/src/debmagic/_preset.py index a10a427..138cf02 100644 --- a/src/debmagic/_preset.py +++ b/src/debmagic/_preset.py @@ -77,6 +77,8 @@ def _get_member(obj: type[T], stage: BuildStage) -> _PresetBuildStepClassMethod: def _get_member(obj: T | type[T], stage: BuildStage) -> BuildStep | _PresetBuildStepClassMethod: match stage: + case BuildStage.clean: + return obj.clean case BuildStage.prepare: return obj.prepare case BuildStage.configure: From c34a9e652cb858dcfbefca5c173f5e5eb8689900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 23 Nov 2025 21:23:05 +0000 Subject: [PATCH 2/8] feat: add debmagic cli entrypoint with build drivers --- src/debmagic/__main__.py | 4 + src/debmagic/_build_driver/__init__.py | 0 src/debmagic/_build_driver/build.py | 35 ++++++ src/debmagic/_build_driver/common.py | 43 +++++++ src/debmagic/_build_driver/driver_docker.py | 121 ++++++++++++++++++++ src/debmagic/_build_driver/driver_lxd.py | 16 +++ src/debmagic/_build_driver/driver_none.py | 28 +++++ src/debmagic/_utils.py | 86 +++++++++++++- src/debmagic/_version.py | 1 + src/debmagic/cli.py | 52 ++++++++- 10 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 src/debmagic/__main__.py create mode 100644 src/debmagic/_build_driver/__init__.py create mode 100644 src/debmagic/_build_driver/build.py create mode 100644 src/debmagic/_build_driver/common.py create mode 100644 src/debmagic/_build_driver/driver_docker.py create mode 100644 src/debmagic/_build_driver/driver_lxd.py create mode 100644 src/debmagic/_build_driver/driver_none.py create mode 100644 src/debmagic/_version.py diff --git a/src/debmagic/__main__.py b/src/debmagic/__main__.py new file mode 100644 index 0000000..9ae637f --- /dev/null +++ b/src/debmagic/__main__.py @@ -0,0 +1,4 @@ +from .cli import main + +if __name__ == "__main__": + main() diff --git a/src/debmagic/_build_driver/__init__.py b/src/debmagic/_build_driver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/debmagic/_build_driver/build.py b/src/debmagic/_build_driver/build.py new file mode 100644 index 0000000..1972aff --- /dev/null +++ b/src/debmagic/_build_driver/build.py @@ -0,0 +1,35 @@ +from debmagic._build_driver.driver_docker import BuildDriverDocker +from debmagic._build_driver.driver_lxd import BuildDriverLxd +from debmagic._build_driver.driver_none import BuildDriverNone + +from .common import BuildConfig, BuildDriver, BuildDriverType + + +def _create_driver(build_driver: BuildDriverType, config: BuildConfig) -> BuildDriver: + match build_driver: + case "docker": + return BuildDriverDocker.create(config=config) + case "lxd": + return BuildDriverLxd.create(config=config) + case "none": + return BuildDriverNone.create(config=config) + + +def build(build_driver: BuildDriverType, config: BuildConfig): + driver = _create_driver(build_driver, config) + try: + driver.run_command(["apt-get", "-y", "build-dep", "."], cwd=config.source_dir) + driver.run_command(["debuild", "-nc", "-uc", "-b"], cwd=config.source_dir) + + # TODO: copy packages to output directory + config.output_dir.mkdir(parents=True, exist_ok=True) + driver.copy_file(source_dir=config.source_dir / "..", glob="debmagic_*.deb", dest_dir=config.output_dir) + except Exception as e: + print(e) + print( + "Something failed during building - dropping into interactive shell in build environment for easier debugging" + ) + driver.drop_into_shell() + raise e + finally: + driver.cleanup() diff --git a/src/debmagic/_build_driver/common.py b/src/debmagic/_build_driver/common.py new file mode 100644 index 0000000..64a20f8 --- /dev/null +++ b/src/debmagic/_build_driver/common.py @@ -0,0 +1,43 @@ +import abc +from dataclasses import dataclass +from pathlib import Path +from typing import Literal, Sequence + +BuildDriverType = Literal["docker"] | Literal["lxd"] | Literal["none"] +SUPPORTED_BUILD_DRIVERS: list[BuildDriverType] = ["docker", "none"] + + +class BuildError(RuntimeError): + pass + + +@dataclass +class BuildConfig: + source_dir: Path + output_dir: Path + dry_run: bool + distro_version: str # e.g. trixie + distro: str = "debian" + + +class BuildDriver: + @classmethod + @abc.abstractmethod + def create(cls, config: BuildConfig) -> "BuildDriver": + pass + + @abc.abstractmethod + def run_command(self, args: Sequence[str | Path], cwd: Path | None = None): + pass + + @abc.abstractmethod + def copy_file(self, source_dir: Path, glob: str, dest_dir: Path): + pass + + @abc.abstractmethod + def cleanup(self): + pass + + @abc.abstractmethod + def drop_into_shell(self): + pass diff --git a/src/debmagic/_build_driver/driver_docker.py b/src/debmagic/_build_driver/driver_docker.py new file mode 100644 index 0000000..c937324 --- /dev/null +++ b/src/debmagic/_build_driver/driver_docker.py @@ -0,0 +1,121 @@ +import tempfile +import uuid +from pathlib import Path +from typing import Sequence + +from debmagic._build_driver.common import BuildConfig, BuildDriver, BuildError +from debmagic._utils import run_cmd, run_cmd_in_foreground + +WORKDIR_IN_CONTAINER = Path("/work/source") +OUTPUT_DIR_IN_CONTAINER = Path("/output") + +DOCKERFILE_TEMPLATE = f""" +FROM docker.io/{{distro}}:{{distro_version}} + +RUN apt-get update && apt-get -y install devscripts + +RUN mkdir -p {WORKDIR_IN_CONTAINER} {OUTPUT_DIR_IN_CONTAINER} +WORKDIR {WORKDIR_IN_CONTAINER} +COPY --from=source_dir . {WORKDIR_IN_CONTAINER} +ENTRYPOINT ["sleep", "infinity"] +""" + + +class BuildDriverDocker(BuildDriver): + def __init__(self, workdir: tempfile.TemporaryDirectory, config: BuildConfig, container_name: str): + self._workdir = workdir + self._workdir_path = Path(workdir.name) + self._config = config + + self._container_name = container_name + + def _translate_source_path(self, path_in_source: Path) -> Path: + if not path_in_source.is_relative_to(self._config.source_dir): + raise BuildError("Cannot run in a path not relative to the original source directory") + rel = path_in_source.relative_to(self._config.source_dir) + return WORKDIR_IN_CONTAINER / rel + + def _translate_output_path(self, path_in_output: Path) -> Path: + if not path_in_output.is_relative_to(self._config.output_dir): + raise BuildError("Cannot run in a path not relative to the original output directory") + rel = path_in_output.relative_to(self._config.output_dir) + return OUTPUT_DIR_IN_CONTAINER / rel + + @classmethod + def create(cls, config: BuildConfig): + workdir = tempfile.TemporaryDirectory() + workdir_path = Path(workdir.name) + + formatted_dockerfile = DOCKERFILE_TEMPLATE.format( + distro=config.distro, + distro_version=config.distro_version, + ) + + dockerfile_path = workdir_path / "Dockerfile" + dockerfile_path.write_text(formatted_dockerfile) + gitignore_file = config.source_dir / ".gitignore" + if gitignore_file.is_file(): + (workdir_path / ".dockerignore").write_text(gitignore_file.read_text()) + + docker_image_name = str(uuid.uuid4()) + ret = run_cmd( + [ + "docker", + "build", + "--tag", + docker_image_name, + "-f", + dockerfile_path, + "--build-context", + f"source_dir={config.source_dir}", + workdir_path, + ], + dry_run=config.dry_run, + check=False, + ) + if ret.returncode != 0: + raise BuildError("Error creating docker image for build") + + docker_container_name = str(uuid.uuid4()) + ret = run_cmd( + [ + "docker", + "run", + "--detach", + "--name", + docker_container_name, + "--mount", + f"type=bind,src={config.output_dir},dst={OUTPUT_DIR_IN_CONTAINER}", + docker_image_name, + ], + dry_run=config.dry_run, + check=False, + ) + if ret.returncode != 0: + raise BuildError("Error creating docker image for build") + + instance = cls(config=config, workdir=workdir, container_name=docker_container_name) + return instance + + def run_command(self, args: Sequence[str | Path], cwd: Path | None = None): + if cwd: + cwd = self._translate_source_path(cwd) + + ret = run_cmd(["docker", "exec", self._container_name, *args], dry_run=self._config.dry_run) + if ret.returncode != 0: + raise BuildError("Error building package") + + def copy_file(self, source_dir: Path, glob: str, dest_dir: Path): + translated_source = self._translate_source_path(source_dir) + translated_output = self._translate_output_path(dest_dir) + self.run_command(["/usr/bin/env", "bash", "-c", f"cp -f {translated_source}/{glob} {translated_output}"]) + + def cleanup(self): + run_cmd(["docker", "rm", "-f", self._container_name], dry_run=self._config.dry_run) + self._workdir.cleanup() + + def drop_into_shell(self): + if not self._config.dry_run: + run_cmd_in_foreground( + ["docker", "exec", "--interactive", "--tty", self._container_name, "/usr/bin/env", "bash"] + ) diff --git a/src/debmagic/_build_driver/driver_lxd.py b/src/debmagic/_build_driver/driver_lxd.py new file mode 100644 index 0000000..3d8c82b --- /dev/null +++ b/src/debmagic/_build_driver/driver_lxd.py @@ -0,0 +1,16 @@ +from pathlib import Path +from typing import Sequence + +from debmagic._build_driver.common import BuildConfig, BuildDriver + + +class BuildDriverLxd(BuildDriver): + @classmethod + def create(cls, config: BuildConfig): + return cls() + + def run_command(self, args: Sequence[str | Path], cwd: Path | None = None): + raise NotImplementedError() + + def cleanup(self): + pass diff --git a/src/debmagic/_build_driver/driver_none.py b/src/debmagic/_build_driver/driver_none.py new file mode 100644 index 0000000..8546237 --- /dev/null +++ b/src/debmagic/_build_driver/driver_none.py @@ -0,0 +1,28 @@ +import shutil +from pathlib import Path +from typing import Sequence + +from debmagic._build_driver.common import BuildConfig, BuildDriver +from debmagic._utils import run_cmd, run_cmd_in_foreground + + +class BuildDriverNone(BuildDriver): + def __init__(self, config: BuildConfig) -> None: + self._config = config + + @classmethod + def create(cls, config: BuildConfig): + return cls(config=config) + + def run_command(self, args: Sequence[str | Path], cwd: Path | None = None): + run_cmd(args=args, dry_run=self._config.dry_run, cwd=cwd) + + def copy_file(self, source_dir: Path, glob: str, dest_dir: Path): + for file in source_dir.glob(glob): + shutil.copy(file, dest_dir) + + def cleanup(self): + pass + + def drop_into_shell(self): + run_cmd_in_foreground(["/usr/bin/env", "bash"]) diff --git a/src/debmagic/_utils.py b/src/debmagic/_utils.py index 680b696..9ccd21a 100644 --- a/src/debmagic/_utils.py +++ b/src/debmagic/_utils.py @@ -1,8 +1,11 @@ import io +import os import re import shlex +import signal import subprocess import sys +from pathlib import Path from typing import Sequence, TypeVar @@ -37,17 +40,17 @@ def get(self, key): def run_cmd( - cmd: Sequence[str] | str, + cmd: Sequence[str | Path] | str, check: bool = True, dry_run: bool = False, **kwargs, ) -> subprocess.CompletedProcess: - cmd_args: Sequence[str] | str = cmd + cmd_args: Sequence[str | Path] | str = cmd cmd_pretty: str if kwargs.get("shell"): if not isinstance(cmd, str): - cmd_args = shlex.join(cmd) + cmd_args = shlex.join([str(x) for x in cmd]) else: if isinstance(cmd, str): cmd_args = shlex.split(cmd) @@ -55,7 +58,7 @@ def run_cmd( if isinstance(cmd, str): cmd_pretty = cmd else: - cmd_pretty = shlex.join(cmd_args) + cmd_pretty = shlex.join([str(x) for x in cmd_args]) print(f"debmagic: {cmd_pretty}") @@ -70,6 +73,81 @@ def run_cmd( return ret +def run_cmd_in_foreground(args: Sequence[str | Path], **kwargs): + """ + the "correct" way of spawning a new subprocess: + signals like C-c must only go + to the child process, and not to this python. + + the args are the same as subprocess.Popen + + returns Popen().wait() value + + Some side-info about "how ctrl-c works": + https://unix.stackexchange.com/a/149756/1321 + + fun fact: this function took a whole night + to be figured out. + """ + cmd_pretty = shlex.join([str(x) for x in args]) + print(f"debmagic[fg]: {cmd_pretty}") + + # import here to only use the dependency if really necessary (not available on Windows) + import termios + + old_pgrp = os.tcgetpgrp(sys.stdin.fileno()) + old_attr = termios.tcgetattr(sys.stdin.fileno()) + + # generally, the child process should stop itself + # before exec so the parent can set its new pgid. + # (setting pgid has to be done before the child execs). + # however, Python 'guarantee' that `preexec_fn` + # is run before `Popen` returns. + # this is because `Popen` waits for the closure of + # the error relay pipe '`errpipe_write`', + # which happens at child's exec. + # this is also the reason the child can't stop itself + # in Python's `Popen`, since the `Popen` call would never + # terminate then. + # `os.kill(os.getpid(), signal.SIGSTOP)` + + try: + # fork the child + child = subprocess.Popen(args, process_group=os.getpid(), **kwargs) + + # we can't set the process group id from the parent since the child + # will already have exec'd. and we can't SIGSTOP it before exec, + # see above. + # `os.setpgid(child.pid, child.pid)` + + # set the child's process group as new foreground + os.tcsetpgrp(sys.stdin.fileno(), child.pid) + # revive the child, + # because it may have been stopped due to SIGTTOU or + # SIGTTIN when it tried using stdout/stdin + # after setpgid was called, and before we made it + # forward process by tcsetpgrp. + os.kill(child.pid, signal.SIGCONT) + + # wait for the child to terminate + ret = child.wait() + + finally: + # we have to mask SIGTTOU because tcsetpgrp + # raises SIGTTOU to all current background + # process group members (i.e. us) when switching tty's pgrp + # it we didn't do that, we'd get SIGSTOP'd + hdlr = signal.signal(signal.SIGTTOU, signal.SIG_IGN) + # make us tty's foreground again + os.tcsetpgrp(sys.stdin.fileno(), old_pgrp) + # now restore the handler + signal.signal(signal.SIGTTOU, hdlr) + # restore terminal attributes + termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_attr) + + return ret + + def disable_output_buffer(): # always flush output sys.stdout = io.TextIOWrapper(open(sys.stdout.fileno(), "wb", 0), write_through=True) diff --git a/src/debmagic/_version.py b/src/debmagic/_version.py new file mode 100644 index 0000000..1cf6267 --- /dev/null +++ b/src/debmagic/_version.py @@ -0,0 +1 @@ +VERSION = "0.1.0" diff --git a/src/debmagic/cli.py b/src/debmagic/cli.py index 67b4f56..5343eef 100644 --- a/src/debmagic/cli.py +++ b/src/debmagic/cli.py @@ -1,14 +1,54 @@ import argparse +from pathlib import Path +from debmagic._build_driver.build import build as build_driver_build +from debmagic._build_driver.common import SUPPORTED_BUILD_DRIVERS, BuildConfig from debmagic._utils import run_cmd +from debmagic._version import VERSION -def parse_args(): - parser = argparse.ArgumentParser(description="Debmagic") - return parser.parse_args() +def _parse_args(): + cli = argparse.ArgumentParser(description="Debmagic") + sp = cli.add_subparsers(dest="operation") + + cli.add_argument("--version", action="version", version=f"%(prog)s {VERSION}") + sp.add_parser("help", help="Show this help page and exit") + sp.add_parser("version", help="Print the version information and exit") + + common_cli = argparse.ArgumentParser(add_help=False) + common_cli.add_argument( + "--dry-run", action="store_true", help="don't actually run anything that changes the system/package state" + ) + + sp.add_parser("debuild", parents=[common_cli], help="Simply run debuild in the current working directory") + build_cli = sp.add_parser( + "build", parents=[common_cli], help="Build a debian package with the selected containerization driver" + ) + build_cli.add_argument("--driver", choices=SUPPORTED_BUILD_DRIVERS, default="none") + build_cli.add_argument("-s", "--source-dir", type=Path, default=Path.cwd()) + build_cli.add_argument("-o", "--output-dir", type=Path, default=Path.cwd()) + + return cli, cli.parse_args() def main(): - # args = parse_args() - # just call debuild for now - run_cmd(["debuild", "-nc", "-uc", "-b"]) + cli, args = _parse_args() + + match args.operation: + case "help": + cli.print_help() + cli.exit(0) + case "version": + print(f"{cli.prog} {VERSION}") + cli.exit(0) + case "build": + build_config = BuildConfig( + output_dir=args.output_dir, + source_dir=args.source_dir, + distro="debian", + distro_version="trixie", + dry_run=args.dry_run, + ) + build_driver_build(build_driver=args.driver, config=build_config) + case "debuild": + run_cmd(["debuild", "-nc", "-uc", "-b"]) From d13bab010207d17f5c09f3f94ed5b6674951e9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 23 Nov 2025 21:25:04 +0000 Subject: [PATCH 3/8] ci: add stage where debmagic packages itself --- .github/workflows/ci.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 803d731..f60c4f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,3 +107,22 @@ jobs: # run: uv sync --locked --all-extras --dev # - name: Run Integrationtests # run: uv run pytest tests/integration + + package_ourself: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + - name: Install the project + run: uv sync --locked --all-extras --dev + - name: Install required apt + run: apt-get install -y devscripts + - name: Run Debmagic build on ourself + run: uv run debmagic build --driver=none \ No newline at end of file From 2b7c4f38f2bed867c6e6a4f1a952b26e3d6f7149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 23 Nov 2025 22:35:42 +0100 Subject: [PATCH 4/8] fix: properly handle root requirement for apt build-dep in build drivers --- .github/workflows/ci.yml | 2 +- src/debmagic/_build_driver/build.py | 2 +- src/debmagic/_build_driver/common.py | 2 +- src/debmagic/_build_driver/driver_docker.py | 4 ++- src/debmagic/_build_driver/driver_lxd.py | 10 ++++-- src/debmagic/_build_driver/driver_none.py | 5 ++- src/debmagic/_utils.py | 37 +++++++++++++-------- 7 files changed, 41 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f60c4f5..c827b07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,6 +123,6 @@ jobs: - name: Install the project run: uv sync --locked --all-extras --dev - name: Install required apt - run: apt-get install -y devscripts + run: sudo apt-get install -y devscripts - name: Run Debmagic build on ourself run: uv run debmagic build --driver=none \ No newline at end of file diff --git a/src/debmagic/_build_driver/build.py b/src/debmagic/_build_driver/build.py index 1972aff..0182c21 100644 --- a/src/debmagic/_build_driver/build.py +++ b/src/debmagic/_build_driver/build.py @@ -18,7 +18,7 @@ def _create_driver(build_driver: BuildDriverType, config: BuildConfig) -> BuildD def build(build_driver: BuildDriverType, config: BuildConfig): driver = _create_driver(build_driver, config) try: - driver.run_command(["apt-get", "-y", "build-dep", "."], cwd=config.source_dir) + driver.run_command(["apt-get", "-y", "build-dep", "."], cwd=config.source_dir, requires_root=True) driver.run_command(["debuild", "-nc", "-uc", "-b"], cwd=config.source_dir) # TODO: copy packages to output directory diff --git a/src/debmagic/_build_driver/common.py b/src/debmagic/_build_driver/common.py index 64a20f8..ecd54b7 100644 --- a/src/debmagic/_build_driver/common.py +++ b/src/debmagic/_build_driver/common.py @@ -27,7 +27,7 @@ def create(cls, config: BuildConfig) -> "BuildDriver": pass @abc.abstractmethod - def run_command(self, args: Sequence[str | Path], cwd: Path | None = None): + def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): pass @abc.abstractmethod diff --git a/src/debmagic/_build_driver/driver_docker.py b/src/debmagic/_build_driver/driver_docker.py index c937324..5293099 100644 --- a/src/debmagic/_build_driver/driver_docker.py +++ b/src/debmagic/_build_driver/driver_docker.py @@ -97,7 +97,9 @@ def create(cls, config: BuildConfig): instance = cls(config=config, workdir=workdir, container_name=docker_container_name) return instance - def run_command(self, args: Sequence[str | Path], cwd: Path | None = None): + def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): + del requires_root # we assume to always be root in the container + if cwd: cwd = self._translate_source_path(cwd) diff --git a/src/debmagic/_build_driver/driver_lxd.py b/src/debmagic/_build_driver/driver_lxd.py index 3d8c82b..8bf3a2b 100644 --- a/src/debmagic/_build_driver/driver_lxd.py +++ b/src/debmagic/_build_driver/driver_lxd.py @@ -9,8 +9,14 @@ class BuildDriverLxd(BuildDriver): def create(cls, config: BuildConfig): return cls() - def run_command(self, args: Sequence[str | Path], cwd: Path | None = None): + def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): + raise NotImplementedError() + + def copy_file(self, source_dir: Path, glob: str, dest_dir: Path): raise NotImplementedError() def cleanup(self): - pass + raise NotImplementedError() + + def drop_into_shell(self): + raise NotImplementedError() diff --git a/src/debmagic/_build_driver/driver_none.py b/src/debmagic/_build_driver/driver_none.py index 8546237..eee8ccf 100644 --- a/src/debmagic/_build_driver/driver_none.py +++ b/src/debmagic/_build_driver/driver_none.py @@ -1,3 +1,4 @@ +import os import shutil from pathlib import Path from typing import Sequence @@ -14,7 +15,9 @@ def __init__(self, config: BuildConfig) -> None: def create(cls, config: BuildConfig): return cls(config=config) - def run_command(self, args: Sequence[str | Path], cwd: Path | None = None): + def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): + if requires_root and not os.getuid() == 0: + args = ["sudo", *args] run_cmd(args=args, dry_run=self._config.dry_run, cwd=cwd) def copy_file(self, source_dir: Path, glob: str, dest_dir: Path): diff --git a/src/debmagic/_utils.py b/src/debmagic/_utils.py index 9ccd21a..f79ae5b 100644 --- a/src/debmagic/_utils.py +++ b/src/debmagic/_utils.py @@ -6,7 +6,7 @@ import subprocess import sys from pathlib import Path -from typing import Sequence, TypeVar +from typing import Callable, Sequence, TypeVar class Namespace: @@ -98,22 +98,31 @@ def run_cmd_in_foreground(args: Sequence[str | Path], **kwargs): old_pgrp = os.tcgetpgrp(sys.stdin.fileno()) old_attr = termios.tcgetattr(sys.stdin.fileno()) - # generally, the child process should stop itself - # before exec so the parent can set its new pgid. - # (setting pgid has to be done before the child execs). - # however, Python 'guarantee' that `preexec_fn` - # is run before `Popen` returns. - # this is because `Popen` waits for the closure of - # the error relay pipe '`errpipe_write`', - # which happens at child's exec. - # this is also the reason the child can't stop itself - # in Python's `Popen`, since the `Popen` call would never - # terminate then. - # `os.kill(os.getpid(), signal.SIGSTOP)` + user_preexec_fn: Callable | None = kwargs.pop("preexec_fn", None) + + def new_pgid(): + if user_preexec_fn: + user_preexec_fn() + + # set a new process group id + os.setpgid(os.getpid(), os.getpid()) + + # generally, the child process should stop itself + # before exec so the parent can set its new pgid. + # (setting pgid has to be done before the child execs). + # however, Python 'guarantee' that `preexec_fn` + # is run before `Popen` returns. + # this is because `Popen` waits for the closure of + # the error relay pipe '`errpipe_write`', + # which happens at child's exec. + # this is also the reason the child can't stop itself + # in Python's `Popen`, since the `Popen` call would never + # terminate then. + # `os.kill(os.getpid(), signal.SIGSTOP)` try: # fork the child - child = subprocess.Popen(args, process_group=os.getpid(), **kwargs) + child = subprocess.Popen(args, preexec_fn=new_pgid, **kwargs) # noqa: PLW1509 # we can't set the process group id from the parent since the child # will already have exec'd. and we can't SIGSTOP it before exec, From 16dd7cd19f92c338ab4f97619d2089d9cb908b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 23 Nov 2025 22:36:54 +0100 Subject: [PATCH 5/8] ci: change self-build to use docker driver --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c827b07..054a83a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,5 @@ jobs: enable-cache: true - name: Install the project run: uv sync --locked --all-extras --dev - - name: Install required apt - run: sudo apt-get install -y devscripts - name: Run Debmagic build on ourself - run: uv run debmagic build --driver=none \ No newline at end of file + run: uv run debmagic build --driver=docker \ No newline at end of file From c474de2416e219540f26906ed7f7d9fd82edd28e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Mon, 24 Nov 2025 22:17:08 +0000 Subject: [PATCH 6/8] feat(module/dh): also allow string as dh_args --- src/debmagic/_module/dh.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/debmagic/_module/dh.py b/src/debmagic/_module/dh.py index c64e34e..f479da5 100644 --- a/src/debmagic/_module/dh.py +++ b/src/debmagic/_module/dh.py @@ -32,8 +32,15 @@ class DHSequenceID(StrEnum): class Preset(PresetBase): - def __init__(self, dh_args: list[str] | None = None): - self._dh_args: list[str] = dh_args or [] + def __init__(self, dh_args: list[str] | str | None = None): + self._dh_args: list[str] + if dh_args is None: + self._dh_args = [] + elif isinstance(dh_args, str): + self._dh_args = shlex.split(dh_args) + else: + self._dh_args = dh_args + self._overrides: dict[str, DHOverride] = dict() self._initialized = False From 390ce978f883122e53b5b4a2c071b9785df99c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Mon, 24 Nov 2025 22:18:38 +0000 Subject: [PATCH 7/8] refactor(driver): move build directory to host --- src/debmagic/_build_driver/build.py | 73 +++++++++++++++++++-- src/debmagic/_build_driver/common.py | 33 ++++++++-- src/debmagic/_build_driver/driver_docker.py | 61 ++++++----------- src/debmagic/_build_driver/driver_lxd.py | 3 - src/debmagic/_build_driver/driver_none.py | 5 -- src/debmagic/cli.py | 19 +++--- 6 files changed, 123 insertions(+), 71 deletions(-) diff --git a/src/debmagic/_build_driver/build.py b/src/debmagic/_build_driver/build.py index 0182c21..6568d3d 100644 --- a/src/debmagic/_build_driver/build.py +++ b/src/debmagic/_build_driver/build.py @@ -1,9 +1,15 @@ +import re +import shutil +from pathlib import Path + from debmagic._build_driver.driver_docker import BuildDriverDocker from debmagic._build_driver.driver_lxd import BuildDriverLxd from debmagic._build_driver.driver_none import BuildDriverNone from .common import BuildConfig, BuildDriver, BuildDriverType +DEBMAGIC_TEMP_BUILD_PARENT_DIR = Path("/tmp/debmagic") + def _create_driver(build_driver: BuildDriverType, config: BuildConfig) -> BuildDriver: match build_driver: @@ -15,15 +21,72 @@ def _create_driver(build_driver: BuildDriverType, config: BuildConfig) -> BuildD return BuildDriverNone.create(config=config) -def build(build_driver: BuildDriverType, config: BuildConfig): +def _ignore_patterns_from_gitignore(gitignore_path: Path): + if not gitignore_path.is_file(): + return None + + contents = gitignore_path.read_text().strip().splitlines() + relevant_lines = filter(lambda line: not re.match(r"\s*#.*", line) and line.strip(), contents) + return shutil.ignore_patterns(*relevant_lines) + + +def _prepare_build_env(source_dir: Path, output_dir: Path, dry_run: bool) -> BuildConfig: + package_name = "debmagic" # TODO + package_version = "0.1.0" # TODO + + package_identifier = f"{package_name}-{package_version}" + build_root = DEBMAGIC_TEMP_BUILD_PARENT_DIR / package_identifier + if build_root.exists(): + shutil.rmtree(build_root) + + config = BuildConfig( + package_identifier=package_identifier, + source_dir=source_dir, + output_dir=output_dir, + build_root_dir=build_root, + distro="debian", + distro_version="trixie", + dry_run=dry_run, + sign_package=False, + ) + + # prepare build environment, create the build directory structure, copy the sources + config.create_dirs() + source_ignore_pattern = _ignore_patterns_from_gitignore(source_dir / ".gitignore") + shutil.copytree(config.source_dir, config.build_source_dir, dirs_exist_ok=True, ignore=source_ignore_pattern) + + return config + + +def _copy_file_if_exists(source: Path, glob: str, dest: Path): + for file in source.glob(glob): + if file.is_dir(): + shutil.copytree(file, dest) + elif file.is_file(): + shutil.copy(file, dest) + else: + raise NotImplementedError("Don't support anything besides files and directories") + + +def build(build_driver: BuildDriverType, source_dir: Path, output_dir: Path, dry_run: bool = False): + config = _prepare_build_env(source_dir=source_dir, output_dir=output_dir, dry_run=dry_run) + driver = _create_driver(build_driver, config) try: - driver.run_command(["apt-get", "-y", "build-dep", "."], cwd=config.source_dir, requires_root=True) - driver.run_command(["debuild", "-nc", "-uc", "-b"], cwd=config.source_dir) + driver.run_command(["apt-get", "-y", "build-dep", "."], cwd=config.build_source_dir, requires_root=True) + driver.run_command(["dpkg-buildpackage", "-us", "-uc", "-ui", "-nc", "-b"], cwd=config.build_source_dir) + if config.sign_package: + pass + # SIGN .changes and .dsc files + # changes = *.changes / *.dsc + # driver.run_command(["debsign", opts, changes], cwd=config.source_dir) + # driver.run_command(["debrsign", opts, username, changes], cwd=config.source_dir) # TODO: copy packages to output directory - config.output_dir.mkdir(parents=True, exist_ok=True) - driver.copy_file(source_dir=config.source_dir / "..", glob="debmagic_*.deb", dest_dir=config.output_dir) + _copy_file_if_exists(source=config.build_source_dir / "..", glob="*.deb", dest=config.output_dir) + _copy_file_if_exists(source=config.build_source_dir / "..", glob="*.buildinfo", dest=config.output_dir) + _copy_file_if_exists(source=config.build_source_dir / "..", glob="*.changes", dest=config.output_dir) + _copy_file_if_exists(source=config.build_source_dir / "..", glob="*.dsc", dest=config.output_dir) except Exception as e: print(e) print( diff --git a/src/debmagic/_build_driver/common.py b/src/debmagic/_build_driver/common.py index ecd54b7..8fbd9f7 100644 --- a/src/debmagic/_build_driver/common.py +++ b/src/debmagic/_build_driver/common.py @@ -1,7 +1,7 @@ import abc from dataclasses import dataclass from pathlib import Path -from typing import Literal, Sequence +from typing import Literal, Self, Sequence BuildDriverType = Literal["docker"] | Literal["lxd"] | Literal["none"] SUPPORTED_BUILD_DRIVERS: list[BuildDriverType] = ["docker", "none"] @@ -13,27 +13,46 @@ class BuildError(RuntimeError): @dataclass class BuildConfig: + package_identifier: str source_dir: Path output_dir: Path dry_run: bool distro_version: str # e.g. trixie - distro: str = "debian" + distro: str # e.g. debian + sign_package: bool # TODO: figure out if this is the right place + + # build paths + build_root_dir: Path + + @property + def build_work_dir(self) -> Path: + return self.build_root_dir / "work" + + @property + def build_temp_dir(self) -> Path: + return self.build_root_dir / "temp" + + @property + def build_source_dir(self) -> Path: + return self.build_work_dir / self.package_identifier + + def create_dirs(self): + self.output_dir.mkdir(exist_ok=True, parents=True) + self.build_work_dir.mkdir(exist_ok=True, parents=True) + self.build_temp_dir.mkdir(exist_ok=True, parents=True) + self.build_source_dir.mkdir(exist_ok=True, parents=True) class BuildDriver: @classmethod @abc.abstractmethod - def create(cls, config: BuildConfig) -> "BuildDriver": + def create(cls, config: BuildConfig) -> Self: pass @abc.abstractmethod def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): pass - @abc.abstractmethod - def copy_file(self, source_dir: Path, glob: str, dest_dir: Path): - pass - @abc.abstractmethod def cleanup(self): pass diff --git a/src/debmagic/_build_driver/driver_docker.py b/src/debmagic/_build_driver/driver_docker.py index 5293099..7c2ae76 100644 --- a/src/debmagic/_build_driver/driver_docker.py +++ b/src/debmagic/_build_driver/driver_docker.py @@ -1,61 +1,43 @@ -import tempfile import uuid from pathlib import Path -from typing import Sequence +from typing import Self, Sequence from debmagic._build_driver.common import BuildConfig, BuildDriver, BuildError from debmagic._utils import run_cmd, run_cmd_in_foreground -WORKDIR_IN_CONTAINER = Path("/work/source") -OUTPUT_DIR_IN_CONTAINER = Path("/output") +BUILD_DIR_IN_CONTAINER = Path("/debmagic") DOCKERFILE_TEMPLATE = f""" FROM docker.io/{{distro}}:{{distro_version}} -RUN apt-get update && apt-get -y install devscripts +RUN apt-get update && apt-get -y install dpkg-dev -RUN mkdir -p {WORKDIR_IN_CONTAINER} {OUTPUT_DIR_IN_CONTAINER} -WORKDIR {WORKDIR_IN_CONTAINER} -COPY --from=source_dir . {WORKDIR_IN_CONTAINER} +RUN mkdir -p {BUILD_DIR_IN_CONTAINER} ENTRYPOINT ["sleep", "infinity"] """ class BuildDriverDocker(BuildDriver): - def __init__(self, workdir: tempfile.TemporaryDirectory, config: BuildConfig, container_name: str): - self._workdir = workdir - self._workdir_path = Path(workdir.name) + def __init__(self, config: BuildConfig, container_name: str): self._config = config self._container_name = container_name - def _translate_source_path(self, path_in_source: Path) -> Path: - if not path_in_source.is_relative_to(self._config.source_dir): + def _translate_path_in_container(self, path_in_source: Path) -> Path: + if not path_in_source.is_relative_to(self._config.build_root_dir): raise BuildError("Cannot run in a path not relative to the original source directory") - rel = path_in_source.relative_to(self._config.source_dir) - return WORKDIR_IN_CONTAINER / rel - - def _translate_output_path(self, path_in_output: Path) -> Path: - if not path_in_output.is_relative_to(self._config.output_dir): - raise BuildError("Cannot run in a path not relative to the original output directory") - rel = path_in_output.relative_to(self._config.output_dir) - return OUTPUT_DIR_IN_CONTAINER / rel + rel = path_in_source.relative_to(self._config.build_root_dir) + return BUILD_DIR_IN_CONTAINER / rel @classmethod - def create(cls, config: BuildConfig): - workdir = tempfile.TemporaryDirectory() - workdir_path = Path(workdir.name) - + def create(cls, config: BuildConfig) -> Self: formatted_dockerfile = DOCKERFILE_TEMPLATE.format( distro=config.distro, distro_version=config.distro_version, ) - dockerfile_path = workdir_path / "Dockerfile" + dockerfile_path = config.build_temp_dir / "Dockerfile" dockerfile_path.write_text(formatted_dockerfile) - gitignore_file = config.source_dir / ".gitignore" - if gitignore_file.is_file(): - (workdir_path / ".dockerignore").write_text(gitignore_file.read_text()) docker_image_name = str(uuid.uuid4()) ret = run_cmd( @@ -66,9 +48,7 @@ def create(cls, config: BuildConfig): docker_image_name, "-f", dockerfile_path, - "--build-context", - f"source_dir={config.source_dir}", - workdir_path, + config.build_temp_dir, ], dry_run=config.dry_run, check=False, @@ -85,7 +65,7 @@ def create(cls, config: BuildConfig): "--name", docker_container_name, "--mount", - f"type=bind,src={config.output_dir},dst={OUTPUT_DIR_IN_CONTAINER}", + f"type=bind,src={config.build_root_dir},dst={BUILD_DIR_IN_CONTAINER}", docker_image_name, ], dry_run=config.dry_run, @@ -94,27 +74,24 @@ def create(cls, config: BuildConfig): if ret.returncode != 0: raise BuildError("Error creating docker image for build") - instance = cls(config=config, workdir=workdir, container_name=docker_container_name) + instance = cls(config=config, container_name=docker_container_name) return instance def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): del requires_root # we assume to always be root in the container if cwd: - cwd = self._translate_source_path(cwd) + cwd = self._translate_path_in_container(cwd) + cwd_args: list[str | Path] = ["--workdir", cwd] + else: + cwd_args = [] - ret = run_cmd(["docker", "exec", self._container_name, *args], dry_run=self._config.dry_run) + ret = run_cmd(["docker", "exec", *cwd_args, self._container_name, *args], dry_run=self._config.dry_run) if ret.returncode != 0: raise BuildError("Error building package") - def copy_file(self, source_dir: Path, glob: str, dest_dir: Path): - translated_source = self._translate_source_path(source_dir) - translated_output = self._translate_output_path(dest_dir) - self.run_command(["/usr/bin/env", "bash", "-c", f"cp -f {translated_source}/{glob} {translated_output}"]) - def cleanup(self): run_cmd(["docker", "rm", "-f", self._container_name], dry_run=self._config.dry_run) - self._workdir.cleanup() def drop_into_shell(self): if not self._config.dry_run: diff --git a/src/debmagic/_build_driver/driver_lxd.py b/src/debmagic/_build_driver/driver_lxd.py index 8bf3a2b..a44baf1 100644 --- a/src/debmagic/_build_driver/driver_lxd.py +++ b/src/debmagic/_build_driver/driver_lxd.py @@ -12,9 +12,6 @@ def create(cls, config: BuildConfig): def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): raise NotImplementedError() - def copy_file(self, source_dir: Path, glob: str, dest_dir: Path): - raise NotImplementedError() - def cleanup(self): raise NotImplementedError() diff --git a/src/debmagic/_build_driver/driver_none.py b/src/debmagic/_build_driver/driver_none.py index eee8ccf..00e40cc 100644 --- a/src/debmagic/_build_driver/driver_none.py +++ b/src/debmagic/_build_driver/driver_none.py @@ -1,5 +1,4 @@ import os -import shutil from pathlib import Path from typing import Sequence @@ -20,10 +19,6 @@ def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requi args = ["sudo", *args] run_cmd(args=args, dry_run=self._config.dry_run, cwd=cwd) - def copy_file(self, source_dir: Path, glob: str, dest_dir: Path): - for file in source_dir.glob(glob): - shutil.copy(file, dest_dir) - def cleanup(self): pass diff --git a/src/debmagic/cli.py b/src/debmagic/cli.py index 5343eef..0a45d23 100644 --- a/src/debmagic/cli.py +++ b/src/debmagic/cli.py @@ -2,7 +2,7 @@ from pathlib import Path from debmagic._build_driver.build import build as build_driver_build -from debmagic._build_driver.common import SUPPORTED_BUILD_DRIVERS, BuildConfig +from debmagic._build_driver.common import SUPPORTED_BUILD_DRIVERS from debmagic._utils import run_cmd from debmagic._version import VERSION @@ -24,10 +24,16 @@ def _parse_args(): build_cli = sp.add_parser( "build", parents=[common_cli], help="Build a debian package with the selected containerization driver" ) - build_cli.add_argument("--driver", choices=SUPPORTED_BUILD_DRIVERS, default="none") + build_cli.add_argument("--driver", choices=SUPPORTED_BUILD_DRIVERS, required=True) build_cli.add_argument("-s", "--source-dir", type=Path, default=Path.cwd()) build_cli.add_argument("-o", "--output-dir", type=Path, default=Path.cwd()) + sp.add_parser("check", parents=[common_cli], help="Run linters (e.g. lintian)") + + sp.add_parser("shell", parents=[common_cli], help="Attach a shell to a running debmagic build") + + sp.add_parser("test", parents=[common_cli], help="Run package tests") + return cli, cli.parse_args() @@ -42,13 +48,8 @@ def main(): print(f"{cli.prog} {VERSION}") cli.exit(0) case "build": - build_config = BuildConfig( - output_dir=args.output_dir, - source_dir=args.source_dir, - distro="debian", - distro_version="trixie", - dry_run=args.dry_run, + build_driver_build( + build_driver=args.driver, source_dir=args.source_dir, output_dir=args.output_dir, dry_run=args.dry_run ) - build_driver_build(build_driver=args.driver, config=build_config) case "debuild": run_cmd(["debuild", "-nc", "-uc", "-b"]) From 08ead934ff6d2d0491aa23071dba5da878232633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Mon, 24 Nov 2025 22:25:50 +0000 Subject: [PATCH 8/8] chore: linter / formatter fixes after rebase --- pyproject.toml | 12 ++++++++++-- src/debmagic/_build_driver/build.py | 3 ++- src/debmagic/_build_driver/common.py | 2 +- src/debmagic/_build_driver/driver_docker.py | 4 ++-- src/debmagic/_build_driver/driver_lxd.py | 2 +- src/debmagic/_build_driver/driver_none.py | 6 +++--- src/debmagic/_module/dh.py | 2 +- 7 files changed, 20 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 327f6e6..0606418 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,11 +32,19 @@ exclude_gitignore = true [tool.ruff] line-length = 120 target-version = "py312" -extend-exclude = [".idea", ".mypy_cache", ".venv*", "docs", "debian", "__pycache__", "*.egg_info"] +extend-exclude = [ + ".idea", + ".mypy_cache", + ".venv*", + "docs", + "debian", + "__pycache__", + "*.egg_info", +] [tool.ruff.lint] select = ["E", "W", "F", "I", "C", "N", "PL", "RUF", "I001"] -ignore = ["E722", "PLR2004", "PLR0912", "PLR5501", "PLC0415"] +ignore = ["E722", "PLR2004", "PLR0912", "PLR5501", "PLC0415", "PLR0911"] mccabe.max-complexity = 25 pylint.max-args = 10 diff --git a/src/debmagic/_build_driver/build.py b/src/debmagic/_build_driver/build.py index 6568d3d..5126893 100644 --- a/src/debmagic/_build_driver/build.py +++ b/src/debmagic/_build_driver/build.py @@ -90,7 +90,8 @@ def build(build_driver: BuildDriverType, source_dir: Path, output_dir: Path, dry except Exception as e: print(e) print( - "Something failed during building - dropping into interactive shell in build environment for easier debugging" + "Something failed during building -" + " dropping into interactive shell in build environment for easier debugging" ) driver.drop_into_shell() raise e diff --git a/src/debmagic/_build_driver/common.py b/src/debmagic/_build_driver/common.py index 8fbd9f7..0ae4c8d 100644 --- a/src/debmagic/_build_driver/common.py +++ b/src/debmagic/_build_driver/common.py @@ -50,7 +50,7 @@ def create(cls, config: BuildConfig) -> Self: pass @abc.abstractmethod - def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): + def run_command(self, cmd: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): pass @abc.abstractmethod diff --git a/src/debmagic/_build_driver/driver_docker.py b/src/debmagic/_build_driver/driver_docker.py index 7c2ae76..cb9d504 100644 --- a/src/debmagic/_build_driver/driver_docker.py +++ b/src/debmagic/_build_driver/driver_docker.py @@ -77,7 +77,7 @@ def create(cls, config: BuildConfig) -> Self: instance = cls(config=config, container_name=docker_container_name) return instance - def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): + def run_command(self, cmd: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): del requires_root # we assume to always be root in the container if cwd: @@ -86,7 +86,7 @@ def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requi else: cwd_args = [] - ret = run_cmd(["docker", "exec", *cwd_args, self._container_name, *args], dry_run=self._config.dry_run) + ret = run_cmd(["docker", "exec", *cwd_args, self._container_name, *cmd], dry_run=self._config.dry_run) if ret.returncode != 0: raise BuildError("Error building package") diff --git a/src/debmagic/_build_driver/driver_lxd.py b/src/debmagic/_build_driver/driver_lxd.py index a44baf1..44aebef 100644 --- a/src/debmagic/_build_driver/driver_lxd.py +++ b/src/debmagic/_build_driver/driver_lxd.py @@ -9,7 +9,7 @@ class BuildDriverLxd(BuildDriver): def create(cls, config: BuildConfig): return cls() - def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): + def run_command(self, cmd: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): raise NotImplementedError() def cleanup(self): diff --git a/src/debmagic/_build_driver/driver_none.py b/src/debmagic/_build_driver/driver_none.py index 00e40cc..a4f62d0 100644 --- a/src/debmagic/_build_driver/driver_none.py +++ b/src/debmagic/_build_driver/driver_none.py @@ -14,10 +14,10 @@ def __init__(self, config: BuildConfig) -> None: def create(cls, config: BuildConfig): return cls(config=config) - def run_command(self, args: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): + def run_command(self, cmd: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False): if requires_root and not os.getuid() == 0: - args = ["sudo", *args] - run_cmd(args=args, dry_run=self._config.dry_run, cwd=cwd) + cmd = ["sudo", *cmd] + run_cmd(cmd=cmd, dry_run=self._config.dry_run, cwd=cwd) def cleanup(self): pass diff --git a/src/debmagic/_module/dh.py b/src/debmagic/_module/dh.py index f479da5..69718f7 100644 --- a/src/debmagic/_module/dh.py +++ b/src/debmagic/_module/dh.py @@ -41,7 +41,7 @@ def __init__(self, dh_args: list[str] | str | None = None): else: self._dh_args = dh_args - self._overrides: dict[str, DHOverride] = dict() + self._overrides: dict[str, DHOverride] = {} self._initialized = False # debmagic's stages, with matching commands from the dh sequence