Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
at 24.05-beta 182 lines 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): 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_specs = ["dependency-data-stable-kf6-qt6"] 126 dep_graph = collections.defaultdict(set) 127 128 for spec in dep_specs: 129 spec_path = repo_metadata / "dependencies" / spec 130 for line in spec_path.open(): 131 line = line.strip() 132 if line.startswith("#"): 133 continue 134 if not line: 135 continue 136 137 dependent, dependency = line.split(": ") 138 139 dependent = self.try_lookup_package(dependent) 140 if dependent is None: 141 continue 142 143 dependency = self.try_lookup_package(dependency) 144 if dependency is None: 145 continue 146 147 dep_graph[dependent].add(dependency) 148 149 self.dep_graph = dep_graph 150 151 return self 152 153 def write_json(self, root: pathlib.Path): 154 root.mkdir(parents=True, exist_ok=True) 155 156 with (root / "projects.json").open("w") as fd: 157 json.dump(self.projects_by_name, fd, cls=DataclassEncoder, sort_keys=True, indent=2) 158 159 with (root / "dependencies.json").open("w") as fd: 160 deps = {k.name: sorted(dep.name for dep in v) for k, v in self.dep_graph.items()} 161 json.dump({"version": self.version, "dependencies": deps}, fd, cls=DataclassEncoder, sort_keys=True, indent=2) 162 163 @classmethod 164 def from_json(cls, root: pathlib.Path): 165 projects = [ 166 Project(**v) for v in json.load((root / "projects.json").open()).values() 167 ] 168 169 deps = json.load((root / "dependencies.json").open()) 170 self = cls( 171 version=deps["version"], 172 projects=projects, 173 dep_graph={}, 174 ) 175 176 dep_graph = collections.defaultdict(set) 177 for dependent, dependencies in deps["dependencies"].items(): 178 for dependency in dependencies: 179 dep_graph[self.projects_by_name[dependent]].add(self.projects_by_name[dependency]) 180 181 self.dep_graph = dep_graph 182 return self