Skip to content

Commit b81a64c

Browse files
authored
Merge pull request #11 from SFTtech/milo/config-loading
feat(debmagic): add config file parsing with overrides from cli args
2 parents e43fbc9 + b85d944 commit b81a64c

File tree

16 files changed

+234
-95
lines changed

16 files changed

+234
-95
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ jobs:
108108
# matrix:
109109
# python-version:
110110
# - "3.12"
111-
# - "3.13"
112111
# os: [ubuntu-latest]
113112
# steps:
114113
# - uses: actions/checkout@v5

debian/control

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Architecture: all
4646
Depends:
4747
python3-debmagic-common,
4848
python3-pydantic,
49+
python3-pydantic-settings,
4950
${misc:Depends},
5051
${python3:Depends}
5152
Multi-Arch: foreign

packages/debmagic/pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ license = "GPL-2.0-or-later"
77
# readme = "../../README.md"
88
requires-python = ">=3.12"
99
classifiers = ["Programming Language :: Python :: 3"]
10-
dependencies = ["debmagic-common", "pydantic>=2,<3"]
10+
dependencies = [
11+
"debmagic-common",
12+
"pydantic>=2,<3",
13+
"pydantic-settings>=2.12.0",
14+
]
1115

1216
[project.urls]
1317
homepage = "https://github.com/SFTtech/debmagic"

packages/debmagic/src/debmagic/cli/_build_driver/build.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@
66

77
from .._config import DebmagicConfig
88
from .common import BuildConfig, BuildDriver, BuildDriverType, BuildMetadata, PackageDescription
9+
from .config import BuildDriverConfig
910
from .driver_bare import BuildDriverBare
1011
from .driver_docker import BuildDriverDocker
1112
from .driver_lxd import BuildDriverLxd
1213

1314

14-
def _create_driver(build_driver: BuildDriverType, config: BuildConfig, additional_args: list[str]) -> BuildDriver:
15+
def _create_driver(build_driver: BuildDriverType, build_config: BuildConfig, config: BuildDriverConfig) -> BuildDriver:
1516
match build_driver:
1617
case "docker":
17-
return BuildDriverDocker.create(config=config, additional_args=additional_args)
18+
return BuildDriverDocker.create(config=build_config, driver_config=config.docker)
1819
case "lxd":
19-
return BuildDriverLxd.create(config=config, additional_args=additional_args)
20+
return BuildDriverLxd.create(config=build_config, driver_config=config.lxd)
2021
case "bare":
21-
return BuildDriverBare.create(config=config, additional_args=additional_args)
22+
return BuildDriverBare.create(config=build_config, driver_config=config.bare)
2223

2324

2425
def _driver_from_build_root(build_root: Path):
@@ -68,9 +69,7 @@ def _get_package_build_root_and_identifier(config: DebmagicConfig, package: Pack
6869
return package_identifier, build_root
6970

7071

71-
def _prepare_build_env(
72-
config: DebmagicConfig, package: PackageDescription, output_dir: Path, dry_run: bool
73-
) -> BuildConfig:
72+
def _prepare_build_env(config: DebmagicConfig, package: PackageDescription, output_dir: Path) -> BuildConfig:
7473
package_identifier, build_root = _get_package_build_root_and_identifier(config, package)
7574
if build_root.exists():
7675
shutil.rmtree(build_root)
@@ -82,7 +81,7 @@ def _prepare_build_env(
8281
build_root_dir=build_root,
8382
distro="debian",
8483
distro_version="trixie",
85-
dry_run=dry_run,
84+
dry_run=config.dry_run,
8685
sign_package=False,
8786
)
8887

@@ -107,12 +106,10 @@ def build(
107106
package: PackageDescription,
108107
build_driver: BuildDriverType,
109108
output_dir: Path,
110-
additional_args: list[str],
111-
dry_run: bool = False,
112109
):
113-
build_config = _prepare_build_env(config=config, package=package, output_dir=output_dir, dry_run=dry_run)
110+
build_config = _prepare_build_env(config=config, package=package, output_dir=output_dir)
114111

115-
driver = _create_driver(build_driver, build_config, additional_args)
112+
driver = _create_driver(build_driver, build_config, config.driver_config)
116113
_write_build_metadata(build_config, driver)
117114
try:
118115
driver.run_command(["apt-get", "-y", "build-dep", "."], cwd=build_config.build_source_dir, requires_root=True)

packages/debmagic/src/debmagic/cli/_build_driver/common.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import abc
22
from dataclasses import dataclass
33
from pathlib import Path
4-
from typing import Literal, Self, Sequence
4+
from typing import Literal, Self, Sequence, TypeVar
55

66
from pydantic import BaseModel
77

@@ -67,10 +67,13 @@ def create_dirs(self):
6767
self.build_source_dir.mkdir(exist_ok=True, parents=True)
6868

6969

70-
class BuildDriver:
70+
ConfigT = TypeVar("ConfigT")
71+
72+
73+
class BuildDriver[ConfigT]:
7174
@classmethod
7275
@abc.abstractmethod
73-
def create(cls, config: BuildConfig, additional_args: list[str]) -> Self:
76+
def create(cls, config: BuildConfig, driver_config: ConfigT) -> Self:
7477
pass
7578

7679
@classmethod
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from pydantic import BaseModel
2+
3+
from .driver_bare import BareDriverConfig
4+
from .driver_docker import DockerDriverConfig
5+
from .driver_lxd import LxdDriverConfig
6+
7+
8+
class BuildDriverConfig(BaseModel):
9+
bare: BareDriverConfig = BareDriverConfig()
10+
docker: DockerDriverConfig = DockerDriverConfig()
11+
lxd: LxdDriverConfig = LxdDriverConfig()

packages/debmagic/src/debmagic/cli/_build_driver/driver_bare.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@
33
from typing import Sequence
44

55
from debmagic.common.utils import run_cmd, run_cmd_in_foreground
6+
from pydantic import BaseModel
67

78
from .common import BuildConfig, BuildDriver
89

910

10-
class BuildDriverBare(BuildDriver):
11+
class BareDriverConfig(BaseModel):
12+
pass
13+
14+
15+
class BuildDriverBare(BuildDriver[BareDriverConfig]):
1116
def __init__(self, config: BuildConfig) -> None:
1217
self._config = config
1318

1419
@classmethod
15-
def create(cls, config: BuildConfig, additional_args: list[str]):
20+
def create(cls, config: BuildConfig, driver_config: BareDriverConfig):
1621
return cls(config=config)
1722

1823
def run_command(self, cmd: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False):

packages/debmagic/src/debmagic/cli/_build_driver/driver_docker.py

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import argparse
1+
import os
22
import uuid
33
from pathlib import Path
44
from typing import Self, Sequence
55

66
from debmagic.common.utils import run_cmd, run_cmd_in_foreground
7-
from pydantic import BaseModel
7+
from pydantic import BaseModel, Field
88

99
from .common import (
1010
BuildConfig,
@@ -17,21 +17,42 @@
1717

1818
BUILD_DIR_IN_CONTAINER = Path("/debmagic")
1919

20+
DOCKER_USER = "user"
21+
2022
DOCKERFILE_TEMPLATE = f"""
2123
FROM {{base_image}}
2224
23-
RUN apt-get update && apt-get -y install dpkg-dev python3
25+
ARG USERNAME={DOCKER_USER}
26+
ARG USER_UID=1000
27+
ARG USER_GID=$USER_UID
28+
29+
RUN apt-get update && apt-get install -y sudo dpkg-dev python3
30+
31+
RUN groupadd --gid $USER_GID $USERNAME \
32+
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
33+
&& echo $USERNAME ALL=\\(root\\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
34+
&& chmod 0440 /etc/sudoers.d/$USERNAME
2435
2536
RUN mkdir -p {BUILD_DIR_IN_CONTAINER}
37+
RUN chown $USERNAME:$USERNAME {BUILD_DIR_IN_CONTAINER}
38+
USER $USERNAME
2639
ENTRYPOINT ["sleep", "infinity"]
2740
"""
2841

2942

43+
class DockerDriverConfig(BaseModel):
44+
base_image: str | None = Field(
45+
default=None,
46+
description="Can be used to override the base image which the docker driver "
47+
"uses to create the build environment",
48+
)
49+
50+
3051
class DockerDriverBuildMetadata(BaseModel):
3152
container_name: str
3253

3354

34-
class BuildDriverDocker(BuildDriver):
55+
class BuildDriverDocker(BuildDriver[DockerDriverConfig]):
3556
def __init__(self, build_root: Path, dry_run: bool, container_name: str):
3657
self._build_root = build_root
3758
self._dry_run = dry_run
@@ -44,30 +65,28 @@ def _translate_path_in_container(self, path_in_source: Path) -> Path:
4465
rel = path_in_source.relative_to(self._build_root)
4566
return BUILD_DIR_IN_CONTAINER / rel
4667

47-
@staticmethod
48-
def _parse_args(args: list[str]) -> argparse.Namespace:
49-
parser = argparse.ArgumentParser()
50-
parser.add_argument("--docker-image", type=str)
51-
return parser.parse_args(args)
52-
5368
@classmethod
54-
def create(cls, config: BuildConfig, additional_args: list[str]) -> Self:
55-
args = cls._parse_args(additional_args)
56-
57-
base_image = f"docker.io/{config.distro}:{config.distro_version}"
58-
if args.docker_image is not None:
59-
base_image = args.docker_image
69+
def create(cls, config: BuildConfig, driver_config: DockerDriverConfig) -> Self:
70+
base_image = driver_config.base_image or f"docker.io/{config.distro}:{config.distro_version}"
6071

6172
formatted_dockerfile = DOCKERFILE_TEMPLATE.format(base_image=base_image)
6273

6374
dockerfile_path = config.build_temp_dir / "Dockerfile"
6475
dockerfile_path.write_text(formatted_dockerfile)
6576

6677
docker_image_name = f"debmagic-{config.build_identifier}"
78+
79+
additional_args = []
80+
if not os.getuid() == 0:
81+
# to reduce potential permission problems with missing user remappings on some systems
82+
# we simply create the build user inside the docker container with the same uid / gid as our host user
83+
additional_args.extend(["--build-arg", f"USER_UID={os.getuid()}", "--build-arg", f"USER_GID={os.getgid()}"])
84+
6785
ret = run_cmd(
6886
[
6987
"docker",
7088
"build",
89+
*additional_args,
7190
"--tag",
7291
docker_image_name,
7392
"-f",
@@ -112,15 +131,16 @@ def get_build_metadata(self) -> DriverSpecificBuildMetadata:
112131
return meta.model_dump()
113132

114133
def run_command(self, cmd: Sequence[str | Path], cwd: Path | None = None, requires_root: bool = False):
115-
del requires_root # we assume to always be root in the container
134+
conditional_args: list[str | Path] = []
116135

117136
if cwd:
118137
cwd = self._translate_path_in_container(cwd)
119-
cwd_args: list[str | Path] = ["--workdir", cwd]
120-
else:
121-
cwd_args = []
138+
conditional_args.extend(["--workdir", cwd])
139+
140+
if requires_root:
141+
conditional_args.extend(["--user", "root"])
122142

123-
ret = run_cmd(["docker", "exec", *cwd_args, self._container_name, *cmd], dry_run=self._dry_run)
143+
ret = run_cmd(["docker", "exec", *conditional_args, self._container_name, *cmd], dry_run=self._dry_run)
124144
if ret.returncode != 0:
125145
raise BuildError("Error building package")
126146

packages/debmagic/src/debmagic/cli/_build_driver/driver_lxd.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
from pathlib import Path
22
from typing import Self, Sequence
33

4+
from pydantic import BaseModel
5+
46
from .common import BuildConfig, BuildDriver
57

68

7-
class BuildDriverLxd(BuildDriver):
9+
class LxdDriverConfig(BaseModel):
10+
pass
11+
12+
13+
class BuildDriverLxd(BuildDriver[LxdDriverConfig]):
814
@classmethod
9-
def create(cls, config: BuildConfig, additional_args: list[str]) -> Self:
15+
def create(cls, config: BuildConfig, driver_config: LxdDriverConfig) -> Self:
1016
raise NotImplementedError()
1117

1218
@classmethod
Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,63 @@
1-
import tomllib
1+
import argparse
22
from pathlib import Path
3-
from typing import Sequence
3+
from typing import Annotated, ClassVar
44

5-
from pydantic import BaseModel
5+
from pydantic import AfterValidator, Field
6+
from pydantic_settings import (
7+
BaseSettings,
8+
CliApp,
9+
CliSettingsSource,
10+
PydanticBaseSettingsSource,
11+
TomlConfigSettingsSource,
12+
)
613

14+
from ._build_driver.config import BuildDriverConfig
715

8-
class DebmagicConfig(BaseModel):
9-
# driver: BuildDriverType | None = None
10-
temp_build_dir: Path = Path("/tmp/debmagic")
1116

17+
def resolve_path(p: Path) -> Path:
18+
return p.resolve()
1219

13-
def merge_configs(configs: Sequence[DebmagicConfig]) -> DebmagicConfig:
14-
return configs[-1]
1520

21+
class DebmagicConfig(BaseSettings, cli_kebab_case=True, cli_avoid_json=True):
22+
_config_file_paths: ClassVar[list[Path]] = []
1623

17-
def load_config(config_path: Path) -> DebmagicConfig:
18-
with config_path.open("rb") as f:
19-
content = tomllib.load(f)
20-
return DebmagicConfig.model_validate(content)
24+
driver_config: BuildDriverConfig = BuildDriverConfig()
25+
26+
temp_build_dir: Annotated[
27+
Path,
28+
Field(description="Temporary directory on the local machine used as root directory for all package builds"),
29+
AfterValidator(resolve_path),
30+
] = Path("/tmp/debmagic")
31+
32+
dry_run: Annotated[bool, Field(description="don't actually run anything that changes the system/package state")] = (
33+
False
34+
)
35+
36+
@classmethod
37+
def settings_customise_sources(
38+
cls,
39+
settings_cls: type[BaseSettings],
40+
init_settings: PydanticBaseSettingsSource,
41+
env_settings: PydanticBaseSettingsSource,
42+
dotenv_settings: PydanticBaseSettingsSource,
43+
file_secret_settings: PydanticBaseSettingsSource,
44+
) -> tuple[PydanticBaseSettingsSource, ...]:
45+
# FIXME: once pydantic properly supports dynamic runtime paths for these kinds of use cases
46+
# use a more sane method of injecting additional paths to load
47+
# see https://github.com/pydantic/pydantic-settings/issues/259
48+
return tuple(TomlConfigSettingsSource(settings_cls, p) for p in cls._config_file_paths)
49+
50+
51+
def get_config_argparser() -> argparse.ArgumentParser:
52+
parser = argparse.ArgumentParser(add_help=False)
53+
CliSettingsSource(DebmagicConfig, root_parser=parser)
54+
return parser
55+
56+
57+
def load_config(args: argparse.Namespace, config_paths: list[Path]) -> DebmagicConfig:
58+
DebmagicConfig._config_file_paths = config_paths
59+
# parser = argparse.ArgumentParser(add_help=False)
60+
source = CliSettingsSource(DebmagicConfig)
61+
config = CliApp.run(DebmagicConfig, cli_args=args, cli_settings_source=source)
62+
63+
return config

0 commit comments

Comments
 (0)