debby 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- debby/__init__.py +14 -0
- debby/__main__.py +28 -0
- debby/args.py +189 -0
- debby/control_file.py +63 -0
- debby/exceptions.py +9 -0
- debby/files.py +60 -0
- debby/meta/__init__.py +5 -0
- debby/meta/meta.py +51 -0
- debby/meta/meta_loader.py +68 -0
- debby/meta/meta_loader_factory.py +24 -0
- debby/meta/poetry_meta_loader.py +28 -0
- debby/meta/pyproject_meta_loader.py +57 -0
- debby/package.py +30 -0
- debby/py.typed +1 -0
- debby-1.0.0.dist-info/LICENSE +674 -0
- debby-1.0.0.dist-info/METADATA +81 -0
- debby-1.0.0.dist-info/RECORD +19 -0
- debby-1.0.0.dist-info/WHEEL +4 -0
- debby-1.0.0.dist-info/entry_points.txt +3 -0
debby/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. include:: ../README.md
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import importlib.metadata as metadata
|
|
6
|
+
|
|
7
|
+
__version__ = metadata.version(__package__ or __name__)
|
|
8
|
+
|
|
9
|
+
from .control_file import ControlFile
|
|
10
|
+
from .files import Files
|
|
11
|
+
from .meta import Meta
|
|
12
|
+
from .package import Package
|
|
13
|
+
|
|
14
|
+
__all__ = ("Package", "ControlFile", "Files", "Meta")
|
debby/__main__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Sequence
|
|
4
|
+
|
|
5
|
+
from debby.args import Args
|
|
6
|
+
from debby.control_file import ControlFile
|
|
7
|
+
from debby.exceptions import DebbyError
|
|
8
|
+
from debby.files import Files
|
|
9
|
+
from debby.meta import MetaLoaderFactory
|
|
10
|
+
from debby.package import Package
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv: Sequence[str] | None = None):
|
|
14
|
+
print(create_package(Args.parse(argv)))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
try:
|
|
19
|
+
main()
|
|
20
|
+
except DebbyError as e:
|
|
21
|
+
print(e, file=sys.stderr)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_package(args: Args) -> Path:
|
|
25
|
+
meta = MetaLoaderFactory(args).loader().load()
|
|
26
|
+
files = Files(args.files)
|
|
27
|
+
package = Package(meta, ControlFile(meta, files, args.template), files)
|
|
28
|
+
return package.create(args.out_dir)
|
debby/args.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from argparse import ArgumentParser
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Literal, Self, Sequence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Args:
|
|
10
|
+
files: Sequence[tuple[Path, Path]]
|
|
11
|
+
template: Path | None
|
|
12
|
+
out_dir: Path
|
|
13
|
+
pyproject: Path | None
|
|
14
|
+
poetry: Path | None
|
|
15
|
+
name: str | None
|
|
16
|
+
source: str | None
|
|
17
|
+
version: str | None
|
|
18
|
+
section: str | None
|
|
19
|
+
priority: str | None
|
|
20
|
+
architecture: str | None
|
|
21
|
+
essential: Literal["yes", "no"] | None
|
|
22
|
+
maintainer: str | None
|
|
23
|
+
description: str | None
|
|
24
|
+
homepage: str | None
|
|
25
|
+
depends: str | None
|
|
26
|
+
pre_depends: str | None
|
|
27
|
+
recommends: str | None
|
|
28
|
+
suggests: str | None
|
|
29
|
+
enhances: str | None
|
|
30
|
+
breaks: str | None
|
|
31
|
+
conflicts: str | None
|
|
32
|
+
no_size: bool = False
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def parse(cls, argv: Sequence[str] | None = None) -> Self:
|
|
36
|
+
parser = ArgumentParser()
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-f",
|
|
39
|
+
"--file",
|
|
40
|
+
dest="files",
|
|
41
|
+
help="Path to the files to package. Can be passed multiple times. E.g. --file SOURCE1 DESTINATION1 --file SOURCE2 DESTINATION2. Sources must point to existing files or directories and destinations will be treated as relative to the package root.",
|
|
42
|
+
type=Path,
|
|
43
|
+
nargs=2,
|
|
44
|
+
action="append",
|
|
45
|
+
default=[],
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"-t",
|
|
49
|
+
"--template",
|
|
50
|
+
help="Path to the control file template",
|
|
51
|
+
type=Path,
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"-o",
|
|
55
|
+
"--out-dir",
|
|
56
|
+
help="Path to the output directory",
|
|
57
|
+
type=Path,
|
|
58
|
+
default=Path("debian/build"),
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--no-size",
|
|
62
|
+
help="Do not include the total size of the package in the control file",
|
|
63
|
+
action="store_true",
|
|
64
|
+
)
|
|
65
|
+
cls._add_meta_source_group(parser)
|
|
66
|
+
cls._add_meta_override_args(parser)
|
|
67
|
+
return cls(**vars(parser.parse_args(argv)))
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def _add_meta_source_group(cls, parser: ArgumentParser) -> None:
|
|
71
|
+
meta_source_group = parser.add_mutually_exclusive_group(required=True)
|
|
72
|
+
meta_source_group.add_argument(
|
|
73
|
+
"--pyproject",
|
|
74
|
+
help="Read metadata according to PEP 621 from the given pyproject.toml file",
|
|
75
|
+
type=Path,
|
|
76
|
+
metavar="pyproject.toml",
|
|
77
|
+
)
|
|
78
|
+
meta_source_group.add_argument(
|
|
79
|
+
"--poetry",
|
|
80
|
+
help="Read poetry metadata from the given pyproject.toml file",
|
|
81
|
+
type=Path,
|
|
82
|
+
metavar="pyproject.toml",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def _add_meta_override_args(cls, parser: ArgumentParser) -> None:
|
|
87
|
+
meta_overrides_group = parser.add_argument_group(
|
|
88
|
+
"Metadata Overrides",
|
|
89
|
+
"Override metadata fields. Environment variables such as DEBBY_META_VERSION can be used to set these fields.",
|
|
90
|
+
)
|
|
91
|
+
meta_overrides_group.add_argument(
|
|
92
|
+
"-n",
|
|
93
|
+
"--name",
|
|
94
|
+
help="Specify the package name",
|
|
95
|
+
default=os.environ.get("DEBBY_META_NAME"),
|
|
96
|
+
)
|
|
97
|
+
meta_overrides_group.add_argument(
|
|
98
|
+
"-s",
|
|
99
|
+
"--source",
|
|
100
|
+
help="Specify the package source",
|
|
101
|
+
default=os.environ.get("DEBBY_META_SOURCE"),
|
|
102
|
+
)
|
|
103
|
+
meta_overrides_group.add_argument(
|
|
104
|
+
"-v",
|
|
105
|
+
"--version",
|
|
106
|
+
help="Specify the package version",
|
|
107
|
+
default=os.environ.get("DEBBY_META_VERSION"),
|
|
108
|
+
)
|
|
109
|
+
meta_overrides_group.add_argument(
|
|
110
|
+
"--section",
|
|
111
|
+
help="Specify the package section",
|
|
112
|
+
default=os.environ.get("DEBBY_META_SECTION"),
|
|
113
|
+
)
|
|
114
|
+
meta_overrides_group.add_argument(
|
|
115
|
+
"-p",
|
|
116
|
+
"--priority",
|
|
117
|
+
help="Specify the package priority",
|
|
118
|
+
default=os.environ.get("DEBBY_META_PRIORITY"),
|
|
119
|
+
)
|
|
120
|
+
meta_overrides_group.add_argument(
|
|
121
|
+
"-a",
|
|
122
|
+
"--architecture",
|
|
123
|
+
help="Specify the package architecture",
|
|
124
|
+
default=os.environ.get("DEBBY_META_ARCHITECTURE"),
|
|
125
|
+
)
|
|
126
|
+
meta_overrides_group.add_argument(
|
|
127
|
+
"-e",
|
|
128
|
+
"--essential",
|
|
129
|
+
choices=["yes", "no"],
|
|
130
|
+
help="Specify whether the package is essential",
|
|
131
|
+
default=os.environ.get("DEBBY_META_ESSENTIAL"),
|
|
132
|
+
)
|
|
133
|
+
meta_overrides_group.add_argument(
|
|
134
|
+
"-m",
|
|
135
|
+
"--maintainer",
|
|
136
|
+
help="Specify the package maintainer",
|
|
137
|
+
default=os.environ.get("DEBBY_META_MAINTAINER"),
|
|
138
|
+
)
|
|
139
|
+
meta_overrides_group.add_argument(
|
|
140
|
+
"-d",
|
|
141
|
+
"--description",
|
|
142
|
+
help="Specify the package description",
|
|
143
|
+
default=os.environ.get("DEBBY_META_DESCRIPTION"),
|
|
144
|
+
)
|
|
145
|
+
meta_overrides_group.add_argument(
|
|
146
|
+
"--homepage",
|
|
147
|
+
help="Specify the package homepage",
|
|
148
|
+
default=os.environ.get("DEBBY_META_HOMEPAGE"),
|
|
149
|
+
)
|
|
150
|
+
cls._add_dependencies_args(meta_overrides_group)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def _add_dependencies_args(cls, parser: ArgumentParser | Any) -> None:
|
|
154
|
+
parser.add_argument_group("Dependencies", "Specify package dependencies")
|
|
155
|
+
parser.add_argument(
|
|
156
|
+
"--depends",
|
|
157
|
+
help="Specify package dependencies",
|
|
158
|
+
default=os.environ.get("DEBBY_META_DEPENDS"),
|
|
159
|
+
)
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"--pre-depends",
|
|
162
|
+
help="Specify package pre-dependencies",
|
|
163
|
+
default=os.environ.get("DEBBY_META_PRE_DEPENDS"),
|
|
164
|
+
)
|
|
165
|
+
parser.add_argument(
|
|
166
|
+
"--recommends",
|
|
167
|
+
help="Specify package recommendations",
|
|
168
|
+
default=os.environ.get("DEBBY_META_RECOMMENDS"),
|
|
169
|
+
)
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--suggests",
|
|
172
|
+
help="Specify package suggestions",
|
|
173
|
+
default=os.environ.get("DEBBY_META_SUGGESTS"),
|
|
174
|
+
)
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
"--enhances",
|
|
177
|
+
help="Specify package enhancements",
|
|
178
|
+
default=os.environ.get("DEBBY_META_ENHANCES"),
|
|
179
|
+
)
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--breaks",
|
|
182
|
+
help="Specify package breaks",
|
|
183
|
+
default=os.environ.get("DEBBY_META_BREAKS"),
|
|
184
|
+
)
|
|
185
|
+
parser.add_argument(
|
|
186
|
+
"--conflicts",
|
|
187
|
+
help="Specify package conflicts",
|
|
188
|
+
default=os.environ.get("DEBBY_META_CONFLICTS"),
|
|
189
|
+
)
|
debby/control_file.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Iterable
|
|
3
|
+
|
|
4
|
+
from debby.files import Files
|
|
5
|
+
from debby.meta import Meta
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ControlFile:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
meta: Meta,
|
|
12
|
+
files: Files,
|
|
13
|
+
template: Path | None = None,
|
|
14
|
+
include_size: bool = True,
|
|
15
|
+
) -> None:
|
|
16
|
+
self._meta = meta
|
|
17
|
+
self._files = files
|
|
18
|
+
self._template = template
|
|
19
|
+
self._include_size = include_size
|
|
20
|
+
|
|
21
|
+
def create(self, path: Path) -> None:
|
|
22
|
+
path.write_text("\n".join(self._lines()) + "\n")
|
|
23
|
+
|
|
24
|
+
def _lines(self) -> Iterable[str]:
|
|
25
|
+
if not self._template:
|
|
26
|
+
return self._default_lines()
|
|
27
|
+
return (
|
|
28
|
+
line.format(metadata=self._meta, files=self._files)
|
|
29
|
+
for line in self._template.read_text().splitlines()
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def _default_lines(self) -> Iterable[str]:
|
|
33
|
+
yield f"Package: {self._meta.name}"
|
|
34
|
+
if self._meta.source:
|
|
35
|
+
yield f"Source: {self._meta.source}"
|
|
36
|
+
yield f"Version: {self._meta.version}"
|
|
37
|
+
if self._meta.section:
|
|
38
|
+
yield f"Section: {self._meta.section}"
|
|
39
|
+
if self._meta.priority:
|
|
40
|
+
yield f"Priority: {self._meta.priority}"
|
|
41
|
+
yield f"Architecture: {self._meta.architecture}"
|
|
42
|
+
if self._meta.essential:
|
|
43
|
+
yield f"Essential: {self._meta.essential}"
|
|
44
|
+
if self._include_size and self._files.total_size:
|
|
45
|
+
yield f"Installed-Size: {self._files.total_size}"
|
|
46
|
+
yield f"Maintainer: {self._meta.maintainer}"
|
|
47
|
+
yield f"Description: {self._meta.description}"
|
|
48
|
+
if homepage := self._meta.homepage:
|
|
49
|
+
yield f"Homepage: {homepage}"
|
|
50
|
+
if depends := self._meta.depends:
|
|
51
|
+
yield f"Depends: {depends}"
|
|
52
|
+
if pre_depends := self._meta.pre_depends:
|
|
53
|
+
yield f"Pre-Depends: {pre_depends}"
|
|
54
|
+
if recommends := self._meta.recommends:
|
|
55
|
+
yield f"Recommends: {recommends}"
|
|
56
|
+
if suggests := self._meta.suggests:
|
|
57
|
+
yield f"Suggests: {suggests}"
|
|
58
|
+
if enhances := self._meta.enhances:
|
|
59
|
+
yield f"Enhances: {enhances}"
|
|
60
|
+
if breaks := self._meta.breaks:
|
|
61
|
+
yield f"Breaks: {breaks}"
|
|
62
|
+
if conflicts := self._meta.conflicts:
|
|
63
|
+
yield f"Conflicts: {conflicts}"
|
debby/exceptions.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class DebbyError(Exception):
|
|
2
|
+
"""Base class for all Debby exceptions."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MissingMetadataError(DebbyError):
|
|
6
|
+
"""Raised when required metadata is missing."""
|
|
7
|
+
|
|
8
|
+
def __str__(self) -> str:
|
|
9
|
+
return f"Missing some required metadata. Make sure it's present in any of the sources, or provide it as a command-line argument or environment variable. {super().__str__()}"
|
debby/files.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Iterable, Iterator, Mapping
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Files(Mapping[Path, Path]):
|
|
7
|
+
"""A mapping of target paths in the package to source paths on the filesystem.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
files: An iterable of pairs of source paths and destination paths.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, files: Iterable[tuple[Path, Path]]) -> None:
|
|
14
|
+
self._files = self._normalize_files(files)
|
|
15
|
+
|
|
16
|
+
def __post_init__(self) -> None:
|
|
17
|
+
if any(
|
|
18
|
+
not src.exists() or dst.is_absolute() for dst, src in self._files.items()
|
|
19
|
+
):
|
|
20
|
+
raise ValueError(
|
|
21
|
+
"source files must all exist and destination paths must all be relative"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def package(self, path: Path) -> None:
|
|
25
|
+
for dst, src in self.items():
|
|
26
|
+
target = path / dst
|
|
27
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
target.symlink_to(src)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def _normalize_files(
|
|
32
|
+
cls, files: Iterable[tuple[Path, Path]]
|
|
33
|
+
) -> Mapping[Path, Path]:
|
|
34
|
+
return {
|
|
35
|
+
dst.relative_to(dst.anchor) if dst.is_absolute() else dst: src
|
|
36
|
+
for src, dst in files
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def sources(self) -> Iterable[Path]:
|
|
41
|
+
return self._files.values()
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def destinations(self) -> Iterable[Path]:
|
|
45
|
+
return self._files.keys()
|
|
46
|
+
|
|
47
|
+
@cached_property
|
|
48
|
+
def total_size(self) -> int:
|
|
49
|
+
return sum(
|
|
50
|
+
(max(f.resolve().stat().st_size, 1024) // 1024) * 1024 for f in self.sources
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def __getitem__(self, key: Path) -> Path:
|
|
54
|
+
return self._files[key]
|
|
55
|
+
|
|
56
|
+
def __iter__(self) -> Iterator[Path]:
|
|
57
|
+
return iter(self._files)
|
|
58
|
+
|
|
59
|
+
def __len__(self) -> int:
|
|
60
|
+
return len(self._files)
|
debby/meta/__init__.py
ADDED
debby/meta/meta.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Literal, TypedDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MetaVars(TypedDict, total=False):
|
|
6
|
+
"""A dictionary of metadata variables."""
|
|
7
|
+
|
|
8
|
+
name: str
|
|
9
|
+
source: str
|
|
10
|
+
version: str
|
|
11
|
+
section: str
|
|
12
|
+
priority: str
|
|
13
|
+
architecture: str
|
|
14
|
+
essential: Literal["yes", "no"]
|
|
15
|
+
maintainer: str
|
|
16
|
+
description: str
|
|
17
|
+
homepage: str
|
|
18
|
+
depends: str
|
|
19
|
+
pre_depends: str
|
|
20
|
+
recommends: str
|
|
21
|
+
suggests: str
|
|
22
|
+
enhances: str
|
|
23
|
+
breaks: str
|
|
24
|
+
conflicts: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(kw_only=True, frozen=True, slots=True)
|
|
28
|
+
class Meta:
|
|
29
|
+
"""Metadata for a Debian package."""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
source: str | None = None
|
|
33
|
+
version: str
|
|
34
|
+
section: str | None = None
|
|
35
|
+
priority: str | None = None
|
|
36
|
+
architecture: str = "all"
|
|
37
|
+
essential: Literal["yes", "no"] | None = None
|
|
38
|
+
maintainer: str
|
|
39
|
+
description: str
|
|
40
|
+
homepage: str | None = None
|
|
41
|
+
depends: str | None = None
|
|
42
|
+
pre_depends: str | None = None
|
|
43
|
+
recommends: str | None = None
|
|
44
|
+
suggests: str | None = None
|
|
45
|
+
enhances: str | None = None
|
|
46
|
+
breaks: str | None = None
|
|
47
|
+
conflicts: str | None = None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def full_name(self) -> str:
|
|
51
|
+
return f"{self.name}_{self.version}_{self.architecture}"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from debby.args import Args
|
|
2
|
+
from debby.exceptions import MissingMetadataError
|
|
3
|
+
|
|
4
|
+
from .meta import Meta, MetaVars
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MetaLoader:
|
|
8
|
+
"""Load metadata from a source.
|
|
9
|
+
|
|
10
|
+
Classes that inherit from this class should implement the `load_from_source` method. The value they return will be overridden by the values specified by the user in the command-line arguments, and the resulting metadata will be returned.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
args: The command-line arguments.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, args: Args) -> None:
|
|
17
|
+
self._args = args
|
|
18
|
+
|
|
19
|
+
def load(self) -> Meta:
|
|
20
|
+
"""Load the metadata."""
|
|
21
|
+
kwargs: MetaVars = {**self.load_from_source(), **self.overrides()}
|
|
22
|
+
try:
|
|
23
|
+
return Meta(**kwargs)
|
|
24
|
+
except TypeError as e:
|
|
25
|
+
raise MissingMetadataError(str(e)) from None
|
|
26
|
+
|
|
27
|
+
def load_from_source(self) -> MetaVars:
|
|
28
|
+
"""Load the metadata from the source."""
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
def overrides(self) -> MetaVars:
|
|
32
|
+
"""Return metadata overrides as specified by the user."""
|
|
33
|
+
result: MetaVars = {}
|
|
34
|
+
if self._args.name:
|
|
35
|
+
result["name"] = self._args.name
|
|
36
|
+
if self._args.source:
|
|
37
|
+
result["source"] = self._args.source
|
|
38
|
+
if self._args.version:
|
|
39
|
+
result["version"] = self._args.version
|
|
40
|
+
if self._args.section:
|
|
41
|
+
result["section"] = self._args.section
|
|
42
|
+
if self._args.priority:
|
|
43
|
+
result["priority"] = self._args.priority
|
|
44
|
+
if self._args.architecture:
|
|
45
|
+
result["architecture"] = self._args.architecture
|
|
46
|
+
if self._args.essential:
|
|
47
|
+
result["essential"] = self._args.essential
|
|
48
|
+
if self._args.maintainer:
|
|
49
|
+
result["maintainer"] = self._args.maintainer
|
|
50
|
+
if self._args.description:
|
|
51
|
+
result["description"] = self._args.description
|
|
52
|
+
if self._args.homepage:
|
|
53
|
+
result["homepage"] = self._args.homepage
|
|
54
|
+
if self._args.depends:
|
|
55
|
+
result["depends"] = self._args.depends
|
|
56
|
+
if self._args.pre_depends:
|
|
57
|
+
result["pre_depends"] = self._args.pre_depends
|
|
58
|
+
if self._args.recommends:
|
|
59
|
+
result["recommends"] = self._args.recommends
|
|
60
|
+
if self._args.suggests:
|
|
61
|
+
result["suggests"] = self._args.suggests
|
|
62
|
+
if self._args.enhances:
|
|
63
|
+
result["enhances"] = self._args.enhances
|
|
64
|
+
if self._args.breaks:
|
|
65
|
+
result["breaks"] = self._args.breaks
|
|
66
|
+
if self._args.conflicts:
|
|
67
|
+
result["conflicts"] = self._args.conflicts
|
|
68
|
+
return result
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from debby.args import Args
|
|
2
|
+
|
|
3
|
+
from .meta_loader import MetaLoader
|
|
4
|
+
from .poetry_meta_loader import PoetryMetaLoader
|
|
5
|
+
from .pyproject_meta_loader import PyprojectMetaLoader
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MetaLoaderFactory:
|
|
9
|
+
"""Factory for creating an appropriate MetaLoader according to the given arguments.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
args: The command-line arguments.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, args: Args) -> None:
|
|
16
|
+
self._args = args
|
|
17
|
+
|
|
18
|
+
def loader(self) -> MetaLoader:
|
|
19
|
+
"""Create the appropriate MetaLoader."""
|
|
20
|
+
if self._args.pyproject:
|
|
21
|
+
return PyprojectMetaLoader(self._args)
|
|
22
|
+
if self._args.poetry:
|
|
23
|
+
return PoetryMetaLoader(self._args)
|
|
24
|
+
return MetaLoader(self._args)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .meta import MetaVars
|
|
5
|
+
from .meta_loader import MetaLoader
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PoetryMetaLoader(MetaLoader):
|
|
9
|
+
"""Load poetry metadata from a pyproject.toml file."""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def _pyproject(self) -> Path:
|
|
13
|
+
assert self._args.poetry is not None
|
|
14
|
+
return self._args.poetry
|
|
15
|
+
|
|
16
|
+
def load_from_source(self) -> MetaVars:
|
|
17
|
+
poetry_data = tomllib.loads(self._pyproject.read_text())["tool"]["poetry"]
|
|
18
|
+
result: MetaVars = {
|
|
19
|
+
"name": poetry_data["name"],
|
|
20
|
+
"version": poetry_data["version"],
|
|
21
|
+
"description": poetry_data["description"],
|
|
22
|
+
"maintainer": next(
|
|
23
|
+
iter((*poetry_data.get("maintainers", ()), *poetry_data["authors"]))
|
|
24
|
+
),
|
|
25
|
+
}
|
|
26
|
+
if homepage := poetry_data.get("homepage"):
|
|
27
|
+
result["homepage"] = homepage
|
|
28
|
+
return result
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Iterable, Mapping, TypedDict
|
|
4
|
+
|
|
5
|
+
from .meta import MetaVars
|
|
6
|
+
from .meta_loader import MetaLoader
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PyprojectMetaLoader(MetaLoader):
|
|
10
|
+
"""Load python project metadata according to PEP621 from a pyproject.toml file."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def _pyproject(self) -> Path:
|
|
14
|
+
assert self._args.pyproject is not None
|
|
15
|
+
return self._args.pyproject
|
|
16
|
+
|
|
17
|
+
def load_from_source(self) -> MetaVars:
|
|
18
|
+
project_data = tomllib.loads(self._pyproject.read_text())["project"]
|
|
19
|
+
result: MetaVars = {
|
|
20
|
+
"name": project_data["name"],
|
|
21
|
+
}
|
|
22
|
+
if "version" not in project_data["dynamic"]:
|
|
23
|
+
result["version"] = project_data["version"]
|
|
24
|
+
if maintainer := self._get_maintainer(project_data):
|
|
25
|
+
result["maintainer"] = maintainer
|
|
26
|
+
if description := project_data.get("description"):
|
|
27
|
+
result["description"] = description
|
|
28
|
+
if homepage := self._get_homepage(project_data.get("urls", {})):
|
|
29
|
+
result["homepage"] = homepage
|
|
30
|
+
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
def _get_maintainer(self, project_data: Mapping[str, Any]) -> str | None:
|
|
34
|
+
people: Iterable[_Person] = (
|
|
35
|
+
project_data.get("maintainers") or project_data.get("authors") or ()
|
|
36
|
+
)
|
|
37
|
+
person = next(iter(people), None)
|
|
38
|
+
if person is None:
|
|
39
|
+
return None
|
|
40
|
+
if person.get("name") and person.get("email"):
|
|
41
|
+
return f"{person.get('name')} <{person.get('email')}>"
|
|
42
|
+
if person.get("name"):
|
|
43
|
+
return person.get("name")
|
|
44
|
+
if person.get("email"):
|
|
45
|
+
return person.get("email")
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def _get_homepage(self, urls: Mapping[str, str]) -> str | None:
|
|
49
|
+
for key in ("Homepage", "Repository", "Documentation"):
|
|
50
|
+
if url := urls.get(key):
|
|
51
|
+
return url
|
|
52
|
+
return next(iter(urls.values()), None)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _Person(TypedDict, total=False):
|
|
56
|
+
name: str
|
|
57
|
+
email: str
|
debby/package.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from debby.control_file import ControlFile
|
|
4
|
+
from debby.files import Files
|
|
5
|
+
from debby.meta.meta import Meta
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Package:
|
|
9
|
+
"""A directory containing a Debian package.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
metadata: The metadata for the package.
|
|
13
|
+
control: The control file for the package.
|
|
14
|
+
files: The files to put in the package.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, metadata: Meta, control: ControlFile, files: Files) -> None:
|
|
18
|
+
self.metadata = metadata
|
|
19
|
+
self.control = control
|
|
20
|
+
self.files = files
|
|
21
|
+
|
|
22
|
+
def create(self, out_dir: Path) -> Path:
|
|
23
|
+
"""Create the directory structure of the package, which can be packaged into a .deb file with dpkg-deb."""
|
|
24
|
+
directory = out_dir / self.metadata.full_name
|
|
25
|
+
control_dir = directory / "DEBIAN"
|
|
26
|
+
control_dir.mkdir(parents=True)
|
|
27
|
+
control_dir.chmod(0o755)
|
|
28
|
+
self.control.create(control_dir / "control")
|
|
29
|
+
self.files.package(directory)
|
|
30
|
+
return directory
|
debby/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|