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