1#!/usr/bin/env nix-shell
2#!nix-shell -i python3 -p "python3.withPackages (ps: [ ps.beautifulsoup4 ps.lxml ps.packaging ])"
3import json
4import os
5import pathlib
6import subprocess
7import sys
8import urllib.request
9from dataclasses import dataclass
10from enum import Enum
11
12from bs4 import BeautifulSoup, NavigableString, Tag
13from packaging.version import parse as parse_version, Version
14from typing import List
15
16HERE = pathlib.Path(__file__).parent
17ROOT = HERE.parent.parent.parent.parent
18VERSIONS_FILE = HERE / "kernels-org.json"
19
20
21class KernelNature(Enum):
22 MAINLINE = 1
23 STABLE = 2
24 LONGTERM = 3
25
26
27@dataclass
28class KernelRelease:
29 nature: KernelNature
30 version: str
31 branch: str
32 date: str
33 link: str
34 eol: bool = False
35
36
37def parse_release(release: Tag) -> KernelRelease | None:
38 columns: list[Tag] = list(release.find_all("td"))
39 try:
40 nature = KernelNature[columns[0].get_text().rstrip(":").upper()]
41 except KeyError:
42 return None
43
44 version = columns[1].get_text().rstrip(" [EOL]")
45 date = columns[2].get_text()
46 link = columns[3].find("a")
47 if link is not None and isinstance(link, Tag):
48 link = link.attrs.get("href")
49 assert link is not None, f"link for kernel {version} is non-existent"
50 eol = bool(release.find(class_="eolkernel"))
51
52 return KernelRelease(
53 nature=nature,
54 branch=get_branch(version),
55 version=version,
56 date=date,
57 link=link,
58 eol=eol,
59 )
60
61
62def get_branch(version: str):
63 # This is a testing kernel.
64 if "rc" in version:
65 return "testing"
66 else:
67 major, minor, *_ = version.split(".")
68 return f"{major}.{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 kernel in parsed_releases:
117 branch = get_branch(kernel.version)
118 nixpkgs_branch = branch.replace(".", "_")
119
120 old_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": 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()