1#!/usr/bin/env nix-shell
2#!nix-shell -i python3 -p "python3.withPackages (ps: [ ps.beautifulsoup4 ps.lxml ps.packaging ])"
3from functools import cached_property
4from itertools import groupby
5import json
6import os
7import pathlib
8import subprocess
9import sys
10import urllib.request
11from dataclasses import dataclass
12from enum import Enum
13
14from bs4 import BeautifulSoup, NavigableString, Tag
15from packaging.version import parse as parse_version, Version
16
17
18HERE = pathlib.Path(__file__).parent
19ROOT = HERE.parent.parent.parent.parent
20VERSIONS_FILE = HERE / "kernels-org.json"
21
22
23class KernelNature(Enum):
24 MAINLINE = 1
25 STABLE = 2
26 LONGTERM = 3
27
28
29@dataclass
30class KernelRelease:
31 nature: KernelNature
32 version: str
33 date: str
34 link: str
35 eol: bool = False
36
37 @cached_property
38 def parsed_version(self) -> Version:
39 return parse_version(self.version)
40
41 @cached_property
42 def branch(self) -> str:
43 version = self.parsed_version
44 # This is a testing kernel.
45 if version.is_prerelease:
46 return "testing"
47 else:
48 return f"{version.major}.{version.minor}"
49
50
51def parse_release(release: Tag) -> KernelRelease | None:
52 columns: list[Tag] = list(release.find_all("td"))
53 try:
54 nature = KernelNature[columns[0].get_text().rstrip(":").upper()]
55 except KeyError:
56 # skip linux-next
57 return None
58
59 version = columns[1].get_text().rstrip(" [EOL]")
60 date = columns[2].get_text()
61 link = columns[3].find("a")
62 if link is not None and isinstance(link, Tag):
63 link = link.attrs.get("href")
64 assert link is not None, f"link for kernel {version} is non-existent"
65 eol = bool(release.find(class_="eolkernel"))
66
67 return KernelRelease(
68 nature=nature,
69 version=version,
70 date=date,
71 link=link,
72 eol=eol,
73 )
74
75
76def get_hash(kernel: KernelRelease):
77 if kernel.branch == "testing":
78 args = ["--unpack"]
79 else:
80 args = []
81
82 hash = (
83 subprocess.check_output(["nix-prefetch-url", kernel.link] + args)
84 .decode()
85 .strip()
86 )
87 return f"sha256:{hash}"
88
89
90def get_oldest_branch(kernels) -> Version:
91 return min(parse_version(v) for v in kernels.keys() if v != "testing")
92
93
94def predates_oldest_branch(oldest: Version, to_compare: str) -> bool:
95 if to_compare == "testing":
96 return False
97
98 return parse_version(to_compare) < oldest
99
100
101def commit(message):
102 return subprocess.check_call(["git", "commit", "-m", message, VERSIONS_FILE])
103
104
105def main():
106 kernel_org = urllib.request.urlopen("https://kernel.org/")
107 soup = BeautifulSoup(kernel_org.read().decode(), "lxml")
108 release_table = soup.find(id="releases")
109 if not release_table or isinstance(release_table, NavigableString):
110 print(release_table, file=sys.stderr)
111 print("Failed to find the release table on https://kernel.org", file=sys.stderr)
112 sys.exit(1)
113
114 releases = release_table.find_all("tr")
115 parsed_releases = [
116 parsed for release in releases
117 if (parsed := parse_release(release)) is not None
118 ]
119 all_kernels = json.load(VERSIONS_FILE.open())
120 oldest_branch = get_oldest_branch(all_kernels)
121
122 for (branch, kernels) in groupby(parsed_releases, lambda kernel: kernel.branch):
123 kernel = max(kernels, key=lambda kernel: kernel.parsed_version)
124 nixpkgs_branch = branch.replace(".", "_")
125
126 old_version = all_kernels.get(branch, {}).get("version")
127 if old_version == kernel.version:
128 print(f"linux_{nixpkgs_branch}: {kernel.version} is latest, skipping...")
129 continue
130
131 if predates_oldest_branch(oldest_branch, kernel.branch):
132 print(
133 f"{kernel.branch} is too old and not supported anymore, skipping...",
134 file=sys.stderr
135 )
136 continue
137
138 if old_version is None:
139 if kernel.eol:
140 print(
141 f"{kernel.branch} is EOL, not adding...",
142 file=sys.stderr
143 )
144 continue
145
146 message = f"linux_{nixpkgs_branch}: init at {kernel.version}"
147 else:
148 message = f"linux_{nixpkgs_branch}: {old_version} -> {kernel.version}"
149
150 print(message, file=sys.stderr)
151
152 all_kernels[branch] = {
153 "version": kernel.version,
154 "hash": get_hash(kernel),
155 "lts": kernel.nature == KernelNature.LONGTERM,
156 }
157
158 with VERSIONS_FILE.open("w") as fd:
159 json.dump(all_kernels, fd, indent=4)
160 fd.write("\n") # makes editorconfig happy
161
162 if os.environ.get("COMMIT") == "1":
163 commit(message)
164
165
166if __name__ == "__main__":
167 main()