Skip to content
Merged
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
45 changes: 23 additions & 22 deletions src/debmagic/_dpkg/buildflags.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import json
import os
import re
import shlex
import subprocess
from pathlib import Path

from .._package_version import PackageVersion
from .._utils import run_cmd

def _run_output(cmd: str, input: str | None = None, env: dict[str, str] | None = None, cwd: Path | None = None) -> str:
input_data = None
if input:
input_data = input.encode()
return subprocess.check_output(shlex.split(cmd), input=input_data, env=env).strip().decode()

def _cmd(cmd: str, input_data: str | None = None, env: dict[str, str] | None = None, cwd: Path | None = None) -> str:
return run_cmd(cmd, check=True, env=env, input=input_data, text=True, capture_output=True).stdout.strip()

def get_flags(package_dir: Path, maint_options: str | None = None) -> dict[str, str]:

def get_pkg_env(package_dir: Path, maint_options: str | None = None) -> tuple[dict[str, str], PackageVersion]:
"""
does what including "/usr/share/dpkg/buildflags.mk" would do.
"""
Expand All @@ -24,7 +22,7 @@ def get_flags(package_dir: Path, maint_options: str | None = None) -> dict[str,
# TODO more vars as parameters, e.g. DEB_CFLAGS_MAINT_APPEND

# get build flags
flags_raw = _run_output("dpkg-buildflags", env=result)
flags_raw = _cmd("dpkg-buildflags", env=result)
for flag_line in flags_raw.splitlines():
flag_name, _, flag_value = flag_line.partition("=")
result[flag_name] = flag_value
Expand All @@ -37,29 +35,32 @@ def get_flags(package_dir: Path, maint_options: str | None = None) -> dict[str,

# architecture.mk
if result.get("DEB_HOST_ARCH") is None:
arch_flags_raw = _run_output("dpkg-architecture", env=result)
arch_flags_raw = _cmd("dpkg-architecture", env=result)
for arch_flag_line in arch_flags_raw.splitlines():
arch_flag_name, _, arch_flag_value = arch_flag_line.partition("=")
result[arch_flag_name] = arch_flag_value

# pkg-info.mk
if result.get("DEB_SOURCE") is None or result.get("DEB_VERSION") is None:
result["DEB_SOURCE"] = _run_output("dpkg-parsechangelog -SSource", env=result, cwd=package_dir)
result["DEB_VERSION"] = _run_output("dpkg-parsechangelog -SVersion", env=result, cwd=package_dir)
result["DEB_SOURCE"] = _cmd("dpkg-parsechangelog -SSource", env=result, cwd=package_dir)
result["DEB_VERSION"] = _cmd("dpkg-parsechangelog -SVersion", env=result, cwd=package_dir)
version = PackageVersion.from_str(result["DEB_VERSION"])

# this would return DEB_VERSION in pkg-info.mk if no epoch is in version.
# instead, we return "0" as oritinally intended if no epoch is in version.
result["DEB_VERSION_EPOCH"] = (
"0" if ":" not in result["DEB_VERSION"] else re.sub(r"^([0-9]+):.*$", r"\1", result["DEB_VERSION"])
)
result["DEB_VERSION_EPOCH_UPSTREAM"] = re.sub(r"^(.*?)(-.*)?$", r"\1", result["DEB_VERSION"])
result["DEB_VERSION_UPSTREAM_REVISION"] = re.sub(r"^([0-9]*:)?(.*?)$", r"\2", result["DEB_VERSION"])
result["DEB_VERSION_UPSTREAM"] = re.sub(r"^([0-9]*:)(.*?)", r"\2", result["DEB_VERSION_EPOCH_UPSTREAM"])
result["DEB_VERSION_REVISION"] = re.sub(r"^.*?-([^-]*)$", r"\1", result["DEB_VERSION"])
result["DEB_DISTRIBUTION"] = _run_output("dpkg-parsechangelog -SDistribution", env=result, cwd=package_dir)
result["DEB_TIMESTAMP"] = _run_output("dpkg-parsechangelog -STimestamp", env=result, cwd=package_dir)
result["DEB_VERSION_EPOCH"] = version.epoch
result["DEB_VERSION_EPOCH_UPSTREAM"] = version.epoch_upstream
result["DEB_VERSION_UPSTREAM_REVISION"] = version.upstream_revision
result["DEB_VERSION_UPSTREAM"] = version.upstream
result["DEB_VERSION_REVISION"] = version.revision

result["DEB_DISTRIBUTION"] = _cmd("dpkg-parsechangelog -SDistribution", env=result, cwd=package_dir)
result["DEB_TIMESTAMP"] = _cmd("dpkg-parsechangelog -STimestamp", env=result, cwd=package_dir)

if result.get("SOURCE_DATE_EPOCH") is None:
result["SOURCE_DATE_EPOCH"] = result["DEB_TIMESTAMP"]
else:
version = PackageVersion.from_str(result["DEB_VERSION"])

if result.get("ELF_PACKAGE_METADATA") is None:
elf_meta = {
Expand All @@ -74,4 +75,4 @@ def get_flags(package_dir: Path, maint_options: str | None = None) -> dict[str,

result["ELF_PACKAGE_METADATA"] = json.dumps(elf_meta, separators=(",", ":"))

return result
return result, version
5 changes: 4 additions & 1 deletion src/debmagic/_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ._build_stage import BuildStage
from ._build_step import BuildStep
from ._dpkg import buildflags
from ._package_version import PackageVersion
from ._preset import Preset, PresetsT, as_presets
from ._rules_file import RulesFile, find_rules_file
from ._types import CustomFuncArg, CustomFuncArgsT
Expand Down Expand Up @@ -118,6 +119,7 @@ class SourcePackage:
rules_file: RulesFile
presets: list[Preset]
binary_packages: list[BinaryPackage]
version: PackageVersion
buildflags: Namespace
stage_functions: dict[BuildStage, BuildStep] = field(default_factory=dict)
custom_functions: dict[str, CustomFunction] = field(default_factory=dict)
Expand Down Expand Up @@ -261,7 +263,7 @@ def package(
presets.append(DefaultPreset())

# set buildflags as environment variables
flags = buildflags.get_flags(rules_file.package_dir, maint_options=maint_options)
flags, version = buildflags.get_pkg_env(rules_file.package_dir, maint_options=maint_options)
os.environ.update(flags)

src_pkg: SourcePackage | None = None
Expand All @@ -281,6 +283,7 @@ def package(
rules_file,
presets,
bin_pkgs,
version,
buildflags=Namespace(**flags),
)

Expand Down
61 changes: 61 additions & 0 deletions src/debmagic/_package_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import re
from dataclasses import dataclass
from typing import Self


@dataclass
class PackageVersion:
"""
debian version string, split into various subparts.
"""

#: distro packaging override base version (default is 0)
epoch: str

#: upstream package version
upstream: str

#: packaging (linux distro) revision
revision: str

@property
def version(self) -> str:
ret = ""
if self.epoch != "0":
ret += f"{self.epoch}:"
ret += self.upstream
if self.revision:
ret += f"-{self.revision}"
return ret

@property
def epoch_upstream(self) -> str:
"""
distro epoch plus upstream version
"""
if self.epoch:
return f"{self.epoch}:{self.upstream}"
else:
return self.upstream

@property
def upstream_revision(self) -> str:
"""
upstream version including distro package revision
"""
if self.revision:
return f"{self.upstream}-{self.revision}"
else:
return self.upstream

@classmethod
def from_str(cls, version: str) -> Self:
epoch_upstream = re.sub(r"^(.*?)(-[^-]*)?$", r"\1", version)
return cls(
# epoch = distro packaging override base version (default is 0)
# pkg-info.mk uses the full version if no epoch is in it.
# instead, we return "0" as oritinally intended if no epoch is in version.
epoch="0" if ":" not in version else re.sub(r"^([0-9]+):.*$", r"\1", version),
upstream=re.sub(r"^([0-9]*:)?(.*?)$", r"\2", epoch_upstream),
revision=re.sub(r"^.*?(-([^-]*))?$", r"\2", version),
)
5 changes: 1 addition & 4 deletions src/debmagic/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,7 @@ def run_cmd(
if dry_run:
return subprocess.CompletedProcess(cmd_args, 0)

ret = subprocess.run(cmd_args, check=False, **kwargs)

if check and ret.returncode != 0:
raise RuntimeError(f"failed to execute {cmd_pretty}")
ret = subprocess.run(cmd_args, check=check, **kwargs)

return ret

Expand Down
45 changes: 45 additions & 0 deletions tests/unit/test_exec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import subprocess
from unittest.mock import patch

from debmagic import _utils


@patch("subprocess.run", autospec=True)
def test_util_exec_str(mock_run):
mock_run.return_value = subprocess.CompletedProcess(["some", "command"], returncode=0)
_utils.run_cmd("some command")
mock_run.assert_called_once_with(["some", "command"], check=True)


@patch("subprocess.run", autospec=True)
def test_util_exec_list(mock_run):
mock_run.return_value = subprocess.CompletedProcess(["some", "command"], returncode=0)
_utils.run_cmd(["some", "command"])
mock_run.assert_called_once_with(["some", "command"], check=True)


@patch("subprocess.run", autospec=True)
def test_util_exec_str_dryrun(mock_run):
_utils.run_cmd("some command", dry_run=True)
mock_run.assert_not_called()


@patch("subprocess.run", autospec=True)
def test_util_exec_str_shlex(mock_run):
mock_run.return_value = subprocess.CompletedProcess(["nothing"], returncode=0)
_utils.run_cmd("some command 'some arg'")
mock_run.assert_called_once_with(["some", "command", "some arg"], check=True)


@patch("subprocess.run", autospec=True)
def test_util_exec_str_check(mock_run):
mock_run.return_value = subprocess.CompletedProcess(["something"], returncode=0)
_utils.run_cmd("something", check=True)
mock_run.assert_called_once_with(["something"], check=True)


@patch("subprocess.run", autospec=True)
def test_util_exec_str_nocheck(mock_run):
mock_run.return_value = subprocess.CompletedProcess(["something"], returncode=0)
_utils.run_cmd("something", check=False)
mock_run.assert_called_once_with(["something"], check=False)
25 changes: 25 additions & 0 deletions tests/unit/test_versioning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import pytest

from debmagic._package_version import PackageVersion


@pytest.mark.parametrize(
"version, expected",
[
(
"1.2.3a.4-42.2-14ubuntu2~20.04.1",
PackageVersion(epoch="0", upstream="1.2.3a.4-42.2", revision="14ubuntu2~20.04.1"),
),
(
"3:1.2.3a.4-42.2-14ubuntu2~20.04.1",
PackageVersion(epoch="3", upstream="1.2.3a.4-42.2", revision="14ubuntu2~20.04.1"),
),
("3:1.2.3a.4ubuntu", PackageVersion(epoch="3", upstream="1.2.3a.4ubuntu", revision="")),
("3:1.2.3a-4ubuntu", PackageVersion(epoch="3", upstream="1.2.3a", revision="4ubuntu")),
("3:1.2.3a-4ubuntu1", PackageVersion(epoch="3", upstream="1.2.3a", revision="4ubuntu1")),
],
)
def test_version_parsing(version: str, expected: PackageVersion):
parsed_version = PackageVersion.from_str(version)

assert parsed_version == expected