at 25.11-pre 5.5 kB view raw
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