linux-kernel-manager 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- linux_kernel_manager-0.1.2.dist-info/METADATA +271 -0
- linux_kernel_manager-0.1.2.dist-info/RECORD +45 -0
- linux_kernel_manager-0.1.2.dist-info/WHEEL +4 -0
- linux_kernel_manager-0.1.2.dist-info/entry_points.txt +3 -0
- linux_kernel_manager-0.1.2.dist-info/licenses/LICENSE +675 -0
- lkm/__init__.py +2 -0
- lkm/cli/__init__.py +0 -0
- lkm/cli/main.py +332 -0
- lkm/cli/output.py +57 -0
- lkm/core/__init__.py +0 -0
- lkm/core/backends/__init__.py +54 -0
- lkm/core/backends/apk.py +46 -0
- lkm/core/backends/apt.py +77 -0
- lkm/core/backends/base.py +81 -0
- lkm/core/backends/dnf.py +44 -0
- lkm/core/backends/nix.py +195 -0
- lkm/core/backends/pacman.py +78 -0
- lkm/core/backends/portage.py +107 -0
- lkm/core/backends/xbps.py +139 -0
- lkm/core/backends/zypper.py +44 -0
- lkm/core/kernel.py +95 -0
- lkm/core/manager.py +243 -0
- lkm/core/providers/__init__.py +62 -0
- lkm/core/providers/base.py +88 -0
- lkm/core/providers/distro.py +139 -0
- lkm/core/providers/gentoo.py +68 -0
- lkm/core/providers/liquorix.py +80 -0
- lkm/core/providers/lkf_build.py +355 -0
- lkm/core/providers/local_file.py +104 -0
- lkm/core/providers/mainline.py +106 -0
- lkm/core/providers/nixos.py +76 -0
- lkm/core/providers/void.py +69 -0
- lkm/core/providers/xanmod.py +81 -0
- lkm/core/system.py +235 -0
- lkm/gui/__init__.py +0 -0
- lkm/gui/app.py +94 -0
- lkm/gui/kernel_model.py +98 -0
- lkm/gui/main_window.py +385 -0
- lkm/gui/widgets/__init__.py +0 -0
- lkm/gui/widgets/gentoo_compile_dialog.py +132 -0
- lkm/gui/widgets/kernel_view.py +121 -0
- lkm/gui/widgets/lkf_build_dialog.py +370 -0
- lkm/gui/widgets/log_panel.py +78 -0
- lkm/gui/widgets/note_dialog.py +38 -0
- lkm/qt.py +122 -0
lkm/core/backends/nix.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nix backend — NixOS.
|
|
3
|
+
|
|
4
|
+
NixOS manages kernels declaratively via /etc/nixos/configuration.nix (channels)
|
|
5
|
+
or a flake.nix (flakes). This backend:
|
|
6
|
+
|
|
7
|
+
- Detects whether the system uses channels or flakes.
|
|
8
|
+
- Patches boot.kernelPackages in the appropriate config file.
|
|
9
|
+
- Runs nixos-rebuild switch to apply the change.
|
|
10
|
+
- Hold/unhold explains flake input pinning rather than silently failing.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import shutil
|
|
17
|
+
import tempfile
|
|
18
|
+
from collections.abc import Iterator
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from lkm.core.backends.base import PackageBackend
|
|
22
|
+
from lkm.core.system import privilege_escalation_cmd
|
|
23
|
+
|
|
24
|
+
_NIXPKGS_KERNEL_ATTRS = {
|
|
25
|
+
"linuxPackages", "linuxPackages_latest", "linuxPackages_6_6",
|
|
26
|
+
"linuxPackages_6_1", "linuxPackages_5_15", "linuxPackages_rt",
|
|
27
|
+
"linuxPackages_hardened", "linuxPackages_zen",
|
|
28
|
+
"linuxPackages_xanmod", "linuxPackages_xanmod_latest",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_NIXOS_CONFIG = Path("/etc/nixos/configuration.nix")
|
|
32
|
+
_NIXOS_FLAKE = Path("/etc/nixos/flake.nix")
|
|
33
|
+
|
|
34
|
+
_KERNEL_PKG_RE = re.compile(
|
|
35
|
+
r"(boot\.kernelPackages\s*=\s*)([^;]+)(;)",
|
|
36
|
+
re.MULTILINE,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_flake_system() -> bool:
|
|
41
|
+
return _NIXOS_FLAKE.exists()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _config_path() -> Path:
|
|
45
|
+
return _NIXOS_FLAKE if _is_flake_system() else _NIXOS_CONFIG
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _read_config(path: Path) -> str:
|
|
49
|
+
try:
|
|
50
|
+
return path.read_text()
|
|
51
|
+
except OSError:
|
|
52
|
+
return ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _write_config(path: Path, text: str) -> tuple[int, str, str]:
|
|
56
|
+
priv = privilege_escalation_cmd()
|
|
57
|
+
with tempfile.NamedTemporaryFile("w", suffix=".nix", delete=False) as f:
|
|
58
|
+
f.write(text)
|
|
59
|
+
tmp = f.name
|
|
60
|
+
import subprocess
|
|
61
|
+
result = subprocess.run(priv + ["cp", tmp, str(path)], capture_output=True, text=True)
|
|
62
|
+
os.unlink(tmp)
|
|
63
|
+
return result.returncode, result.stdout, result.stderr
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _patch_kernel_attr(text: str, new_attr: str) -> tuple[str, bool]:
|
|
67
|
+
"""Replace boot.kernelPackages = <old>; or insert it if absent."""
|
|
68
|
+
if _KERNEL_PKG_RE.search(text):
|
|
69
|
+
patched = _KERNEL_PKG_RE.sub(lambda m: m.group(1) + new_attr + m.group(3), text)
|
|
70
|
+
return patched, patched != text
|
|
71
|
+
insert = f"\n boot.kernelPackages = {new_attr};\n"
|
|
72
|
+
idx = text.rfind("}")
|
|
73
|
+
if idx == -1:
|
|
74
|
+
return text + insert, True
|
|
75
|
+
return text[:idx] + insert + text[idx:], True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _nixpkgs_attr_for(package_name: str) -> str:
|
|
79
|
+
"""Convert a package name / version string to a pkgs.linuxPackages_* expression."""
|
|
80
|
+
if package_name.startswith("pkgs."):
|
|
81
|
+
return package_name
|
|
82
|
+
if package_name in _NIXPKGS_KERNEL_ATTRS:
|
|
83
|
+
return f"pkgs.{package_name}"
|
|
84
|
+
m = re.match(r"^(\d+)\.(\d+)", package_name)
|
|
85
|
+
if m:
|
|
86
|
+
return f"pkgs.linuxPackages_{m.group(1)}_{m.group(2)}"
|
|
87
|
+
return f"pkgs.{package_name}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class NixBackend(PackageBackend):
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def name(self) -> str:
|
|
94
|
+
return "nix"
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def available() -> bool:
|
|
98
|
+
return bool(shutil.which("nixos-rebuild") or shutil.which("nix-env"))
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def is_nixos() -> bool:
|
|
102
|
+
try:
|
|
103
|
+
return "nixos" in open("/etc/os-release").read().lower()
|
|
104
|
+
except OSError:
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def is_flake() -> bool:
|
|
109
|
+
return _is_flake_system()
|
|
110
|
+
|
|
111
|
+
def install_packages(self, packages: list[str]) -> Iterator[str]:
|
|
112
|
+
if not packages:
|
|
113
|
+
return
|
|
114
|
+
attr = _nixpkgs_attr_for(packages[0])
|
|
115
|
+
cfg = _config_path()
|
|
116
|
+
text = _read_config(cfg)
|
|
117
|
+
if not text:
|
|
118
|
+
yield f"Cannot read {cfg} — is this NixOS?\n"
|
|
119
|
+
yield f" boot.kernelPackages = {attr};\n"
|
|
120
|
+
return
|
|
121
|
+
patched, changed = _patch_kernel_attr(text, attr)
|
|
122
|
+
if not changed:
|
|
123
|
+
yield f"boot.kernelPackages already set to {attr} in {cfg}\n"
|
|
124
|
+
else:
|
|
125
|
+
yield f"Patching {cfg}: boot.kernelPackages = {attr}\n"
|
|
126
|
+
rc, _, err = _write_config(cfg, patched)
|
|
127
|
+
if rc != 0:
|
|
128
|
+
yield f"Failed to write {cfg}: {err}\n"
|
|
129
|
+
yield f"Apply manually: boot.kernelPackages = {attr};\n"
|
|
130
|
+
return
|
|
131
|
+
yield f"Written {cfg}\n"
|
|
132
|
+
yield from self._rebuild()
|
|
133
|
+
|
|
134
|
+
def install_local(self, path: str) -> Iterator[str]:
|
|
135
|
+
yield f"Adding {path} to the Nix store...\n"
|
|
136
|
+
if shutil.which("nix-store"):
|
|
137
|
+
priv = privilege_escalation_cmd()
|
|
138
|
+
yield from self._run_streaming(priv + ["nix-store", "--add-fixed", "sha256", path])
|
|
139
|
+
yield f"Add the store path to boot.kernelPackages in {_config_path()}\n"
|
|
140
|
+
yield "Then run: sudo nixos-rebuild switch\n"
|
|
141
|
+
|
|
142
|
+
def remove_packages(self, packages: list[str], purge: bool = False) -> Iterator[str]:
|
|
143
|
+
yield f"Reverting boot.kernelPackages to pkgs.linuxPackages in {_config_path()}\n"
|
|
144
|
+
yield from self.install_packages(["linuxPackages"])
|
|
145
|
+
|
|
146
|
+
def hold(self, packages: list[str]) -> tuple[int, str, str]:
|
|
147
|
+
if _is_flake_system():
|
|
148
|
+
msg = (
|
|
149
|
+
"NixOS (flakes): pin nixpkgs to a specific revision in flake.lock:\n"
|
|
150
|
+
" nix flake lock --update-input nixpkgs "
|
|
151
|
+
"--override-input nixpkgs github:NixOS/nixpkgs/<commit>\n"
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
msg = (
|
|
155
|
+
"NixOS (channels): pin to a fixed channel URL:\n"
|
|
156
|
+
" sudo nix-channel --add "
|
|
157
|
+
"https://releases.nixos.org/nixos/<version>/nixos-<version> nixos\n"
|
|
158
|
+
" sudo nix-channel --update\n"
|
|
159
|
+
)
|
|
160
|
+
return 0, msg, ""
|
|
161
|
+
|
|
162
|
+
def unhold(self, packages: list[str]) -> tuple[int, str, str]:
|
|
163
|
+
if _is_flake_system():
|
|
164
|
+
msg = "NixOS (flakes): unpin by running: nix flake update\n"
|
|
165
|
+
else:
|
|
166
|
+
msg = (
|
|
167
|
+
"NixOS (channels): unpin by switching back to the rolling channel:\n"
|
|
168
|
+
" sudo nix-channel --add https://nixos.org/channels/nixos-unstable nixos\n"
|
|
169
|
+
" sudo nix-channel --update\n"
|
|
170
|
+
)
|
|
171
|
+
return 0, msg, ""
|
|
172
|
+
|
|
173
|
+
def is_installed(self, package: str) -> bool:
|
|
174
|
+
attr = _nixpkgs_attr_for(package).replace("pkgs.", "")
|
|
175
|
+
current = self.current_kernel_attr()
|
|
176
|
+
return bool(current) and attr in current
|
|
177
|
+
|
|
178
|
+
def current_kernel_attr(self) -> str:
|
|
179
|
+
text = _read_config(_config_path())
|
|
180
|
+
m = _KERNEL_PKG_RE.search(text)
|
|
181
|
+
return m.group(2).strip() if m else ""
|
|
182
|
+
|
|
183
|
+
def list_available_kernels(self) -> list[str]:
|
|
184
|
+
rc, out, _ = self._run(["nix-env", "-qaP", "--no-name", "linuxPackages"])
|
|
185
|
+
if rc != 0:
|
|
186
|
+
return list(_NIXPKGS_KERNEL_ATTRS)
|
|
187
|
+
return [line.strip() for line in out.splitlines() if "linuxPackages" in line]
|
|
188
|
+
|
|
189
|
+
def _rebuild(self) -> Iterator[str]:
|
|
190
|
+
if not shutil.which("nixos-rebuild"):
|
|
191
|
+
yield "nixos-rebuild not found — run: sudo nixos-rebuild switch\n"
|
|
192
|
+
return
|
|
193
|
+
priv = privilege_escalation_cmd()
|
|
194
|
+
yield "Running nixos-rebuild switch...\n"
|
|
195
|
+
yield from self._run_streaming(priv + ["nixos-rebuild", "switch"])
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""pacman backend — Arch, Manjaro, EndeavourOS, CachyOS, and derivatives."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
|
|
6
|
+
from lkm.core.backends.base import PackageBackend
|
|
7
|
+
from lkm.core.system import privilege_escalation_cmd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PacmanBackend(PackageBackend):
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def name(self) -> str:
|
|
14
|
+
return "pacman"
|
|
15
|
+
|
|
16
|
+
def install_packages(self, packages: list[str]) -> Iterator[str]:
|
|
17
|
+
priv = privilege_escalation_cmd()
|
|
18
|
+
yield from self._run_streaming(
|
|
19
|
+
priv + ["pacman", "-S", "--noconfirm", "--needed"] + packages
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def install_local(self, path: str) -> Iterator[str]:
|
|
23
|
+
priv = privilege_escalation_cmd()
|
|
24
|
+
yield from self._run_streaming(
|
|
25
|
+
priv + ["pacman", "-U", "--noconfirm", path]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def remove_packages(self, packages: list[str], purge: bool = False) -> Iterator[str]:
|
|
29
|
+
priv = privilege_escalation_cmd()
|
|
30
|
+
flags = ["-Rns"] if purge else ["-R"]
|
|
31
|
+
yield from self._run_streaming(
|
|
32
|
+
priv + ["pacman"] + flags + ["--noconfirm"] + packages
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def hold(self, packages: list[str]) -> tuple[int, str, str]:
|
|
36
|
+
# pacman uses IgnorePkg in /etc/pacman.conf; we append to it
|
|
37
|
+
import re
|
|
38
|
+
priv = privilege_escalation_cmd()
|
|
39
|
+
conf = "/etc/pacman.conf"
|
|
40
|
+
try:
|
|
41
|
+
text = open(conf).read()
|
|
42
|
+
except OSError as e:
|
|
43
|
+
return 1, "", str(e)
|
|
44
|
+
pkg_str = " ".join(packages)
|
|
45
|
+
if "IgnorePkg" in text:
|
|
46
|
+
text = re.sub(
|
|
47
|
+
r"(IgnorePkg\s*=\s*)(.*)",
|
|
48
|
+
lambda m: m.group(1) + (m.group(2) + " " + pkg_str).strip(),
|
|
49
|
+
text,
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
text = text.replace("[options]", f"[options]\nIgnorePkg = {pkg_str}", 1)
|
|
53
|
+
import tempfile
|
|
54
|
+
with tempfile.NamedTemporaryFile("w", suffix=".conf", delete=False) as f:
|
|
55
|
+
f.write(text)
|
|
56
|
+
tmp = f.name
|
|
57
|
+
rc, out, err = self._run(priv + ["cp", tmp, conf])
|
|
58
|
+
return rc, out, err
|
|
59
|
+
|
|
60
|
+
def unhold(self, packages: list[str]) -> tuple[int, str, str]:
|
|
61
|
+
import re
|
|
62
|
+
priv = privilege_escalation_cmd()
|
|
63
|
+
conf = "/etc/pacman.conf"
|
|
64
|
+
try:
|
|
65
|
+
text = open(conf).read()
|
|
66
|
+
except OSError as e:
|
|
67
|
+
return 1, "", str(e)
|
|
68
|
+
for pkg in packages:
|
|
69
|
+
text = re.sub(rf"\b{re.escape(pkg)}\b\s*", "", text)
|
|
70
|
+
import tempfile
|
|
71
|
+
with tempfile.NamedTemporaryFile("w", suffix=".conf", delete=False) as f:
|
|
72
|
+
f.write(text)
|
|
73
|
+
tmp = f.name
|
|
74
|
+
return self._run(priv + ["cp", tmp, conf])
|
|
75
|
+
|
|
76
|
+
def is_installed(self, package: str) -> bool:
|
|
77
|
+
rc, _, _ = self._run(["pacman", "-Q", package])
|
|
78
|
+
return rc == 0
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""portage backend — Gentoo."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from lkm.core.backends.base import PackageBackend
|
|
10
|
+
from lkm.core.system import privilege_escalation_cmd
|
|
11
|
+
|
|
12
|
+
_KERNEL_SOURCES_DIR = Path("/usr/src")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PortageBackend(PackageBackend):
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
return "portage"
|
|
20
|
+
|
|
21
|
+
def install_packages(self, packages: list[str]) -> Iterator[str]:
|
|
22
|
+
priv = privilege_escalation_cmd()
|
|
23
|
+
yield from self._run_streaming(
|
|
24
|
+
priv + ["emerge", "--ask=n", "--quiet-build"] + packages
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def install_local(self, path: str) -> Iterator[str]:
|
|
28
|
+
# Portage doesn't install arbitrary binary packages; delegate to
|
|
29
|
+
# a manual unpack with a clear message.
|
|
30
|
+
yield f"Portage does not support binary package install from {path}.\n"
|
|
31
|
+
yield "Use 'lkm build' to compile from source on Gentoo.\n"
|
|
32
|
+
|
|
33
|
+
def remove_packages(self, packages: list[str], purge: bool = False) -> Iterator[str]:
|
|
34
|
+
priv = privilege_escalation_cmd()
|
|
35
|
+
flags = ["--depclean"] if purge else ["--unmerge"]
|
|
36
|
+
yield from self._run_streaming(
|
|
37
|
+
priv + ["emerge"] + flags + packages
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def hold(self, packages: list[str]) -> tuple[int, str, str]:
|
|
41
|
+
# Write package.mask entries
|
|
42
|
+
priv = privilege_escalation_cmd()
|
|
43
|
+
mask_dir = Path("/etc/portage/package.mask")
|
|
44
|
+
mask_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
mask_file = mask_dir / "lkm-held"
|
|
46
|
+
existing = mask_file.read_text() if mask_file.exists() else ""
|
|
47
|
+
new_entries = "\n".join(f"={p}" for p in packages if f"={p}" not in existing)
|
|
48
|
+
if new_entries:
|
|
49
|
+
return self._run(
|
|
50
|
+
priv + ["tee", "-a", str(mask_file)],
|
|
51
|
+
input=new_entries + "\n",
|
|
52
|
+
)
|
|
53
|
+
return 0, "", ""
|
|
54
|
+
|
|
55
|
+
def unhold(self, packages: list[str]) -> tuple[int, str, str]:
|
|
56
|
+
import re
|
|
57
|
+
mask_file = Path("/etc/portage/package.mask/lkm-held")
|
|
58
|
+
if not mask_file.exists():
|
|
59
|
+
return 0, "", ""
|
|
60
|
+
text = mask_file.read_text()
|
|
61
|
+
for pkg in packages:
|
|
62
|
+
text = re.sub(rf"^={re.escape(pkg)}\n?", "", text, flags=re.MULTILINE)
|
|
63
|
+
priv = privilege_escalation_cmd()
|
|
64
|
+
return self._run(priv + ["tee", str(mask_file)], input=text)
|
|
65
|
+
|
|
66
|
+
def is_installed(self, package: str) -> bool:
|
|
67
|
+
rc, _, _ = self._run(["qlist", "-I", package])
|
|
68
|
+
return rc == 0
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
# Gentoo-specific helpers used by the Gentoo provider
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def list_kernel_sources(self) -> list[str]:
|
|
75
|
+
"""Return paths to installed kernel source trees under /usr/src."""
|
|
76
|
+
if not _KERNEL_SOURCES_DIR.exists():
|
|
77
|
+
return []
|
|
78
|
+
return sorted(
|
|
79
|
+
str(p) for p in _KERNEL_SOURCES_DIR.iterdir()
|
|
80
|
+
if p.is_dir() and p.name.startswith("linux-")
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def has_genkernel(self) -> bool:
|
|
84
|
+
return bool(shutil.which("genkernel"))
|
|
85
|
+
|
|
86
|
+
def compile(self, src: str, use_genkernel: bool, jobs: int) -> Iterator[str]:
|
|
87
|
+
"""Stream kernel compilation output."""
|
|
88
|
+
priv = privilege_escalation_cmd()
|
|
89
|
+
if use_genkernel:
|
|
90
|
+
yield from self._run_streaming(
|
|
91
|
+
priv + ["genkernel", "--kernel-config=/proc/config.gz", "all"],
|
|
92
|
+
cwd=src,
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
j = jobs if jobs > 0 else os.cpu_count() or 1
|
|
96
|
+
yield from self._run_streaming(
|
|
97
|
+
priv + ["make", f"-j{j}"],
|
|
98
|
+
cwd=src,
|
|
99
|
+
)
|
|
100
|
+
yield from self._run_streaming(
|
|
101
|
+
priv + ["make", "modules_install"],
|
|
102
|
+
cwd=src,
|
|
103
|
+
)
|
|
104
|
+
yield from self._run_streaming(
|
|
105
|
+
priv + ["make", "install"],
|
|
106
|
+
cwd=src,
|
|
107
|
+
)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xbps backend — Void Linux.
|
|
3
|
+
|
|
4
|
+
Void ships its own kernel packages (linux, linux-lts, linux-mainline) via the
|
|
5
|
+
official xbps repository. This backend handles install/remove/hold for those
|
|
6
|
+
packages and for locally built .xbps archives produced by lkf.
|
|
7
|
+
|
|
8
|
+
Hold/unhold is implemented via xbps-pkgdb -m hold/unhold, which is the
|
|
9
|
+
official Void mechanism for pinning packages.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
from collections.abc import Iterator
|
|
16
|
+
|
|
17
|
+
from lkm.core.backends.base import PackageBackend
|
|
18
|
+
from lkm.core.system import privilege_escalation_cmd
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class XbpsBackend(PackageBackend):
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def name(self) -> str:
|
|
25
|
+
return "xbps"
|
|
26
|
+
|
|
27
|
+
# ------------------------------------------------------------------
|
|
28
|
+
# Availability guard
|
|
29
|
+
# ------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def available() -> bool:
|
|
33
|
+
return bool(shutil.which("xbps-install"))
|
|
34
|
+
|
|
35
|
+
# ------------------------------------------------------------------
|
|
36
|
+
# Core operations
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def install_packages(self, packages: list[str]) -> Iterator[str]:
|
|
40
|
+
priv = privilege_escalation_cmd()
|
|
41
|
+
# -y: assume yes; -S: sync repos first
|
|
42
|
+
yield from self._run_streaming(
|
|
43
|
+
priv + ["xbps-install", "-Sy"] + packages
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def install_local(self, path: str) -> Iterator[str]:
|
|
47
|
+
"""
|
|
48
|
+
Install a locally built .xbps package.
|
|
49
|
+
|
|
50
|
+
xbps-install can install from a local repository directory. We point
|
|
51
|
+
it at the directory containing the package file and install by name.
|
|
52
|
+
"""
|
|
53
|
+
import os
|
|
54
|
+
priv = privilege_escalation_cmd()
|
|
55
|
+
pkg_dir = os.path.dirname(os.path.abspath(path))
|
|
56
|
+
# xbps-install -R <repodir> -y <pkgname>
|
|
57
|
+
# The package name is the filename without the arch+suffix.
|
|
58
|
+
pkg_name = os.path.basename(path).split("-")[0]
|
|
59
|
+
yield from self._run_streaming(
|
|
60
|
+
priv + ["xbps-install", "-R", pkg_dir, "-y", pkg_name]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def remove_packages(self, packages: list[str], purge: bool = False) -> Iterator[str]:
|
|
64
|
+
priv = privilege_escalation_cmd()
|
|
65
|
+
# -R: remove recursively (orphaned deps); -y: assume yes
|
|
66
|
+
flags = ["-Ry"] if purge else ["-y"]
|
|
67
|
+
yield from self._run_streaming(
|
|
68
|
+
priv + ["xbps-remove"] + flags + packages
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def hold(self, packages: list[str]) -> tuple[int, str, str]:
|
|
72
|
+
"""
|
|
73
|
+
Pin packages with xbps-pkgdb -m hold.
|
|
74
|
+
|
|
75
|
+
A held package is excluded from xbps-install -u (system upgrades)
|
|
76
|
+
but can still be explicitly upgraded.
|
|
77
|
+
"""
|
|
78
|
+
priv = privilege_escalation_cmd()
|
|
79
|
+
rc, out, err = 0, "", ""
|
|
80
|
+
for pkg in packages:
|
|
81
|
+
r, o, e = self._run(priv + ["xbps-pkgdb", "-m", "hold", pkg])
|
|
82
|
+
rc = rc or r
|
|
83
|
+
out += o
|
|
84
|
+
err += e
|
|
85
|
+
return rc, out, err
|
|
86
|
+
|
|
87
|
+
def unhold(self, packages: list[str]) -> tuple[int, str, str]:
|
|
88
|
+
priv = privilege_escalation_cmd()
|
|
89
|
+
rc, out, err = 0, "", ""
|
|
90
|
+
for pkg in packages:
|
|
91
|
+
r, o, e = self._run(priv + ["xbps-pkgdb", "-m", "unhold", pkg])
|
|
92
|
+
rc = rc or r
|
|
93
|
+
out += o
|
|
94
|
+
err += e
|
|
95
|
+
return rc, out, err
|
|
96
|
+
|
|
97
|
+
def is_installed(self, package: str) -> bool:
|
|
98
|
+
rc, out, _ = self._run(["xbps-query", package])
|
|
99
|
+
return rc == 0 and "state: installed" in out.lower()
|
|
100
|
+
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
# Void-specific helpers
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def list_available_kernels(self) -> list[str]:
|
|
106
|
+
"""
|
|
107
|
+
Return kernel package names available in the xbps repos.
|
|
108
|
+
|
|
109
|
+
xbps-query -Rs output format (varies by version):
|
|
110
|
+
[-] linux-6.6.30_1 The Linux kernel and modules
|
|
111
|
+
[*] linux-6.6.30_1 The Linux kernel and modules (installed)
|
|
112
|
+
|
|
113
|
+
We accept both the flag-prefixed form and bare pkgver strings.
|
|
114
|
+
Only packages whose name starts with "linux" and whose name does
|
|
115
|
+
NOT contain "-headers" or "-dbg" are returned.
|
|
116
|
+
"""
|
|
117
|
+
rc, out, _ = self._run(["xbps-query", "-Rs", "linux"])
|
|
118
|
+
if rc != 0:
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
names: list[str] = []
|
|
122
|
+
# Match: optional [x] prefix, then pkgver like linux-6.6.30_1
|
|
123
|
+
_LINE_RE = re.compile(r"(?:\[.\]\s+)?(linux[a-zA-Z0-9._+-]+?)(?:_\d+)?\s")
|
|
124
|
+
for line in out.splitlines():
|
|
125
|
+
m = _LINE_RE.search(line)
|
|
126
|
+
if not m:
|
|
127
|
+
continue
|
|
128
|
+
name = m.group(1)
|
|
129
|
+
# Skip sub-packages
|
|
130
|
+
if any(x in name for x in ("-headers", "-dbg", "-doc", "-devel")):
|
|
131
|
+
continue
|
|
132
|
+
if name not in names:
|
|
133
|
+
names.append(name)
|
|
134
|
+
return names
|
|
135
|
+
|
|
136
|
+
def sync(self) -> Iterator[str]:
|
|
137
|
+
"""Sync xbps repository index."""
|
|
138
|
+
priv = privilege_escalation_cmd()
|
|
139
|
+
yield from self._run_streaming(priv + ["xbps-install", "-S"])
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""zypper backend — openSUSE Leap, Tumbleweed, SLES, Regata."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
|
|
6
|
+
from lkm.core.backends.base import PackageBackend
|
|
7
|
+
from lkm.core.system import privilege_escalation_cmd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ZypperBackend(PackageBackend):
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def name(self) -> str:
|
|
14
|
+
return "zypper"
|
|
15
|
+
|
|
16
|
+
def install_packages(self, packages: list[str]) -> Iterator[str]:
|
|
17
|
+
priv = privilege_escalation_cmd()
|
|
18
|
+
yield from self._run_streaming(
|
|
19
|
+
priv + ["zypper", "--non-interactive", "install"] + packages
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def install_local(self, path: str) -> Iterator[str]:
|
|
23
|
+
priv = privilege_escalation_cmd()
|
|
24
|
+
yield from self._run_streaming(
|
|
25
|
+
priv + ["zypper", "--non-interactive", "install", path]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def remove_packages(self, packages: list[str], purge: bool = False) -> Iterator[str]:
|
|
29
|
+
priv = privilege_escalation_cmd()
|
|
30
|
+
yield from self._run_streaming(
|
|
31
|
+
priv + ["zypper", "--non-interactive", "remove"] + packages
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def hold(self, packages: list[str]) -> tuple[int, str, str]:
|
|
35
|
+
priv = privilege_escalation_cmd()
|
|
36
|
+
return self._run(priv + ["zypper", "addlock"] + packages)
|
|
37
|
+
|
|
38
|
+
def unhold(self, packages: list[str]) -> tuple[int, str, str]:
|
|
39
|
+
priv = privilege_escalation_cmd()
|
|
40
|
+
return self._run(priv + ["zypper", "removelock"] + packages)
|
|
41
|
+
|
|
42
|
+
def is_installed(self, package: str) -> bool:
|
|
43
|
+
rc, _, _ = self._run(["rpm", "-q", package])
|
|
44
|
+
return rc == 0
|
lkm/core/kernel.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kernel data model: KernelEntry, KernelVersion, KernelFamily, KernelStatus.
|
|
3
|
+
|
|
4
|
+
Unchanged from ukm except KernelFamily gains LKF_BUILD for locally compiled
|
|
5
|
+
kernels produced by the lkf build pipeline.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from functools import total_ordering
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class KernelFamily(Enum):
|
|
16
|
+
MAINLINE = "mainline" # Ubuntu Mainline PPA
|
|
17
|
+
XANMOD = "xanmod"
|
|
18
|
+
LIQUORIX = "liquorix"
|
|
19
|
+
DISTRO = "distro" # whatever the distro ships
|
|
20
|
+
GENTOO = "gentoo"
|
|
21
|
+
LOCAL_FILE = "local_file" # pre-built .deb/.rpm/.pkg.tar.* dropped in
|
|
22
|
+
LKF_BUILD = "lkf_build" # compiled by the lkf build pipeline
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class KernelStatus(Enum):
|
|
26
|
+
AVAILABLE = "available"
|
|
27
|
+
INSTALLED = "installed"
|
|
28
|
+
RUNNING = "running"
|
|
29
|
+
HELD = "held"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@total_ordering
|
|
33
|
+
@dataclass
|
|
34
|
+
class KernelVersion:
|
|
35
|
+
"""Comparable semantic kernel version, e.g. 6.12.3."""
|
|
36
|
+
major: int
|
|
37
|
+
minor: int
|
|
38
|
+
patch: int
|
|
39
|
+
extra: str = "" # e.g. "-xanmod1", "-rt4", "-lkf"
|
|
40
|
+
|
|
41
|
+
_VERSION_RE = re.compile(
|
|
42
|
+
r"^(\d+)\.(\d+)(?:\.(\d+))?(.*)$"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def parse(cls, s: str) -> KernelVersion:
|
|
47
|
+
m = cls._VERSION_RE.match(s.strip())
|
|
48
|
+
if not m:
|
|
49
|
+
raise ValueError(f"Cannot parse kernel version: {s!r}")
|
|
50
|
+
major = int(m.group(1))
|
|
51
|
+
minor = int(m.group(2))
|
|
52
|
+
patch = int(m.group(3) or 0)
|
|
53
|
+
extra = m.group(4) or ""
|
|
54
|
+
return cls(major=major, minor=minor, patch=patch, extra=extra)
|
|
55
|
+
|
|
56
|
+
def __str__(self) -> str:
|
|
57
|
+
return f"{self.major}.{self.minor}.{self.patch}{self.extra}"
|
|
58
|
+
|
|
59
|
+
def __eq__(self, other: object) -> bool:
|
|
60
|
+
if not isinstance(other, KernelVersion):
|
|
61
|
+
return NotImplemented
|
|
62
|
+
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
|
|
63
|
+
|
|
64
|
+
def __lt__(self, other: KernelVersion) -> bool:
|
|
65
|
+
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
|
|
66
|
+
|
|
67
|
+
def __hash__(self) -> int:
|
|
68
|
+
return hash((self.major, self.minor, self.patch, self.extra))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class KernelEntry:
|
|
73
|
+
"""A single kernel known to lkm — may be available, installed, or running."""
|
|
74
|
+
version: KernelVersion
|
|
75
|
+
family: KernelFamily
|
|
76
|
+
flavor: str # e.g. "generic", "rt", "xanmod-edge", "lkf-tkg"
|
|
77
|
+
arch: str # normalised arch string
|
|
78
|
+
provider_id: str # matches KernelProvider.id
|
|
79
|
+
status: KernelStatus = KernelStatus.AVAILABLE
|
|
80
|
+
held: bool = False
|
|
81
|
+
notes: str = ""
|
|
82
|
+
# For LOCAL_FILE / LKF_BUILD: path to the package file
|
|
83
|
+
local_path: str | None = field(default=None)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def display_name(self) -> str:
|
|
87
|
+
return f"{self.version} ({self.family.value}, {self.flavor})"
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def is_installed(self) -> bool:
|
|
91
|
+
return self.status in (KernelStatus.INSTALLED, KernelStatus.RUNNING, KernelStatus.HELD)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def is_running(self) -> bool:
|
|
95
|
+
return self.status == KernelStatus.RUNNING
|