1#!/usr/bin/env nix-shell
2#!nix-shell -i python3 -p "python3.withPackages(ps: [ ps.beautifulsoup4 ps.click ps.httpx ps.jinja2 ps.packaging ps.pyyaml ])" nix-update
3import base64
4import binascii
5import hashlib
6import json
7import pathlib
8import subprocess
9from urllib.parse import urljoin, urlparse
10
11import bs4
12import click
13import httpx
14import jinja2
15import packaging.version as v
16
17import utils
18
19
20LEAF_TEMPLATE = jinja2.Template('''
21{ mkKdeDerivation }:
22mkKdeDerivation {
23 pname = "{{ pname }}";
24}
25'''.strip())
26
27ROOT_TEMPLATE = jinja2.Template('''
28{ callPackage }:
29{
30 {%- for p in packages %}
31 {{ p }} = callPackage ./{{ p }} { };
32 {%- endfor %}
33}
34'''.strip())
35
36PROJECTS_WITH_RUST = {
37 "akonadi-search",
38 "angelfish",
39 "kdepim-addons",
40}
41
42def to_sri(hash):
43 raw = binascii.unhexlify(hash)
44 b64 = base64.b64encode(raw).decode()
45 return f"sha256-{b64}"
46
47
48@click.command
49@click.argument(
50 "pkgset",
51 type=click.Choice(["frameworks", "gear", "plasma"]),
52 required=True
53)
54@click.argument(
55 "version",
56 type=str,
57 required=True
58)
59@click.option(
60 "--nixpkgs",
61 type=click.Path(
62 exists=True,
63 file_okay=False,
64 resolve_path=True,
65 writable=True,
66 path_type=pathlib.Path,
67 ),
68 default=pathlib.Path(__file__).parent.parent.parent.parent
69)
70@click.option(
71 "--sources-url",
72 type=str,
73 default=None,
74)
75def main(pkgset: str, version: str, nixpkgs: pathlib.Path, sources_url: str | None):
76 root_dir = nixpkgs / "pkgs/kde"
77 set_dir = root_dir / pkgset
78 generated_dir = root_dir / "generated"
79 metadata = utils.KDERepoMetadata.from_json(generated_dir)
80
81 if sources_url is None:
82 set_url = {
83 "frameworks": f"frameworks/{version}/",
84 "gear": f"release-service/{version}/src/",
85 "plasma": f"plasma/{version}/",
86 }[pkgset]
87 sources_url = f"https://download.kde.org/stable/{set_url}"
88
89 client = httpx.Client()
90 sources = client.get(sources_url)
91 sources.raise_for_status()
92 bs = bs4.BeautifulSoup(sources.text, features="html.parser")
93
94 results = {}
95 projects_to_update_rust = set()
96 for item in bs.select("tr")[3:]:
97 link = item.select_one("td:nth-child(2) a")
98 if not link:
99 continue
100
101 project_name, version_and_ext = link.text.rsplit("-", maxsplit=1)
102
103 if project_name not in metadata.projects_by_name:
104 print(f"Warning: unknown tarball: {project_name}")
105
106 if project_name in PROJECTS_WITH_RUST:
107 projects_to_update_rust.add(project_name)
108
109 if version_and_ext.endswith(".sig"):
110 continue
111
112 version = version_and_ext.removesuffix(".tar.xz")
113
114 url = urljoin(sources_url, link.attrs["href"])
115
116 hash = client.get(url + ".sha256").text.strip()
117
118 if hash == "Hash type not supported":
119 print(f"{url} missing hash on CDN, downloading...")
120 hasher = hashlib.sha256()
121 with client.stream("GET", url, follow_redirects=True) as r:
122 for data in r.iter_bytes():
123 hasher.update(data)
124 hash = hasher.hexdigest()
125 else:
126 hash = hash.split(" ", maxsplit=1)[0]
127
128 if existing := results.get(project_name):
129 old_version = existing["version"]
130 if v.parse(old_version) > v.parse(version):
131 print(f"{project_name} {old_version} is newer than {version}, skipping...")
132 continue
133
134 results[project_name] = {
135 "version": version,
136 "url": "mirror://kde" + urlparse(url).path,
137 "hash": to_sri(hash)
138 }
139
140 pkg_dir = set_dir / project_name
141 pkg_file = pkg_dir / "default.nix"
142
143 if not pkg_file.exists():
144 print(f"Generated new package: {pkgset}/{project_name}")
145 pkg_dir.mkdir(parents=True, exist_ok=True)
146 with pkg_file.open("w") as fd:
147 fd.write(LEAF_TEMPLATE.render(pname=project_name) + "\n")
148
149 set_dir.mkdir(parents=True, exist_ok=True)
150 with (set_dir / "default.nix").open("w") as fd:
151 fd.write(ROOT_TEMPLATE.render(packages=sorted(results.keys())) + "\n")
152
153 sources_dir = generated_dir / "sources"
154 sources_dir.mkdir(parents=True, exist_ok=True)
155 with (sources_dir / f"{pkgset}.json").open("w") as fd:
156 json.dump(results, fd, indent=2)
157
158 for project_name in projects_to_update_rust:
159 print(f"Updating cargoDeps hash for {pkgset}/{project_name}...")
160 subprocess.run([
161 "nix-update",
162 f"kdePackages.{project_name}",
163 "--version",
164 "skip",
165 "--override-filename",
166 pkg_file
167 ])
168
169
170if __name__ == "__main__":
171 main() # type: ignore