Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,20 @@ 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: Run Debmagic build on ourself
run: uv run debmagic build --driver=docker
10 changes: 8 additions & 2 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ Uploaders:
Jonas Jelten <jj@sft.lol>,
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
Expand All @@ -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.
43 changes: 43 additions & 0 deletions debian/debmagic.1
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 8 additions & 3 deletions debian/rules
Original file line number Diff line number Diff line change
Expand Up @@ -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()
15 changes: 12 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -31,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

Expand Down
4 changes: 4 additions & 0 deletions src/debmagic/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .cli import main

if __name__ == "__main__":
main()
Empty file.
99 changes: 99 additions & 0 deletions src/debmagic/_build_driver/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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:
case "docker":
return BuildDriverDocker.create(config=config)
case "lxd":
return BuildDriverLxd.create(config=config)
case "none":
return BuildDriverNone.create(config=config)


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.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
_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(
"Something failed during building -"
" dropping into interactive shell in build environment for easier debugging"
)
driver.drop_into_shell()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a way to get into the build (and stop it at/after/... arbitrary stages) directly would be very handy

raise e
finally:
driver.cleanup()
62 changes: 62 additions & 0 deletions src/debmagic/_build_driver/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import abc
from dataclasses import dataclass
from pathlib import Path
from typing import Literal, Self, Sequence

BuildDriverType = Literal["docker"] | Literal["lxd"] | Literal["none"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not enum?

SUPPORTED_BUILD_DRIVERS: list[BuildDriverType] = ["docker", "none"]


class BuildError(RuntimeError):
pass


@dataclass
class BuildConfig:
package_identifier: str
source_dir: Path
output_dir: Path
dry_run: bool
distro_version: str # e.g. trixie
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) -> Self:
pass

@abc.abstractmethod
def run_command(self, cmd: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False):
pass

@abc.abstractmethod
def cleanup(self):
pass

@abc.abstractmethod
def drop_into_shell(self):
pass
Loading