diff --git a/src/debmagic/_dpkg/buildflags.py b/src/debmagic/_dpkg/buildflags.py index 7d1cdb2..fe6f809 100644 --- a/src/debmagic/_dpkg/buildflags.py +++ b/src/debmagic/_dpkg/buildflags.py @@ -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. """ @@ -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 @@ -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 = { @@ -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 diff --git a/src/debmagic/_package.py b/src/debmagic/_package.py index 039705f..c2ffe19 100644 --- a/src/debmagic/_package.py +++ b/src/debmagic/_package.py @@ -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 @@ -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) @@ -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 @@ -281,6 +283,7 @@ def package( rules_file, presets, bin_pkgs, + version, buildflags=Namespace(**flags), ) diff --git a/src/debmagic/_package_version.py b/src/debmagic/_package_version.py new file mode 100644 index 0000000..c2b5bef --- /dev/null +++ b/src/debmagic/_package_version.py @@ -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), + ) diff --git a/src/debmagic/_utils.py b/src/debmagic/_utils.py index 04b56a6..b7c31c4 100644 --- a/src/debmagic/_utils.py +++ b/src/debmagic/_utils.py @@ -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 diff --git a/tests/unit/test_exec.py b/tests/unit/test_exec.py new file mode 100644 index 0000000..3b2a2d7 --- /dev/null +++ b/tests/unit/test_exec.py @@ -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) diff --git a/tests/unit/test_versioning.py b/tests/unit/test_versioning.py new file mode 100644 index 0000000..8dc3a99 --- /dev/null +++ b/tests/unit/test_versioning.py @@ -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