1import collections
2import dataclasses
3import functools
4import json
5import pathlib
6import subprocess
7
8import yaml
9
10class DataclassEncoder(json.JSONEncoder):
11 def default(self, it):
12 if dataclasses.is_dataclass(it):
13 return dataclasses.asdict(it)
14 return super().default(it)
15
16
17@dataclasses.dataclass
18class Project:
19 name: str
20 description: str | None
21 project_path: str
22 repo_path: str | None
23
24 def __hash__(self) -> int:
25 return hash(self.name)
26
27 @classmethod
28 def from_yaml(cls, path: pathlib.Path):
29 data = yaml.safe_load(path.open())
30 return cls(
31 name=data["identifier"],
32 description=data["description"],
33 project_path=data["projectpath"],
34 repo_path=data["repopath"]
35 )
36
37
38def get_git_commit(path: pathlib.Path):
39 return subprocess.check_output(["git", "-C", path, "rev-parse", "--short", "HEAD"]).decode().strip()
40
41
42def validate_unique(projects: list[Project], attr: str):
43 seen = set()
44 for item in projects:
45 attr_value = getattr(item, attr)
46 if attr_value in seen:
47 raise Exception(f"Duplicate {attr}: {attr_value}")
48 seen.add(attr_value)
49
50
51THIRD_PARTY = {
52 "third-party/appstream": "appstream-qt",
53 "third-party/cmark": "cmark",
54 "third-party/gpgme": "gpgme",
55 "third-party/kdsoap": "kdsoap",
56 "third-party/libaccounts-qt": "accounts-qt",
57 "third-party/libgpg-error": "libgpg-error",
58 "third-party/libquotient": "libquotient",
59 "third-party/packagekit-qt": "packagekit-qt",
60 "third-party/poppler": "poppler",
61 "third-party/qcoro": "qcoro",
62 "third-party/qmltermwidget": "qmltermwidget",
63 "third-party/qtkeychain": "qtkeychain",
64 "third-party/signond": "signond",
65 "third-party/taglib": "taglib",
66 "third-party/wayland-protocols": "wayland-protocols",
67 "third-party/wayland": "wayland",
68 "third-party/zxing-cpp": "zxing-cpp",
69}
70
71IGNORE = {
72 "kdesupport/phonon-directshow",
73 "kdesupport/phonon-mmf",
74 "kdesupport/phonon-mplayer",
75 "kdesupport/phonon-quicktime",
76 "kdesupport/phonon-waveout",
77 "kdesupport/phonon-xine"
78}
79
80WARNED = set()
81
82
83@dataclasses.dataclass
84class KDERepoMetadata:
85 version: str
86 projects: list[Project]
87 dep_graph: dict[Project, set[Project]]
88
89 @functools.cached_property
90 def projects_by_name(self):
91 return {p.name: p for p in self.projects}
92
93 @functools.cached_property
94 def projects_by_path(self):
95 return {p.project_path: p for p in self.projects}
96
97 def try_lookup_package(self, path):
98 if path in IGNORE:
99 return None
100 project = self.projects_by_path.get(path)
101 if project is None and path not in WARNED:
102 WARNED.add(path)
103 print(f"Warning: unknown project {path}")
104 return project
105
106 @classmethod
107 def from_repo_metadata_checkout(cls, repo_metadata: pathlib.Path, unstable=False):
108 projects = [
109 Project.from_yaml(metadata_file)
110 for metadata_file in repo_metadata.glob("projects-invent/**/metadata.yaml")
111 ] + [
112 Project(id, None, project_path, None)
113 for project_path, id in THIRD_PARTY.items()
114 ]
115
116 validate_unique(projects, "name")
117 validate_unique(projects, "project_path")
118
119 self = cls(
120 version=get_git_commit(repo_metadata),
121 projects=projects,
122 dep_graph={},
123 )
124
125 dep_graph = collections.defaultdict(set)
126
127 if unstable:
128 spec_name = "dependency-data-kf6-qt6"
129 else:
130 spec_name = "dependency-data-stable-kf6-qt6"
131
132 spec_path = repo_metadata / "dependencies" / spec_name
133 for line in spec_path.open():
134 line = line.strip()
135 if line.startswith("#"):
136 continue
137 if not line:
138 continue
139
140 dependent, dependency = line.split(": ")
141
142 dependent = self.try_lookup_package(dependent)
143 if dependent is None:
144 continue
145
146 dependency = self.try_lookup_package(dependency)
147 if dependency is None:
148 continue
149
150 dep_graph[dependent].add(dependency)
151
152 self.dep_graph = dep_graph
153
154 return self
155
156 def write_json(self, root: pathlib.Path):
157 root.mkdir(parents=True, exist_ok=True)
158
159 with (root / "projects.json").open("w") as fd:
160 json.dump(self.projects_by_name, fd, cls=DataclassEncoder, sort_keys=True, indent=2)
161
162 with (root / "dependencies.json").open("w") as fd:
163 deps = {k.name: sorted(dep.name for dep in v) for k, v in self.dep_graph.items()}
164 json.dump({"version": self.version, "dependencies": deps}, fd, cls=DataclassEncoder, sort_keys=True, indent=2)
165
166 @classmethod
167 def from_json(cls, root: pathlib.Path):
168 projects = [
169 Project(**v) for v in json.load((root / "projects.json").open()).values()
170 ]
171
172 deps = json.load((root / "dependencies.json").open())
173 self = cls(
174 version=deps["version"],
175 projects=projects,
176 dep_graph={},
177 )
178
179 dep_graph = collections.defaultdict(set)
180 for dependent, dependencies in deps["dependencies"].items():
181 for dependency in dependencies:
182 dep_graph[self.projects_by_name[dependent]].add(self.projects_by_name[dependency])
183
184 self.dep_graph = dep_graph
185 return self