debgraph 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- debgraph-0.2.0/PKG-INFO +78 -0
- debgraph-0.2.0/README +61 -0
- debgraph-0.2.0/debgraph/__init__.py +21 -0
- debgraph-0.2.0/debgraph/debgraph.py +515 -0
- debgraph-0.2.0/debgraph.egg-info/PKG-INFO +78 -0
- {debgraph-0.1.0 → debgraph-0.2.0}/debgraph.egg-info/SOURCES.txt +4 -1
- debgraph-0.2.0/debgraph.egg-info/requires.txt +5 -0
- {debgraph-0.1.0 → debgraph-0.2.0}/pyproject.toml +7 -4
- debgraph-0.2.0/tests/test_debgraph.py +64 -0
- debgraph-0.1.0/PKG-INFO +0 -39
- debgraph-0.1.0/README +0 -27
- debgraph-0.1.0/debgraph/debgraph.py +0 -188
- debgraph-0.1.0/debgraph.egg-info/PKG-INFO +0 -39
- {debgraph-0.1.0 → debgraph-0.2.0}/COPYING +0 -0
- {debgraph-0.1.0 → debgraph-0.2.0}/LICENSE +0 -0
- {debgraph-0.1.0 → debgraph-0.2.0}/debgraph.egg-info/dependency_links.txt +0 -0
- {debgraph-0.1.0 → debgraph-0.2.0}/debgraph.egg-info/entry_points.txt +0 -0
- {debgraph-0.1.0 → debgraph-0.2.0}/debgraph.egg-info/top_level.txt +0 -0
- {debgraph-0.1.0 → debgraph-0.2.0}/setup.cfg +0 -0
debgraph-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: debgraph
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Generates a graph of your debian packages
|
|
5
|
+
License-Expression: GPL-3.0-only
|
|
6
|
+
Project-URL: Homepage, https://github.com/garyg1/debgraph
|
|
7
|
+
Project-URL: Issues, https://github.com/garyg1/debgraph/issues
|
|
8
|
+
Requires-Python: >=3.7
|
|
9
|
+
Description-Content-Type: text/plain
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: COPYING
|
|
12
|
+
Requires-Dist: graphviz
|
|
13
|
+
Provides-Extra: tests
|
|
14
|
+
Requires-Dist: pytest; extra == "tests"
|
|
15
|
+
Requires-Dist: black; extra == "tests"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
Debgraph, a program like debtree to view all the Debian packages on your system, in a single graph.
|
|
19
|
+
|
|
20
|
+
Supports GEXF, DOT (Graphviz), and JSON-Lines output.
|
|
21
|
+
|
|
22
|
+
In development. Please note that API may change across 0.x versions.
|
|
23
|
+
|
|
24
|
+
License
|
|
25
|
+
See COPYING
|
|
26
|
+
|
|
27
|
+
Installation
|
|
28
|
+
pip install debgraph
|
|
29
|
+
|
|
30
|
+
Usage
|
|
31
|
+
DOT Example
|
|
32
|
+
debgraph
|
|
33
|
+
|
|
34
|
+
head debian.dot
|
|
35
|
+
|
|
36
|
+
digraph Debian {
|
|
37
|
+
"adduser" [label="adduser"];
|
|
38
|
+
"adwaita-icon-theme" [label="adwaita-icon-theme"];
|
|
39
|
+
"alsa-topology-conf" [label="alsa-topology-conf"];
|
|
40
|
+
"alsa-ucm-conf" [label="alsa-ucm-conf"];
|
|
41
|
+
"apparmor" [label="apparmor"];
|
|
42
|
+
"apport" [label="apport"];
|
|
43
|
+
"apport-core-dump-handler" [label="apport-core-dump-handler"];
|
|
44
|
+
"apport-symptoms" [label="apport-symptoms"];
|
|
45
|
+
"appstream" [label="appstream"];
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
GEXF Example
|
|
49
|
+
debgraph debian.gexf
|
|
50
|
+
|
|
51
|
+
head debian.gexf
|
|
52
|
+
|
|
53
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
54
|
+
<gexf xmlns="http://www.gexf.net/1.3" version="1.3" xmlns:viz="http://www.gexf.net/1.3/viz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gexf.net/1.3 http://www.gexf.net/1.3/gexf.xsd">
|
|
55
|
+
<meta lastmodifieddate="2026-06-30">
|
|
56
|
+
<creator>debgraph</creator>
|
|
57
|
+
<description>A graph of apt packages on a Debian system.</description>
|
|
58
|
+
</meta>
|
|
59
|
+
<graph defaultedgetype="directed" idtype="string" type="static">
|
|
60
|
+
|
|
61
|
+
<attributes class="node">
|
|
62
|
+
<attribute id="0" title="binary:Synopsis" type="string"/>
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
JSON-Lines Example
|
|
66
|
+
debgraph debian.jsonl
|
|
67
|
+
|
|
68
|
+
head debian.jsonl
|
|
69
|
+
|
|
70
|
+
{"id": 1, "name": "adduser", "version": "3.153ubuntu1", "dependencies": ... }
|
|
71
|
+
{"id": 2, "name": "adwaita-icon-theme", "version": "50.0-1", "dependencies": ... }
|
|
72
|
+
{"id": 3, "name": "alsa-topology-conf", "version": "1.2.5.1-3build1", "dependencies": ... }
|
|
73
|
+
{"id": 4, "name": "alsa-ucm-conf", "version": "1.2.15.3-1ubuntu1", "dependencies": ... }
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
Development
|
|
77
|
+
pip install ".[tests]" .
|
|
78
|
+
python -m pytest .
|
debgraph-0.2.0/README
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Debgraph, a program like debtree to view all the Debian packages on your system, in a single graph.
|
|
2
|
+
|
|
3
|
+
Supports GEXF, DOT (Graphviz), and JSON-Lines output.
|
|
4
|
+
|
|
5
|
+
In development. Please note that API may change across 0.x versions.
|
|
6
|
+
|
|
7
|
+
License
|
|
8
|
+
See COPYING
|
|
9
|
+
|
|
10
|
+
Installation
|
|
11
|
+
pip install debgraph
|
|
12
|
+
|
|
13
|
+
Usage
|
|
14
|
+
DOT Example
|
|
15
|
+
debgraph
|
|
16
|
+
|
|
17
|
+
head debian.dot
|
|
18
|
+
|
|
19
|
+
digraph Debian {
|
|
20
|
+
"adduser" [label="adduser"];
|
|
21
|
+
"adwaita-icon-theme" [label="adwaita-icon-theme"];
|
|
22
|
+
"alsa-topology-conf" [label="alsa-topology-conf"];
|
|
23
|
+
"alsa-ucm-conf" [label="alsa-ucm-conf"];
|
|
24
|
+
"apparmor" [label="apparmor"];
|
|
25
|
+
"apport" [label="apport"];
|
|
26
|
+
"apport-core-dump-handler" [label="apport-core-dump-handler"];
|
|
27
|
+
"apport-symptoms" [label="apport-symptoms"];
|
|
28
|
+
"appstream" [label="appstream"];
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
GEXF Example
|
|
32
|
+
debgraph debian.gexf
|
|
33
|
+
|
|
34
|
+
head debian.gexf
|
|
35
|
+
|
|
36
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
37
|
+
<gexf xmlns="http://www.gexf.net/1.3" version="1.3" xmlns:viz="http://www.gexf.net/1.3/viz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gexf.net/1.3 http://www.gexf.net/1.3/gexf.xsd">
|
|
38
|
+
<meta lastmodifieddate="2026-06-30">
|
|
39
|
+
<creator>debgraph</creator>
|
|
40
|
+
<description>A graph of apt packages on a Debian system.</description>
|
|
41
|
+
</meta>
|
|
42
|
+
<graph defaultedgetype="directed" idtype="string" type="static">
|
|
43
|
+
|
|
44
|
+
<attributes class="node">
|
|
45
|
+
<attribute id="0" title="binary:Synopsis" type="string"/>
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
JSON-Lines Example
|
|
49
|
+
debgraph debian.jsonl
|
|
50
|
+
|
|
51
|
+
head debian.jsonl
|
|
52
|
+
|
|
53
|
+
{"id": 1, "name": "adduser", "version": "3.153ubuntu1", "dependencies": ... }
|
|
54
|
+
{"id": 2, "name": "adwaita-icon-theme", "version": "50.0-1", "dependencies": ... }
|
|
55
|
+
{"id": 3, "name": "alsa-topology-conf", "version": "1.2.5.1-3build1", "dependencies": ... }
|
|
56
|
+
{"id": 4, "name": "alsa-ucm-conf", "version": "1.2.15.3-1ubuntu1", "dependencies": ... }
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
Development
|
|
60
|
+
pip install ".[tests]" .
|
|
61
|
+
python -m pytest .
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .debgraph import (
|
|
2
|
+
run_debgraph,
|
|
3
|
+
DpkgReader,
|
|
4
|
+
GraphFileWriter,
|
|
5
|
+
Package,
|
|
6
|
+
PackageDependencyAlternatives,
|
|
7
|
+
PackageDependency,
|
|
8
|
+
DebgraphError,
|
|
9
|
+
Options,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
run_debgraph,
|
|
14
|
+
DpkgReader,
|
|
15
|
+
GraphFileWriter,
|
|
16
|
+
Package,
|
|
17
|
+
PackageDependencyAlternatives,
|
|
18
|
+
PackageDependency,
|
|
19
|
+
DebgraphError,
|
|
20
|
+
Options,
|
|
21
|
+
]
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Debgraph, a program like debtree to view ALL the Debian packages on your system.
|
|
3
|
+
Copyright (C) 2026 Gary Gurlaskie
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU General Public License, version 3 as
|
|
7
|
+
published by the Free Software Foundation.
|
|
8
|
+
|
|
9
|
+
This program is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU General Public License
|
|
15
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import List, Optional, Dict, Iterable, Any
|
|
22
|
+
from datetime import date
|
|
23
|
+
import argparse
|
|
24
|
+
import collections
|
|
25
|
+
import csv
|
|
26
|
+
import io
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
from xml.sax.saxutils import escape, quoteattr
|
|
33
|
+
import graphviz
|
|
34
|
+
|
|
35
|
+
__version__ = "0.2.0"
|
|
36
|
+
|
|
37
|
+
_sep = "\n"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GenericEncoder(json.JSONEncoder):
|
|
41
|
+
def default(self, obj):
|
|
42
|
+
if isinstance(obj, PackageDependencyAlternatives):
|
|
43
|
+
return {
|
|
44
|
+
**obj.__dict__,
|
|
45
|
+
"actuals": [
|
|
46
|
+
{"name": actual.name, "version": actual.version}
|
|
47
|
+
for actual in obj.actuals
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
return obj.__dict__
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DebgraphError(Exception):
|
|
54
|
+
def __init__(self, message, posix_status):
|
|
55
|
+
self.posix_status = posix_status
|
|
56
|
+
self.message = message
|
|
57
|
+
super().__init__(message)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PackageDependency:
|
|
61
|
+
"""
|
|
62
|
+
Represents a dpkg package dependency, such as "apt-transport-https (= 3.2.0)".
|
|
63
|
+
|
|
64
|
+
See: https://www.debian.org/doc/debian-policy/ch-relationships.html.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, name, op=None, version=None):
|
|
68
|
+
self.name = name
|
|
69
|
+
self.op = op
|
|
70
|
+
self.version = version
|
|
71
|
+
|
|
72
|
+
def __repr__(self):
|
|
73
|
+
return json.dumps(self.__dict__, cls=GenericEncoder)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PackageDependencyAlternatives:
|
|
77
|
+
"""
|
|
78
|
+
Represents a dpkg package alternative dependency list, such as "apt-transport-https (= 3.2.0) | libappstream5 (= 1)".
|
|
79
|
+
|
|
80
|
+
See: https://www.debian.org/doc/debian-policy/ch-relationships.html.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, alts: List[PackageDependency]):
|
|
84
|
+
self.alts = alts
|
|
85
|
+
self.actuals: List[Package] = []
|
|
86
|
+
|
|
87
|
+
def register_actual(self, actual: Package):
|
|
88
|
+
# use list scan to simplify serialization
|
|
89
|
+
if actual not in self.actuals:
|
|
90
|
+
self.actuals.append(actual)
|
|
91
|
+
|
|
92
|
+
def __repr__(self):
|
|
93
|
+
return json.dumps(self.__dict__, cls=GenericEncoder)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _identity_mapper(s: str) -> str:
|
|
97
|
+
return s
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _alt_syntax_mapper(s: str) -> str:
|
|
101
|
+
if s is None:
|
|
102
|
+
return None
|
|
103
|
+
serialized = Package._parse_package_refs(s)
|
|
104
|
+
return str(serialized)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Package:
|
|
108
|
+
"""
|
|
109
|
+
Represents a dpkg binary package. The supported fields are listed in get_all_dpkg_fields.
|
|
110
|
+
Implementation is not thread-safe.
|
|
111
|
+
|
|
112
|
+
See: https://www.debian.org/doc/debian-policy/ch-binary.html
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
_id = 1
|
|
116
|
+
_fields = [
|
|
117
|
+
"binary:Package",
|
|
118
|
+
"Depends",
|
|
119
|
+
]
|
|
120
|
+
_extra_field_mapping = [
|
|
121
|
+
("binary:Synopsis", "binary:Synopsis", _identity_mapper, "string"),
|
|
122
|
+
("Breaks", "Breaks", _alt_syntax_mapper, "string"),
|
|
123
|
+
("Description", "Description", _identity_mapper, "string"),
|
|
124
|
+
("Enhances", "Enhances", _alt_syntax_mapper, "string"),
|
|
125
|
+
("Installed-Size", "InstalledSizeKB", _identity_mapper, "integer"),
|
|
126
|
+
("Essential", "IsEssential", _identity_mapper, "string"),
|
|
127
|
+
("Maintainer", "Maintainer", _identity_mapper, "string"),
|
|
128
|
+
("Origin", "Origin", _identity_mapper, "string"),
|
|
129
|
+
("Provides", "Provides", _alt_syntax_mapper, "string"),
|
|
130
|
+
("Recommends", "Recommends", _alt_syntax_mapper, "string"),
|
|
131
|
+
("Replaces", "Replaces", _alt_syntax_mapper, "string"),
|
|
132
|
+
("Section", "Section", _identity_mapper, "string"),
|
|
133
|
+
("source:Package", "source:Package", _identity_mapper, "string"),
|
|
134
|
+
("source:Version", "source:Version", _identity_mapper, "string"),
|
|
135
|
+
("Suggests", "Suggests", _alt_syntax_mapper, "string"),
|
|
136
|
+
("Version", "Version", _identity_mapper, "string"),
|
|
137
|
+
]
|
|
138
|
+
_long_fields = [
|
|
139
|
+
"Description",
|
|
140
|
+
]
|
|
141
|
+
_pkgref_re = r"(?P<name>[a-zA-Z0-9\+\-\._]+)\s*(\(\s*(?P<op>(\=|\>\=|\>\>|\<\=|\<\<))\s*(?P<version>[^\)]+)\s*\))?"
|
|
142
|
+
|
|
143
|
+
def __init__(self):
|
|
144
|
+
self.id = Package._id
|
|
145
|
+
Package._id += 1
|
|
146
|
+
|
|
147
|
+
self.name: Optional[str] = None
|
|
148
|
+
self.version: Optional[str] = None
|
|
149
|
+
self.dependencies: List[PackageDependencyAlternatives] = []
|
|
150
|
+
self.provides: List[PackageDependency] = []
|
|
151
|
+
self.extra: collections.defaultdict[str, Optional[str]] = (
|
|
152
|
+
collections.defaultdict(None)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def get_all_dpkg_fields(cls):
|
|
157
|
+
fields = [
|
|
158
|
+
*cls._fields,
|
|
159
|
+
*(dpkg_name for dpkg_name, _, _, _ in cls._extra_field_mapping),
|
|
160
|
+
]
|
|
161
|
+
return fields
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def get_all_extra_output_fields(cls):
|
|
165
|
+
return [
|
|
166
|
+
(output_name, output_type)
|
|
167
|
+
for _, output_name, _, output_type in cls._extra_field_mapping
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
def __repr__(self):
|
|
171
|
+
return json.dumps(self.__dict__, cls=GenericEncoder)
|
|
172
|
+
|
|
173
|
+
def get_no_dep_repr(self):
|
|
174
|
+
return {"name": self.name, "version": self.version}
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def _from_dict(cls, dict: Dict[str, str], long_fields: bool):
|
|
178
|
+
new = cls()
|
|
179
|
+
new.name = dict["binary:Package"]
|
|
180
|
+
new.version = dict["Version"]
|
|
181
|
+
new.dependencies = [
|
|
182
|
+
PackageDependencyAlternatives(altlist)
|
|
183
|
+
for altlist in cls._parse_package_refs(dict["Depends"])
|
|
184
|
+
]
|
|
185
|
+
new.provides = cls._flatten(cls._parse_package_refs(dict["Provides"]))
|
|
186
|
+
|
|
187
|
+
for dpkg_field, output_field, map_fn, _ in cls._extra_field_mapping:
|
|
188
|
+
if not long_fields and dpkg_field in cls._long_fields:
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
new.extra[output_field] = map_fn(dict.get(dpkg_field))
|
|
192
|
+
|
|
193
|
+
new.extra["Version"] = new.version
|
|
194
|
+
|
|
195
|
+
return new
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def _parse_package_ref(cls, raw_ref):
|
|
199
|
+
"""Parses a pkgref, i.e., `<name> (<op> <version>)"""
|
|
200
|
+
m = re.match(cls._pkgref_re, raw_ref)
|
|
201
|
+
if m:
|
|
202
|
+
return PackageDependency(m.group("name"), m.group("op"), m.group("version"))
|
|
203
|
+
else:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def _parse_package_ref_alt(cls, raw_ref_alt):
|
|
208
|
+
"""Parses a pkgrefalt, i.e., `<pkgref> | <pkgref> | ...`"""
|
|
209
|
+
ref_alt = raw_ref_alt.split("|")
|
|
210
|
+
alts = []
|
|
211
|
+
for raw in ref_alt:
|
|
212
|
+
raw = raw.strip()
|
|
213
|
+
if raw:
|
|
214
|
+
alts.append(cls._parse_package_ref(raw))
|
|
215
|
+
return alts
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def _parse_package_refs(cls, raw_str):
|
|
219
|
+
"""Parses a list of pkgrefalt, i.e., `<pkgrefalt>, <pkgrefalt>, ...`"""
|
|
220
|
+
raw_refs = raw_str.split(",")
|
|
221
|
+
refs = []
|
|
222
|
+
for raw_ref in raw_refs:
|
|
223
|
+
raw_ref = raw_ref.strip()
|
|
224
|
+
if raw_ref:
|
|
225
|
+
ref = cls._parse_package_ref_alt(raw_ref)
|
|
226
|
+
refs.append(ref)
|
|
227
|
+
return refs
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _flatten(l):
|
|
231
|
+
return [x for sublist in l for x in sublist]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class DpkgReader:
|
|
235
|
+
start_entry_delimiter = "[[debgraph magic start entry]]\n"
|
|
236
|
+
comma_delimiter = "[[debgraph magic comma]]\n"
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
def _get_dpkg_stdout(cls):
|
|
240
|
+
# dpkg-query doesn't have a way of escaping CSV, so we use magic strings
|
|
241
|
+
# in order to produce machine-readable output.
|
|
242
|
+
|
|
243
|
+
cmd = [
|
|
244
|
+
"dpkg-query",
|
|
245
|
+
"--show",
|
|
246
|
+
"--showformat",
|
|
247
|
+
cls.start_entry_delimiter
|
|
248
|
+
+ cls.comma_delimiter.join(
|
|
249
|
+
("${" + field + "}" for field in Package.get_all_dpkg_fields())
|
|
250
|
+
),
|
|
251
|
+
]
|
|
252
|
+
result = subprocess.run(
|
|
253
|
+
cmd,
|
|
254
|
+
stdout=subprocess.PIPE,
|
|
255
|
+
stderr=subprocess.PIPE,
|
|
256
|
+
text=True,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if result.returncode != 0:
|
|
260
|
+
print(f"Failed: {result.returncode} {result.stderr}", file=sys.stderr)
|
|
261
|
+
|
|
262
|
+
return result.stdout
|
|
263
|
+
|
|
264
|
+
@classmethod
|
|
265
|
+
def _parse_dpkg_stdout(cls, s: str, long_fields: bool) -> List[Package]:
|
|
266
|
+
i = 0
|
|
267
|
+
packages = {}
|
|
268
|
+
while i < len(s):
|
|
269
|
+
next_idx = s.find(cls.start_entry_delimiter, i)
|
|
270
|
+
if next_idx == -1:
|
|
271
|
+
next_idx = len(s)
|
|
272
|
+
package_entry = s[i:next_idx]
|
|
273
|
+
values = package_entry.split(cls.comma_delimiter)
|
|
274
|
+
if len(values) == len(Package.get_all_dpkg_fields()):
|
|
275
|
+
dict_ = {
|
|
276
|
+
field: val
|
|
277
|
+
for field, val in zip(Package.get_all_dpkg_fields(), values)
|
|
278
|
+
}
|
|
279
|
+
package = Package._from_dict(dict_, long_fields=long_fields)
|
|
280
|
+
if package.name in packages:
|
|
281
|
+
raise DebgraphError(
|
|
282
|
+
f"Found duplicate packages: {package}, {packages[package.name]}",
|
|
283
|
+
1,
|
|
284
|
+
)
|
|
285
|
+
packages[package.name] = package
|
|
286
|
+
|
|
287
|
+
i = next_idx + len(cls.start_entry_delimiter)
|
|
288
|
+
|
|
289
|
+
cls._postprocess_packages(packages)
|
|
290
|
+
return list(packages.values())
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def _postprocess_packages(cls, packages: Dict[str, Package]):
|
|
294
|
+
providers = collections.defaultdict(list)
|
|
295
|
+
for package in packages.values():
|
|
296
|
+
for provided in package.provides:
|
|
297
|
+
providers[provided.name].append(package)
|
|
298
|
+
|
|
299
|
+
# for each dependency find the one(s) that actually provide it
|
|
300
|
+
for package in packages.values():
|
|
301
|
+
for alt in package.dependencies:
|
|
302
|
+
for requested in alt.alts:
|
|
303
|
+
if requested.name in packages:
|
|
304
|
+
alt.register_actual(packages[requested.name])
|
|
305
|
+
if requested.name in providers:
|
|
306
|
+
alt.register_actual(providers[requested.name][0])
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class Options:
|
|
310
|
+
def __init__(
|
|
311
|
+
self,
|
|
312
|
+
use_fixed_dates: bool = False,
|
|
313
|
+
override_input_stream: Optional[str] = None,
|
|
314
|
+
argv: Optional[List[str]] = None,
|
|
315
|
+
):
|
|
316
|
+
self.use_fixed_dates = use_fixed_dates
|
|
317
|
+
self.override_input_stream = override_input_stream
|
|
318
|
+
self.argv = argv
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class GraphFileWriter:
|
|
322
|
+
@staticmethod
|
|
323
|
+
def _write_jsonl(
|
|
324
|
+
fout: io.TextIOWrapper, packages: Iterable[Package], options: Options
|
|
325
|
+
):
|
|
326
|
+
fout.write("\n".join(map(str, packages)))
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def _write_dotfile(
|
|
330
|
+
fout: io.TextIOWrapper, packages: Iterable[Package], options: Options
|
|
331
|
+
):
|
|
332
|
+
dot = graphviz.Digraph("Debian")
|
|
333
|
+
|
|
334
|
+
for package in packages:
|
|
335
|
+
dot.node(package.name, label=package.name, _attributes=package.extra)
|
|
336
|
+
|
|
337
|
+
for package in packages:
|
|
338
|
+
for alt in package.dependencies:
|
|
339
|
+
for actual in alt.actuals:
|
|
340
|
+
dot.edge(package.name, actual.name, label=str(alt.alts))
|
|
341
|
+
|
|
342
|
+
fout.write(dot.source)
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def _write_gexf(
|
|
346
|
+
fout: io.TextIOWrapper, packages: Iterable[Package], options: Options
|
|
347
|
+
):
|
|
348
|
+
today_iso = (
|
|
349
|
+
"2026-06-30"
|
|
350
|
+
if options.use_fixed_dates
|
|
351
|
+
else date.today().strftime("%Y-%m-%d")
|
|
352
|
+
)
|
|
353
|
+
creator = "debgraph"
|
|
354
|
+
description = "A graph of apt packages on a Debian system."
|
|
355
|
+
nodes = []
|
|
356
|
+
edges = []
|
|
357
|
+
node_attributes = []
|
|
358
|
+
edge_attributes = []
|
|
359
|
+
|
|
360
|
+
field_to_index = {
|
|
361
|
+
field: idx
|
|
362
|
+
for idx, (field, _) in enumerate(Package.get_all_extra_output_fields())
|
|
363
|
+
}
|
|
364
|
+
for idx, (field, type_) in enumerate(Package.get_all_extra_output_fields()):
|
|
365
|
+
node_attributes.append(
|
|
366
|
+
f""" <attribute id="{idx}" title="{field}" type="{type_}"/>"""
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
edge_attributes.append(
|
|
370
|
+
f""" <attribute id="0" title="alts" type="string"/>"""
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
for package in packages:
|
|
374
|
+
attvalues = []
|
|
375
|
+
for field, value in package.extra.items():
|
|
376
|
+
attvalues.append(
|
|
377
|
+
f""" <attvalue for="{field_to_index[field]}" value={quoteattr(value)} />"""
|
|
378
|
+
)
|
|
379
|
+
nodes.append(
|
|
380
|
+
f""" <node id="{package.id}" label={quoteattr(package.name)}>
|
|
381
|
+
<attvalues>
|
|
382
|
+
{_sep.join(attvalues)}
|
|
383
|
+
</attvalues>
|
|
384
|
+
</node>"""
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
for package in packages:
|
|
388
|
+
for alt in package.dependencies:
|
|
389
|
+
for actual in alt.actuals:
|
|
390
|
+
attvalues = []
|
|
391
|
+
attvalues.append(
|
|
392
|
+
f""" <attvalue for="0" value={quoteattr(str(alt.alts))} />"""
|
|
393
|
+
)
|
|
394
|
+
edges.append(
|
|
395
|
+
f""" <edge source="{package.id}" target="{actual.id}">
|
|
396
|
+
<attvalues>
|
|
397
|
+
{_sep.join(attvalues)}
|
|
398
|
+
</attvalues>
|
|
399
|
+
</edge>"""
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
xml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
403
|
+
<gexf xmlns="http://www.gexf.net/1.3" version="1.3" xmlns:viz="http://www.gexf.net/1.3/viz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gexf.net/1.3 http://www.gexf.net/1.3/gexf.xsd">
|
|
404
|
+
<meta lastmodifieddate="{today_iso}">
|
|
405
|
+
<creator>{escape(creator)}</creator>
|
|
406
|
+
<description>{escape(description)}</description>
|
|
407
|
+
</meta>
|
|
408
|
+
|
|
409
|
+
<graph defaultedgetype="directed" idtype="string" type="static">
|
|
410
|
+
<attributes class="node">
|
|
411
|
+
{_sep.join(node_attributes)}
|
|
412
|
+
</attributes>
|
|
413
|
+
|
|
414
|
+
<attributes class="edge">
|
|
415
|
+
{_sep.join(edge_attributes)}
|
|
416
|
+
</attributes>
|
|
417
|
+
|
|
418
|
+
<nodes count="{len(nodes)}">
|
|
419
|
+
{_sep.join(nodes)}
|
|
420
|
+
</nodes>
|
|
421
|
+
|
|
422
|
+
<edges>
|
|
423
|
+
{_sep.join(edges)}
|
|
424
|
+
</edges>
|
|
425
|
+
</graph>
|
|
426
|
+
</gexf>"""
|
|
427
|
+
|
|
428
|
+
fout.write(xml)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
_supported_formats = {
|
|
432
|
+
"dot": GraphFileWriter._write_dotfile,
|
|
433
|
+
"gexf": GraphFileWriter._write_gexf,
|
|
434
|
+
"jsonl": GraphFileWriter._write_jsonl,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _infer_format(filename: str):
|
|
439
|
+
_, ext = os.path.splitext(filename)
|
|
440
|
+
format_str = ext[len(os.path.extsep) :].lower()
|
|
441
|
+
if format_str in _supported_formats:
|
|
442
|
+
return format_str
|
|
443
|
+
else:
|
|
444
|
+
raise DebgraphError(
|
|
445
|
+
f"Could not infer format from {filename} with extension {ext.lower()}, please specify it explictly using -t.",
|
|
446
|
+
1,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def run_debgraph(options: Options):
|
|
451
|
+
ap = argparse.ArgumentParser("debgraph")
|
|
452
|
+
ap.add_argument(
|
|
453
|
+
"output", nargs="?", default="debian.dot", help="Name of the output file"
|
|
454
|
+
)
|
|
455
|
+
ap.add_argument(
|
|
456
|
+
"-t",
|
|
457
|
+
"--format",
|
|
458
|
+
required=False,
|
|
459
|
+
choices=_supported_formats.keys(),
|
|
460
|
+
help="Output format, can be inferred from --output.",
|
|
461
|
+
)
|
|
462
|
+
ap.add_argument(
|
|
463
|
+
"-v",
|
|
464
|
+
"--version",
|
|
465
|
+
help="Print version and exit.",
|
|
466
|
+
action="store_true",
|
|
467
|
+
)
|
|
468
|
+
ap.add_argument(
|
|
469
|
+
"-l",
|
|
470
|
+
"--long",
|
|
471
|
+
help="Include long fields (Description) in the graph",
|
|
472
|
+
action="store_true",
|
|
473
|
+
)
|
|
474
|
+
args = ap.parse_args(options.argv)
|
|
475
|
+
|
|
476
|
+
if args.version:
|
|
477
|
+
raise DebgraphError(f"Debgraph {__version__}", 0)
|
|
478
|
+
|
|
479
|
+
# reset numbering for this graph
|
|
480
|
+
Package._id = 1
|
|
481
|
+
|
|
482
|
+
output_abs_path = os.path.abspath(args.output)
|
|
483
|
+
dirname, filename = os.path.split(output_abs_path)
|
|
484
|
+
|
|
485
|
+
format = args.format
|
|
486
|
+
if format is None:
|
|
487
|
+
format = _infer_format(filename)
|
|
488
|
+
print(f"Using format {format}", file=sys.stderr)
|
|
489
|
+
write_fn = _supported_formats[format]
|
|
490
|
+
|
|
491
|
+
s = (
|
|
492
|
+
DpkgReader._get_dpkg_stdout()
|
|
493
|
+
if not options.override_input_stream
|
|
494
|
+
else options.override_input_stream
|
|
495
|
+
)
|
|
496
|
+
packages = DpkgReader._parse_dpkg_stdout(s, long_fields=args.long)
|
|
497
|
+
|
|
498
|
+
os.makedirs(dirname, exist_ok=True)
|
|
499
|
+
with open(output_abs_path, "w") as fout:
|
|
500
|
+
write_fn(fout, packages, options)
|
|
501
|
+
|
|
502
|
+
print(f"Finished writing output to {output_abs_path}")
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def main():
|
|
506
|
+
try:
|
|
507
|
+
options = Options()
|
|
508
|
+
run_debgraph(options)
|
|
509
|
+
except DebgraphError as e:
|
|
510
|
+
print(e.message, file=sys.stdout if e.posix_status == 0 else sys.stderr)
|
|
511
|
+
sys.exit(e.posix_status)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
if __name__ == "__main__":
|
|
515
|
+
main()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: debgraph
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Generates a graph of your debian packages
|
|
5
|
+
License-Expression: GPL-3.0-only
|
|
6
|
+
Project-URL: Homepage, https://github.com/garyg1/debgraph
|
|
7
|
+
Project-URL: Issues, https://github.com/garyg1/debgraph/issues
|
|
8
|
+
Requires-Python: >=3.7
|
|
9
|
+
Description-Content-Type: text/plain
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: COPYING
|
|
12
|
+
Requires-Dist: graphviz
|
|
13
|
+
Provides-Extra: tests
|
|
14
|
+
Requires-Dist: pytest; extra == "tests"
|
|
15
|
+
Requires-Dist: black; extra == "tests"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
Debgraph, a program like debtree to view all the Debian packages on your system, in a single graph.
|
|
19
|
+
|
|
20
|
+
Supports GEXF, DOT (Graphviz), and JSON-Lines output.
|
|
21
|
+
|
|
22
|
+
In development. Please note that API may change across 0.x versions.
|
|
23
|
+
|
|
24
|
+
License
|
|
25
|
+
See COPYING
|
|
26
|
+
|
|
27
|
+
Installation
|
|
28
|
+
pip install debgraph
|
|
29
|
+
|
|
30
|
+
Usage
|
|
31
|
+
DOT Example
|
|
32
|
+
debgraph
|
|
33
|
+
|
|
34
|
+
head debian.dot
|
|
35
|
+
|
|
36
|
+
digraph Debian {
|
|
37
|
+
"adduser" [label="adduser"];
|
|
38
|
+
"adwaita-icon-theme" [label="adwaita-icon-theme"];
|
|
39
|
+
"alsa-topology-conf" [label="alsa-topology-conf"];
|
|
40
|
+
"alsa-ucm-conf" [label="alsa-ucm-conf"];
|
|
41
|
+
"apparmor" [label="apparmor"];
|
|
42
|
+
"apport" [label="apport"];
|
|
43
|
+
"apport-core-dump-handler" [label="apport-core-dump-handler"];
|
|
44
|
+
"apport-symptoms" [label="apport-symptoms"];
|
|
45
|
+
"appstream" [label="appstream"];
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
GEXF Example
|
|
49
|
+
debgraph debian.gexf
|
|
50
|
+
|
|
51
|
+
head debian.gexf
|
|
52
|
+
|
|
53
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
54
|
+
<gexf xmlns="http://www.gexf.net/1.3" version="1.3" xmlns:viz="http://www.gexf.net/1.3/viz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gexf.net/1.3 http://www.gexf.net/1.3/gexf.xsd">
|
|
55
|
+
<meta lastmodifieddate="2026-06-30">
|
|
56
|
+
<creator>debgraph</creator>
|
|
57
|
+
<description>A graph of apt packages on a Debian system.</description>
|
|
58
|
+
</meta>
|
|
59
|
+
<graph defaultedgetype="directed" idtype="string" type="static">
|
|
60
|
+
|
|
61
|
+
<attributes class="node">
|
|
62
|
+
<attribute id="0" title="binary:Synopsis" type="string"/>
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
JSON-Lines Example
|
|
66
|
+
debgraph debian.jsonl
|
|
67
|
+
|
|
68
|
+
head debian.jsonl
|
|
69
|
+
|
|
70
|
+
{"id": 1, "name": "adduser", "version": "3.153ubuntu1", "dependencies": ... }
|
|
71
|
+
{"id": 2, "name": "adwaita-icon-theme", "version": "50.0-1", "dependencies": ... }
|
|
72
|
+
{"id": 3, "name": "alsa-topology-conf", "version": "1.2.5.1-3build1", "dependencies": ... }
|
|
73
|
+
{"id": 4, "name": "alsa-ucm-conf", "version": "1.2.15.3-1ubuntu1", "dependencies": ... }
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
Development
|
|
77
|
+
pip install ".[tests]" .
|
|
78
|
+
python -m pytest .
|
|
@@ -2,9 +2,12 @@ COPYING
|
|
|
2
2
|
LICENSE
|
|
3
3
|
README
|
|
4
4
|
pyproject.toml
|
|
5
|
+
debgraph/__init__.py
|
|
5
6
|
debgraph/debgraph.py
|
|
6
7
|
debgraph.egg-info/PKG-INFO
|
|
7
8
|
debgraph.egg-info/SOURCES.txt
|
|
8
9
|
debgraph.egg-info/dependency_links.txt
|
|
9
10
|
debgraph.egg-info/entry_points.txt
|
|
10
|
-
debgraph.egg-info/
|
|
11
|
+
debgraph.egg-info/requires.txt
|
|
12
|
+
debgraph.egg-info/top_level.txt
|
|
13
|
+
tests/test_debgraph.py
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "debgraph"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Generates a graph of your debian packages"
|
|
5
5
|
requires-python = ">=3.7"
|
|
6
|
-
dependencies = []
|
|
6
|
+
dependencies = ["graphviz"]
|
|
7
7
|
license = "GPL-3.0-only"
|
|
8
8
|
license-files = [ "LICENSE", "COPYING" ]
|
|
9
|
-
readme = "README"
|
|
9
|
+
readme = { file = "README", content-type = "text/plain" }
|
|
10
10
|
|
|
11
11
|
[project.urls]
|
|
12
12
|
Homepage = "https://github.com/garyg1/debgraph"
|
|
13
13
|
Issues = "https://github.com/garyg1/debgraph/issues"
|
|
14
14
|
|
|
15
15
|
[project.scripts]
|
|
16
|
-
debgraph = "debgraph.debgraph:main"
|
|
16
|
+
debgraph = "debgraph.debgraph:main"
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
tests = ["pytest", "black"]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
import pytest
|
|
3
|
+
import debgraph
|
|
4
|
+
import pathlib
|
|
5
|
+
import os
|
|
6
|
+
from . import make_fixture
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_parser():
|
|
12
|
+
tests = [
|
|
13
|
+
[
|
|
14
|
+
"apt-transport-https (= 3.2.0)",
|
|
15
|
+
{"name": "apt-transport-https", "op": "=", "version": "3.2.0"},
|
|
16
|
+
],
|
|
17
|
+
["libappstream5 (= 1)", {"name": "libappstream5", "op": "=", "version": "1"}],
|
|
18
|
+
]
|
|
19
|
+
for s, expected in tests:
|
|
20
|
+
assert debgraph.Package._parse_package_ref(s).__dict__ == expected
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _assert_files_equal(file1, file2):
|
|
24
|
+
diff = list(difflib.unified_diff(file1.readlines(), file2.readlines()))
|
|
25
|
+
assert diff == [], "".join(diff)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _test_fixture(fixture: str, *extra_args: str):
|
|
29
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
30
|
+
in_fixture, out_fixtures = make_fixture.get_fixtures(fixture)
|
|
31
|
+
|
|
32
|
+
with open(in_fixture, "r") as in_file:
|
|
33
|
+
s = in_file.read()
|
|
34
|
+
|
|
35
|
+
for expected_fixture, _ in out_fixtures:
|
|
36
|
+
actual_fixture = os.path.join(temp_dir, os.path.split(expected_fixture)[1])
|
|
37
|
+
|
|
38
|
+
debgraph.run_debgraph(
|
|
39
|
+
debgraph.Options(
|
|
40
|
+
use_fixed_dates=True,
|
|
41
|
+
override_input_stream=s,
|
|
42
|
+
argv=[actual_fixture, *extra_args],
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
with open(actual_fixture, "r") as actual, open(
|
|
47
|
+
expected_fixture, "r"
|
|
48
|
+
) as expected:
|
|
49
|
+
_assert_files_equal(actual, expected)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_wsl():
|
|
53
|
+
_test_fixture("wsl")
|
|
54
|
+
_test_fixture("wsl_long", "--long")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_version():
|
|
58
|
+
e = None
|
|
59
|
+
try:
|
|
60
|
+
debgraph.run_debgraph(debgraph.Options(argv=["--version"]))
|
|
61
|
+
except debgraph.DebgraphError as actual:
|
|
62
|
+
e = actual
|
|
63
|
+
|
|
64
|
+
assert e != None and e.message == "Debgraph 0.2.0"
|
debgraph-0.1.0/PKG-INFO
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: debgraph
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Generates a graph of your debian packages
|
|
5
|
-
License-Expression: GPL-3.0-only
|
|
6
|
-
Project-URL: Homepage, https://github.com/garyg1/debgraph
|
|
7
|
-
Project-URL: Issues, https://github.com/garyg1/debgraph/issues
|
|
8
|
-
Requires-Python: >=3.7
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
License-File: COPYING
|
|
11
|
-
Dynamic: license-file
|
|
12
|
-
|
|
13
|
-
Debgraph, a program like debtree to view ALL the Debian packages on your system.
|
|
14
|
-
|
|
15
|
-
License
|
|
16
|
-
See COPYING
|
|
17
|
-
|
|
18
|
-
Installation
|
|
19
|
-
pip install debgraph
|
|
20
|
-
|
|
21
|
-
Usage
|
|
22
|
-
debgraph
|
|
23
|
-
|
|
24
|
-
Output Format
|
|
25
|
-
head debian.dot
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
digraph Debian {
|
|
29
|
-
"adduser" [label="adduser"];
|
|
30
|
-
"adwaita-icon-theme" [label="adwaita-icon-theme"];
|
|
31
|
-
"alsa-topology-conf" [label="alsa-topology-conf"];
|
|
32
|
-
"alsa-ucm-conf" [label="alsa-ucm-conf"];
|
|
33
|
-
"apparmor" [label="apparmor"];
|
|
34
|
-
"apport" [label="apport"];
|
|
35
|
-
"apport-core-dump-handler" [label="apport-core-dump-handler"];
|
|
36
|
-
"apport-symptoms" [label="apport-symptoms"];
|
|
37
|
-
"appstream" [label="appstream"];
|
|
38
|
-
...
|
|
39
|
-
```
|
debgraph-0.1.0/README
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
Debgraph, a program like debtree to view ALL the Debian packages on your system.
|
|
2
|
-
|
|
3
|
-
License
|
|
4
|
-
See COPYING
|
|
5
|
-
|
|
6
|
-
Installation
|
|
7
|
-
pip install debgraph
|
|
8
|
-
|
|
9
|
-
Usage
|
|
10
|
-
debgraph
|
|
11
|
-
|
|
12
|
-
Output Format
|
|
13
|
-
head debian.dot
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
digraph Debian {
|
|
17
|
-
"adduser" [label="adduser"];
|
|
18
|
-
"adwaita-icon-theme" [label="adwaita-icon-theme"];
|
|
19
|
-
"alsa-topology-conf" [label="alsa-topology-conf"];
|
|
20
|
-
"alsa-ucm-conf" [label="alsa-ucm-conf"];
|
|
21
|
-
"apparmor" [label="apparmor"];
|
|
22
|
-
"apport" [label="apport"];
|
|
23
|
-
"apport-core-dump-handler" [label="apport-core-dump-handler"];
|
|
24
|
-
"apport-symptoms" [label="apport-symptoms"];
|
|
25
|
-
"appstream" [label="appstream"];
|
|
26
|
-
...
|
|
27
|
-
```
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
'''
|
|
2
|
-
Debgraph, a program like debtree to view ALL the Debian packages on your system.
|
|
3
|
-
Copyright (C) 2026 Gary Gurlaskie
|
|
4
|
-
|
|
5
|
-
This program is free software: you can redistribute it and/or modify
|
|
6
|
-
it under the terms of the GNU General Public License, version 3 as
|
|
7
|
-
published by the Free Software Foundation.
|
|
8
|
-
|
|
9
|
-
This program is distributed in the hope that it will be useful,
|
|
10
|
-
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
-
GNU General Public License for more details.
|
|
13
|
-
|
|
14
|
-
You should have received a copy of the GNU General Public License
|
|
15
|
-
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
-
|
|
17
|
-
'''
|
|
18
|
-
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import collections
|
|
22
|
-
import subprocess
|
|
23
|
-
import sys
|
|
24
|
-
import csv
|
|
25
|
-
import io
|
|
26
|
-
import re
|
|
27
|
-
import json
|
|
28
|
-
from typing import List, Optional, Dict
|
|
29
|
-
|
|
30
|
-
class GenericEncoder(json.JSONEncoder):
|
|
31
|
-
def default(self, obj):
|
|
32
|
-
return obj.__dict__
|
|
33
|
-
|
|
34
|
-
class PackageRef:
|
|
35
|
-
def __init__(self, name, op=None, version=None):
|
|
36
|
-
self.name = name
|
|
37
|
-
self.op = op
|
|
38
|
-
self.version = version
|
|
39
|
-
|
|
40
|
-
def __repr__(self):
|
|
41
|
-
return json.dumps(self.__dict__, cls=GenericEncoder)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class PackageDependencyAlt:
|
|
45
|
-
def __init__(self, alts: List[PackageRef]):
|
|
46
|
-
self.alts = alts
|
|
47
|
-
self.actual: Optional[Package] = None
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class Package:
|
|
51
|
-
fields = [
|
|
52
|
-
'binary:Package',
|
|
53
|
-
'Version',
|
|
54
|
-
'Depends',
|
|
55
|
-
'Provides',
|
|
56
|
-
'Maintainer',
|
|
57
|
-
]
|
|
58
|
-
_pkgref_re = r"(?P<name>[a-zA-Z0-9\+\-\._]+)\s*(\(\s*(?P<op>(\=|\>\=|\>\>|\<\=|\<\<))\s*(?P<version>[^\)]+)\s*\))?"
|
|
59
|
-
|
|
60
|
-
def __init__(self):
|
|
61
|
-
self.name: Optional[str] = None
|
|
62
|
-
self.version: Optional[str] = None
|
|
63
|
-
self.dependencies: List[PackageDependencyAlt] = []
|
|
64
|
-
self.provides: List[PackageRef] = []
|
|
65
|
-
self.maintainer: Optional[str] = None
|
|
66
|
-
|
|
67
|
-
@classmethod
|
|
68
|
-
def from_dict(cls, dict):
|
|
69
|
-
new = Package()
|
|
70
|
-
new.name = dict['binary:Package']
|
|
71
|
-
new.version= dict['Version']
|
|
72
|
-
new.dependencies = list(map(PackageDependencyAlt, cls.parse_package_refs(dict['Depends'])))
|
|
73
|
-
new.provides = cls.flatten(cls.parse_package_refs(dict['Provides']))
|
|
74
|
-
new.maintainer = dict['Maintainer']
|
|
75
|
-
return new
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@classmethod
|
|
79
|
-
def parse_package_ref(cls, raw_ref):
|
|
80
|
-
"""Parses `<name> (<op> <version>)"""
|
|
81
|
-
m = re.match(cls._pkgref_re, raw_ref)
|
|
82
|
-
if m:
|
|
83
|
-
return PackageRef(m.group('name'), m.group('op'), m.group('version'))
|
|
84
|
-
else:
|
|
85
|
-
return None
|
|
86
|
-
|
|
87
|
-
@classmethod
|
|
88
|
-
def parse_package_ref_alt(cls, raw_ref_alt):
|
|
89
|
-
"""Parses <pkgref> | <pkgref> | ..."""
|
|
90
|
-
ref_alt = raw_ref_alt.split('|')
|
|
91
|
-
alts = []
|
|
92
|
-
for raw in ref_alt:
|
|
93
|
-
raw = raw.strip()
|
|
94
|
-
if raw:
|
|
95
|
-
alts.append(cls.parse_package_ref(raw))
|
|
96
|
-
return alts
|
|
97
|
-
|
|
98
|
-
@classmethod
|
|
99
|
-
def parse_package_refs(cls, raw_str):
|
|
100
|
-
"""Parses `<pkgrefmany>, <pkgrefmany>, ...`"""
|
|
101
|
-
raw_refs = raw_str.split(',')
|
|
102
|
-
refs = []
|
|
103
|
-
for raw_ref in raw_refs:
|
|
104
|
-
raw_ref = raw_ref.strip()
|
|
105
|
-
if raw_ref:
|
|
106
|
-
ref = cls.parse_package_ref_alt(raw_ref)
|
|
107
|
-
refs.append(ref)
|
|
108
|
-
return refs
|
|
109
|
-
|
|
110
|
-
@staticmethod
|
|
111
|
-
def flatten(l):
|
|
112
|
-
return [x for sublist in l for x in sublist]
|
|
113
|
-
|
|
114
|
-
def __repr__(self):
|
|
115
|
-
return json.dumps(self.__dict__, cls=GenericEncoder)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def main():
|
|
120
|
-
result = subprocess.run(
|
|
121
|
-
['dpkg-query', '--show', '--showformat', ','.join(('"${' + field + '}"' for field in Package.fields )) + '\n'],
|
|
122
|
-
stdout=subprocess.PIPE,
|
|
123
|
-
stderr=subprocess.PIPE,
|
|
124
|
-
text=True
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
if result.returncode != 0:
|
|
128
|
-
print(f"Failed: {result.returncode} {result.stderr}")
|
|
129
|
-
sys.exit(1)
|
|
130
|
-
|
|
131
|
-
reader = csv.DictReader(io.StringIO(result.stdout), fieldnames=Package.fields)
|
|
132
|
-
|
|
133
|
-
packages: Dict[str, Package] = {}
|
|
134
|
-
for dict_ in reader:
|
|
135
|
-
package = Package.from_dict(dict_)
|
|
136
|
-
assert package.name not in packages
|
|
137
|
-
packages[package.name] = package
|
|
138
|
-
|
|
139
|
-
providers = collections.defaultdict(list)
|
|
140
|
-
for package in packages.values():
|
|
141
|
-
for provided in package.provides:
|
|
142
|
-
providers[provided.name].append(package)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
# for each dependency find which actually provides it
|
|
146
|
-
for package in packages.values():
|
|
147
|
-
for alt in package.dependencies:
|
|
148
|
-
for requested in alt.alts:
|
|
149
|
-
if requested.name in packages:
|
|
150
|
-
alt.actual = packages[requested.name]
|
|
151
|
-
break
|
|
152
|
-
if requested.name in providers:
|
|
153
|
-
alt.actual = providers[requested.name][0]
|
|
154
|
-
break
|
|
155
|
-
|
|
156
|
-
# render the dotfile
|
|
157
|
-
# graphviz library tries to position them which is not useful
|
|
158
|
-
# so write to stdout
|
|
159
|
-
output = [
|
|
160
|
-
"digraph Debian {"
|
|
161
|
-
]
|
|
162
|
-
|
|
163
|
-
for package in packages.values():
|
|
164
|
-
output.append(f'"{package.name}" [label="{package.name}"];')
|
|
165
|
-
|
|
166
|
-
for package in packages.values():
|
|
167
|
-
for alt in package.dependencies:
|
|
168
|
-
if alt.actual is not None:
|
|
169
|
-
output.append(f'"{package.name}" -> "{alt.actual.name}";')
|
|
170
|
-
|
|
171
|
-
output.append("}")
|
|
172
|
-
|
|
173
|
-
with open('debian.dot', 'w') as fout:
|
|
174
|
-
fout.write('\n'.join(output))
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def test():
|
|
179
|
-
tests = [
|
|
180
|
-
'apt-transport-https (= 3.2.0)',
|
|
181
|
-
'libappstream5 (= 1)',
|
|
182
|
-
]
|
|
183
|
-
for test in tests:
|
|
184
|
-
print(parse_package_ref(test))
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if __name__ == '__main__':
|
|
188
|
-
main()
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: debgraph
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Generates a graph of your debian packages
|
|
5
|
-
License-Expression: GPL-3.0-only
|
|
6
|
-
Project-URL: Homepage, https://github.com/garyg1/debgraph
|
|
7
|
-
Project-URL: Issues, https://github.com/garyg1/debgraph/issues
|
|
8
|
-
Requires-Python: >=3.7
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
License-File: COPYING
|
|
11
|
-
Dynamic: license-file
|
|
12
|
-
|
|
13
|
-
Debgraph, a program like debtree to view ALL the Debian packages on your system.
|
|
14
|
-
|
|
15
|
-
License
|
|
16
|
-
See COPYING
|
|
17
|
-
|
|
18
|
-
Installation
|
|
19
|
-
pip install debgraph
|
|
20
|
-
|
|
21
|
-
Usage
|
|
22
|
-
debgraph
|
|
23
|
-
|
|
24
|
-
Output Format
|
|
25
|
-
head debian.dot
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
digraph Debian {
|
|
29
|
-
"adduser" [label="adduser"];
|
|
30
|
-
"adwaita-icon-theme" [label="adwaita-icon-theme"];
|
|
31
|
-
"alsa-topology-conf" [label="alsa-topology-conf"];
|
|
32
|
-
"alsa-ucm-conf" [label="alsa-ucm-conf"];
|
|
33
|
-
"apparmor" [label="apparmor"];
|
|
34
|
-
"apport" [label="apport"];
|
|
35
|
-
"apport-core-dump-handler" [label="apport-core-dump-handler"];
|
|
36
|
-
"apport-symptoms" [label="apport-symptoms"];
|
|
37
|
-
"appstream" [label="appstream"];
|
|
38
|
-
...
|
|
39
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|