initial
This commit is contained in:
commit
c59be062b1
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.swp
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Florian Schrögendorfer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
61
Makefile
Normal file
61
Makefile
Normal file
@ -0,0 +1,61 @@
|
||||
# python executable
|
||||
PYTHON = python3 -B
|
||||
|
||||
# virtual environment path
|
||||
VENV = venv
|
||||
|
||||
# command to activate the virtual environment
|
||||
ACTIVATE = . $(VENV)/bin/activate
|
||||
|
||||
#
|
||||
# testing
|
||||
#
|
||||
|
||||
TESTS =
|
||||
|
||||
.PHONY: test
|
||||
test: export PYTHONDONTWRITEBYTECODE = 1
|
||||
test:
|
||||
$(PYTHON) -m unittest -f $(TESTS)
|
||||
|
||||
#
|
||||
# building
|
||||
#
|
||||
|
||||
build: ekernel.py pyproject.toml setup.py
|
||||
$(PYTHON) -m build --sdist
|
||||
|
||||
#
|
||||
# code quality
|
||||
#
|
||||
|
||||
FLAKE8_OPTS = --ignore E123,E124,E128,E201,E202,E211,E302,E306,E701
|
||||
|
||||
.PHONY: lint
|
||||
lint: venv
|
||||
@$(ACTIVATE) && flake8 $(FLAKE8_OPTS) lib cli tests
|
||||
|
||||
#
|
||||
# initialize environment
|
||||
#
|
||||
|
||||
.PHONY: init
|
||||
init: requirements
|
||||
|
||||
# disable pip's cache (under ~/.cache/pip)
|
||||
export PIP_NO_CACHE_DIR ?= true
|
||||
|
||||
# install dependencies
|
||||
.PHONY: requirements
|
||||
requirements: $(VENV)
|
||||
$(ACTIVATE) && pip install -U -r requirements.txt
|
||||
|
||||
# create virtual environment
|
||||
$(VENV):
|
||||
$(PYTHON) -m venv $(VENV)
|
||||
$(ACTIVATE) && pip install --upgrade pip wheel
|
||||
|
||||
# remove virtual environment
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(VENV)
|
124
README.md
Normal file
124
README.md
Normal file
@ -0,0 +1,124 @@
|
||||
# ekernel - Custom Gentoo EFI Stub Kernel Updater
|
||||
|
||||
Automate custom gentoo kernel update tasks.
|
||||
|
||||
The update process can roughly be divided into the usual three steps:
|
||||
|
||||
1) **configure** - copy and update the previous `.config`
|
||||
2) **build** - compile the new kernel and install modules
|
||||
3) **install** - copy the new EFI stub kernel image to the EFI system partition
|
||||
4) **commit** - commit the new config with a detailed commit message
|
||||
5) **clean** - remove unused kernel source directories, modules and boot images
|
||||
|
||||
This tool aims to minimize the effort by automating the previous steps, while
|
||||
ensuring maximum flexibility w.r.t. configuration changes.
|
||||
|
||||
## Tasks
|
||||
|
||||
The following variables will be used throughout this description:
|
||||
|
||||
`old` - previous kernel's source directory
|
||||
|
||||
`new` - new kernel's source directory
|
||||
|
||||
`esp` - directory containing the live kernel image on the EFI system partition (`/boot/EFI/Gentoo/bootx64.efi`)
|
||||
|
||||
`jobs` - number of parallel make jobs given by ``-j`` (default: 64)
|
||||
|
||||
`version` - new kernel's version string
|
||||
|
||||
### <p>configure<span style="float:right">`ekernel-configure`</span></p>
|
||||
|
||||
Runs `make menuconfig` if the current config file is missing, or no other kernel is installed
|
||||
|
||||
Otherwise, copy the current config file to the new source directory if it doesn't exist (prevent accidental overwrite).
|
||||
|
||||
```sh
|
||||
cp -n ${old}/.config ${new}
|
||||
```
|
||||
|
||||
Get newly added config options and store the result in `${new}/.newoptions`.
|
||||
|
||||
```sh
|
||||
cd ${new}
|
||||
make listnewconfig > ${new}/.newoptions
|
||||
```
|
||||
|
||||
If ``-l`` was selected: print newly added config options and exit.
|
||||
|
||||
```sh
|
||||
cat ${new}/.newoptions
|
||||
exit
|
||||
```
|
||||
|
||||
Interactively update the previous config and exit if aborted.
|
||||
|
||||
```sh
|
||||
cd ${new}
|
||||
make oldconfig || exit
|
||||
```
|
||||
|
||||
### <p>build<span style="float:right">`ekernel-build`</span></p>
|
||||
|
||||
Build and install modules, using the given number of jobs.
|
||||
|
||||
```sh
|
||||
make -j ${jobs} && make modules_install
|
||||
```
|
||||
|
||||
### <p>install<span style="float:right">`ekernel-install`</span></p>
|
||||
|
||||
Update symlink ``/usr/src/linux`` to the new source directory.
|
||||
|
||||
```sh
|
||||
eselect kernel set $(basename ${new})
|
||||
```
|
||||
|
||||
Install the EFI stub kernel image (and a backup copy to revert to in case something breaks after a subsequent kernel update).
|
||||
|
||||
```sh
|
||||
mount /boot
|
||||
cp ${new}/arch/x86_64/boot/bzImage ${esp}/bootx64.efi
|
||||
cp ${new}/arch/x86_64/boot/bzImage ${esp}/gentoo-${version}.efi
|
||||
```
|
||||
|
||||
Rebuild external modules.
|
||||
|
||||
```sh
|
||||
emerge @module-rebuild
|
||||
```
|
||||
|
||||
### <p>commit<span style="float:right">`ekernel-commit`</span></p>
|
||||
|
||||
Commit the new kernel config with a detailed commit message.
|
||||
|
||||
```sh
|
||||
git add -f /usr/src/linux/.config
|
||||
git commit -S -m "${msg}"
|
||||
```
|
||||
|
||||
The message will not only contain the version change, but also details about the newly added or removed options.
|
||||
|
||||
### <p>clean<span style="float:right">`ekernel-clean`</span></p>
|
||||
|
||||
Remove unused kernel source directories, modules and boot images.
|
||||
|
||||
```sh
|
||||
emerge --depclean sys-kernel/gentoo-sources
|
||||
rm -rf $(find /usr/src/ -maxdepth 1 -name "linux-*" | sed -e '/${old}/d' -e '/${new}/d')
|
||||
rm -rf $(ls -1 /lib/modules/ | sed -e '/${old}/d' -e '/${new}/d')
|
||||
rm -rf $(ls -1 ${esp} | sed -e '/${old}/d' -e '/${new}/d' -e '/bootx64/d')
|
||||
```
|
||||
|
||||
The default is to keep the previous kernel version in case something goes
|
||||
horribly wrong.
|
||||
|
||||
## Installation
|
||||
|
||||
TODO: add ebuild `app-admin/ekernel`
|
||||
|
||||
## Requirements
|
||||
|
||||
* [`>=dev-lang/python-3.10`](https://packages.gentoo.org/packages/dev-lang/python)
|
||||
* [`dev-python/packaging`](https://packages.gentoo.org/packages/dev-python/packaging)
|
||||
* [`sys-apps/portage`](https://packages.gentoo.org/packages/sys-apps/portage)
|
786
ekernel.py
Normal file
786
ekernel.py
Normal file
@ -0,0 +1,786 @@
|
||||
import argparse
|
||||
import difflib
|
||||
import functools
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from packaging.version import Version
|
||||
from portage import output, colorize
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
# number of parallel build jobs
|
||||
jobs = "4"
|
||||
|
||||
# gentoo's fancy terminal output functions
|
||||
out = output.EOutput()
|
||||
out.print = lambda s: print(s) if not out.quiet else None
|
||||
out.hilite = lambda s: colorize("HILITE", str(s))
|
||||
|
||||
# disable colorization for pipes and redirects
|
||||
if not sys.stdout.isatty():
|
||||
output.havecolor = 0
|
||||
|
||||
def version (string: str):
|
||||
"""Extract the version from a given string."""
|
||||
return Version("".join(filter(
|
||||
None,
|
||||
re.search(r"(\d+.\d+.\d+)-gentoo(-\w+\d+)?", string).groups()
|
||||
)))
|
||||
|
||||
class Kernel:
|
||||
|
||||
# kernel source directory
|
||||
src = pathlib.Path("/usr/src")
|
||||
|
||||
# kernel source symlink
|
||||
linux = src / "linux"
|
||||
|
||||
# EFI system partition
|
||||
esp = pathlib.Path("/boot/EFI/Gentoo")
|
||||
|
||||
# boot image
|
||||
bootx64 = esp / "bootx64.efi"
|
||||
|
||||
# module directory
|
||||
modules = pathlib.Path("/lib/modules")
|
||||
|
||||
def __init__ (self, src):
|
||||
"""Construct a Kernel based on a given source path."""
|
||||
self.src = pathlib.Path(src)
|
||||
if not self.src.exists():
|
||||
raise ValueError(f"missing source: {src}")
|
||||
try:
|
||||
self.version = version(self.src.name)
|
||||
except Exception as e:
|
||||
raise ValueError(f"illegal source: {src}") from e
|
||||
self.config = self.src / ".config"
|
||||
self.bzImage = self.src / "arch/x86_64/boot/bzImage"
|
||||
self.efi = self.esp / f"gentoo-{self.version.base_version}.efi"
|
||||
self.modules = self.modules / f"{self.version.base_version}-gentoo"
|
||||
|
||||
def __eq__ (self, other):
|
||||
if not isinstance(other, Kernel):
|
||||
return False
|
||||
return self.src == other.src
|
||||
|
||||
def __str__ (self):
|
||||
return (
|
||||
f"{self.src.name}\n"
|
||||
f"* version = {self.version}\n"
|
||||
f"* src = {self.src}\n"
|
||||
f"* config = {self.config}\n"
|
||||
f"* bzImage = {self.bzImage}\n"
|
||||
f"* efi = {self.efi}\n"
|
||||
f"* modules = {self.modules}\n"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def list (cls, descending=True):
|
||||
"""Get an descending list of available kernels."""
|
||||
return list(sorted(
|
||||
( Kernel(src) for src in cls.src.glob("linux-*") ),
|
||||
key=lambda k: k.version,
|
||||
reverse=descending
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def current (cls):
|
||||
"""
|
||||
Get the currently running kernel.
|
||||
|
||||
Returns:
|
||||
Kernel: the current kernel, pointed to by ``/usr/src/linux``
|
||||
"""
|
||||
return cls(Kernel.linux.resolve())
|
||||
|
||||
@classmethod
|
||||
def latest (cls):
|
||||
"""
|
||||
Get the latest available kernel.
|
||||
|
||||
Returns:
|
||||
Kernel: the newest kernel under ``/usr/src``
|
||||
"""
|
||||
return cls.list()[0]
|
||||
|
||||
def cli (f):
|
||||
"""A top level exception handling decorator for script main functions."""
|
||||
@functools.wraps(f)
|
||||
def handler (argv=sys.argv[1:]):
|
||||
try:
|
||||
r = f(argv)
|
||||
return 0 if r is None else r
|
||||
except Exception as e:
|
||||
out.eerror(str(e))
|
||||
sys.exit(1)
|
||||
return handler
|
||||
|
||||
def mount (path):
|
||||
"""Decorator ensuring a given path is mounted."""
|
||||
def wrapper (f):
|
||||
@functools.wraps(f)
|
||||
def mounter (*args, **kwargs):
|
||||
mounted = False
|
||||
try:
|
||||
subprocess.run(
|
||||
["mount", path],
|
||||
capture_output=True,
|
||||
check=True
|
||||
)
|
||||
mounted = True
|
||||
except subprocess.CalledProcessError as e:
|
||||
msg = e.stderr.decode().strip()
|
||||
if f"already mounted on {path}" not in msg:
|
||||
raise RuntimeError(e.stderr.decode().splitlines()[0])
|
||||
r = f(*args, **kwargs)
|
||||
if mounted:
|
||||
subprocess.run(["umount", path], check=True)
|
||||
return r
|
||||
return mounter
|
||||
return wrapper
|
||||
|
||||
@cli
|
||||
def configure (argv):
|
||||
"""
|
||||
Configure a kernel.
|
||||
===================
|
||||
|
||||
Runs ``make menuconfig`` in the latest kernel's source directory if it is
|
||||
already configured, the current config missing or no other kernel is
|
||||
installed. Otherwise, configure the latest kernel with ``make oldconfig``,
|
||||
using the current kernel config.
|
||||
|
||||
Command Line Arguments
|
||||
----------------------
|
||||
|
||||
``-s <src>``
|
||||
kernel source directory (default: latest)
|
||||
|
||||
``-q``
|
||||
be quiet
|
||||
|
||||
Files
|
||||
-----
|
||||
|
||||
The following files are created in the new kernel's source directory,
|
||||
storing details about changes in the configuration:
|
||||
|
||||
``.newoptions``
|
||||
Newly added configuration options w.r.t. to the previous config (the
|
||||
output of ``make listnewconfig``).
|
||||
|
||||
Process Outline
|
||||
---------------
|
||||
|
||||
This command is a mere wrapper to::
|
||||
|
||||
if [[ ! -f ${old}/.config || $(ls -1dq /usr/src/linux-* | wc -l) == "1"]]
|
||||
then
|
||||
cd ${new}
|
||||
make menuconfig
|
||||
else
|
||||
cp -n ${old}/.config ${new}
|
||||
cd ${new}
|
||||
make listnewconfig > .newoptions
|
||||
make oldconfig || exit
|
||||
fi
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="ekernel-configure",
|
||||
description="Configure a kernel.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
dest="list",
|
||||
action="store_true",
|
||||
help="print newly added config options and exit"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
dest="delete",
|
||||
action="store_true",
|
||||
help="delete config (perform a fresh install / reconfigure)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
metavar="<src>",
|
||||
dest="kernel",
|
||||
type=Kernel,
|
||||
default=Kernel.latest(),
|
||||
help="kernel source directory (default: latest)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
dest="quiet",
|
||||
action="store_true",
|
||||
help="be quiet"
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
out.quiet = args.quiet
|
||||
newoptions = args.kernel.src / ".newoptions"
|
||||
|
||||
# check if current kernel config exists
|
||||
try:
|
||||
oldconfig = Kernel.current().config
|
||||
except FileNotFoundError:
|
||||
oldconfig = Kernel.esp / "FILENOTFOUND"
|
||||
|
||||
# change to source directory
|
||||
os.chdir(args.kernel.src)
|
||||
|
||||
# delete config - reconfigure
|
||||
if args.delete and args.kernel.config.exists():
|
||||
out.einfo(f"deleting {args.kernel.config}")
|
||||
args.kernel.config.unlink()
|
||||
|
||||
# make oldconfig
|
||||
if not args.kernel.config.exists() and oldconfig.exists():
|
||||
# copy oldconfig
|
||||
out.einfo(f"copying {out.hilite(oldconfig)}")
|
||||
shutil.copy(oldconfig, args.kernel.config)
|
||||
# store newly added options
|
||||
out.einfo(f"running {out.hilite('make listnewconfig')}")
|
||||
make = subprocess.run(["make", "listnewconfig"], capture_output=True)
|
||||
newoptions.write_text(make.stdout.decode())
|
||||
# configure
|
||||
if not args.list:
|
||||
out.einfo(f"running {out.hilite('make oldconfig')}")
|
||||
subprocess.run(["make", "oldconfig"], check=True)
|
||||
# make menuconfig
|
||||
elif not args.list:
|
||||
out.einfo(f"running {out.hilite('make menuconfig')}")
|
||||
subprocess.run(["make", "menuconfig"], check=True)
|
||||
|
||||
# check if we should print new options
|
||||
if args.list:
|
||||
if not newoptions.exists():
|
||||
raise FileNotFoundError(f"missing {newoptions}")
|
||||
for l in newoptions.read_text().splitlines():
|
||||
opt, val = l.split("=", maxsplit=1)
|
||||
out.print(f" {opt} = {val}")
|
||||
|
||||
@cli
|
||||
def build (argv):
|
||||
"""
|
||||
Build a kernel.
|
||||
===============
|
||||
|
||||
Build the latest kernel found in ``/usr/src`` or any other by supplying
|
||||
a source directory and install it's modules.
|
||||
|
||||
Command Line Arguments
|
||||
----------------------
|
||||
|
||||
``-j <jobs>``
|
||||
number of parallel make jobs (default: 4)
|
||||
|
||||
``-s <src>``
|
||||
kernel source directory (default: latest)
|
||||
|
||||
``-q``
|
||||
be quiet
|
||||
|
||||
Process Outline
|
||||
---------------
|
||||
|
||||
Changes into the kernel's source directory and builds the image.
|
||||
|
||||
This command is a mere wrapper to::
|
||||
|
||||
cd ${new}
|
||||
make -k ${jobs} && make modules_install
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="ekernel-build",
|
||||
description="Build a kernel.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
metavar="<jobs>",
|
||||
dest="jobs",
|
||||
type=int,
|
||||
default=int(jobs),
|
||||
help=f"number of parallel make jobs (default: {jobs})"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
metavar="<src>",
|
||||
dest="kernel",
|
||||
type=Kernel,
|
||||
default=Kernel.latest(),
|
||||
help="kernel source directory (default: latest)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
dest="quiet",
|
||||
action="store_true",
|
||||
help="be quiet"
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
out.quiet = args.quiet
|
||||
|
||||
# check if config exists
|
||||
if not args.kernel.config.exists():
|
||||
raise FileNotFoundError(f"missing config: {args.kernel.config}")
|
||||
|
||||
# change directory
|
||||
os.chdir(args.kernel.src)
|
||||
|
||||
# build and install modules
|
||||
out.einfo(f"building {out.hilite(args.kernel.src.name)}")
|
||||
subprocess.run(["make", "-j", str(args.jobs)], check=True)
|
||||
out.einfo("installing modules")
|
||||
subprocess.run(["make", "modules_install"], check=True)
|
||||
|
||||
@cli
|
||||
@mount("/boot")
|
||||
def install (argv):
|
||||
"""
|
||||
Install a kernel.
|
||||
=================
|
||||
|
||||
Install the latest kernel found in ``/usr/src`` or any other by supplying
|
||||
it's source directory.
|
||||
|
||||
Command Line Arguments
|
||||
----------------------
|
||||
|
||||
``-s <src>``
|
||||
kernel source directory (default: latest)
|
||||
|
||||
``-q``
|
||||
be quiet
|
||||
|
||||
Process Outline
|
||||
---------------
|
||||
|
||||
Update ``/usr/src`` to the given kernel, install it's ``bzImage`` into the
|
||||
EFI system partition as ``bootx64.efi`` and add a backup copy
|
||||
``gentoo-${version}.efi`` in case something goes horribly wrong.
|
||||
|
||||
This command is a mere wrapper to::
|
||||
|
||||
eselect kernel set $(basename ${src})
|
||||
mount /boot
|
||||
esp=/boot/EFI/Gentoo
|
||||
cp ${src}/arch/x86_64/boot/bzImage ${esp}/bootx64.efi
|
||||
cp ${src}/arch/x86_64/boot/bzImage ${esp}/gentoo-${version}.efi
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="ekernel-install",
|
||||
description="Install a kernel.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
metavar="<src>",
|
||||
dest="kernel",
|
||||
type=Kernel,
|
||||
default=Kernel.latest(),
|
||||
help="kernel source directory (default: latest)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
dest="quiet",
|
||||
action="store_true",
|
||||
help="be quiet"
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
out.quiet = args.quiet
|
||||
|
||||
# check if bzImage exists
|
||||
if not args.kernel.bzImage.exists():
|
||||
raise FileNotFoundError(f"missing bzImage {args.kernel.bzImage}")
|
||||
|
||||
# update symlink to the new source directory
|
||||
out.einfo(
|
||||
"updating symlink "
|
||||
f"{out.hilite(args.kernel.linux)} → {out.hilite(args.kernel.src)}"
|
||||
)
|
||||
subprocess.run(
|
||||
["eselect", "kernel", "set", args.kernel.src.name],
|
||||
check=True
|
||||
)
|
||||
|
||||
# copy boot image
|
||||
out.einfo(f"creating boot image {out.hilite(args.kernel.bootx64)}")
|
||||
shutil.copy(args.kernel.bzImage, args.kernel.bootx64)
|
||||
|
||||
# create backup
|
||||
out.einfo(f"creating backup image {out.hilite(args.kernel.efi)}")
|
||||
shutil.copy(args.kernel.bzImage, args.kernel.efi)
|
||||
|
||||
# rebuild external modules
|
||||
out.einfo(f"rebuilding external kernel modules")
|
||||
subprocess.run(["emerge", "@module-rebuild"], check=True)
|
||||
|
||||
@cli
|
||||
@mount("/boot")
|
||||
def clean (argv):
|
||||
"""
|
||||
Remove unused kernel leftovers.
|
||||
===============================
|
||||
|
||||
Remove unused kernel source directories, modules and boot images.
|
||||
|
||||
The default is to keep the ``k`` previous kernel versions in case something
|
||||
goes horribly wrong.
|
||||
|
||||
Command Line Arguments
|
||||
----------------------
|
||||
|
||||
``-k <num>``
|
||||
keep the previous ``<num>`` kernels (default: 1)
|
||||
|
||||
``-n``
|
||||
perform a dry run (show what would be removed)
|
||||
|
||||
``-q``
|
||||
be quiet
|
||||
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="ekernel-clean",
|
||||
description="Remove unused kernel leftovers.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"-k",
|
||||
metavar="<keep>",
|
||||
dest="keep",
|
||||
type=int,
|
||||
default=1,
|
||||
help="keep the previous <num> bootable kernels (default: 1)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
dest="dry",
|
||||
action="store_true",
|
||||
help="perform a dry run (show what would be removed)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
dest="quiet",
|
||||
action="store_true",
|
||||
help="be quiet"
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
out.quiet = args.quiet
|
||||
if args.keep < 0:
|
||||
raise ValueError("invalid int value: must be greater equal zero")
|
||||
|
||||
# kernels to remove
|
||||
kernels = Kernel.list()
|
||||
def obsolete (k):
|
||||
if args.keep and k.efi.exists() and k.modules.exists():
|
||||
args.keep -= 1
|
||||
return False
|
||||
return True
|
||||
leftovers = filter(obsolete, kernels[kernels.index(Kernel.current()) + 1:])
|
||||
|
||||
# dry run
|
||||
if args.dry:
|
||||
out.einfo("the following kernels will be removed:")
|
||||
for k in leftovers:
|
||||
print(f" {colorize('BAD', '✗')} {k.src.name}")
|
||||
return
|
||||
|
||||
# run depclean
|
||||
subprocess.run(["emerge", "-cq", "gentoo-sources"])
|
||||
|
||||
# remove leftovers
|
||||
for k in leftovers:
|
||||
out.einfo(f"removing {out.hilite(k.src.name)}")
|
||||
shutil.rmtree(k.src)
|
||||
shutil.rmtree(k.modules, ignore_errors=True)
|
||||
k.efi.unlink(missing_ok=True)
|
||||
|
||||
@cli
|
||||
def commit (argv):
|
||||
"""
|
||||
Commit the current kernel config.
|
||||
=================================
|
||||
|
||||
Commit the current kernel config with a detailed commit message.
|
||||
|
||||
This command module is a mere wrapper to::
|
||||
|
||||
git add -f /usr/src/linux/.config
|
||||
git commit -m "${msg}"
|
||||
|
||||
Command Line Arguments
|
||||
----------------------
|
||||
|
||||
``-m``
|
||||
additional information for the commit message
|
||||
|
||||
``-n``
|
||||
perform a dry run (show what would be commited)
|
||||
|
||||
``-q``
|
||||
be quiet
|
||||
|
||||
"""
|
||||
msg = io.StringIO()
|
||||
|
||||
def git (argv: list[str]):
|
||||
"""Run git, capture output and check exit code."""
|
||||
return subprocess.run(["git"] + argv, capture_output=True, check=True)
|
||||
|
||||
def summarize (diff: list[str]):
|
||||
"""Generate the summary of changed options."""
|
||||
def changes (s):
|
||||
return dict([
|
||||
x[1:].split("=", maxsplit=1)
|
||||
for x in diff
|
||||
if x.startswith(s + "CONFIG") and "CC_VERSION" not in x
|
||||
])
|
||||
additions = changes("+")
|
||||
deletions = changes("-")
|
||||
changes = {
|
||||
k: (deletions[k], additions[k])
|
||||
for k in additions.keys() & deletions.keys()
|
||||
}
|
||||
additions = {k: v for k, v in additions.items() if k not in changes}
|
||||
deletions = {k: v for k, v in deletions.items() if k not in changes}
|
||||
if additions:
|
||||
msg.write("\nenabled:\n")
|
||||
for opt, val in additions.items():
|
||||
msg.write(f"* {opt} = {val}\n")
|
||||
if changes:
|
||||
msg.write("\nchanged:\n")
|
||||
for opt, (old, new) in changes.items():
|
||||
msg.write(f"* {opt} = {old} → {new}\n")
|
||||
if deletions:
|
||||
msg.write("\nremoved:\n")
|
||||
for opt, val in deletions.items():
|
||||
msg.write(f"* {opt}\n")
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="ekernel-commit",
|
||||
description="Commit the current kernel config.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
metavar="<msg>",
|
||||
dest="msg",
|
||||
type=str,
|
||||
default="",
|
||||
help="additional information for the commit message"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
dest="dry",
|
||||
action="store_true",
|
||||
help="perform a dry run (show what would be commited)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
dest="quiet",
|
||||
action="store_true",
|
||||
help="be quiet"
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
out.quiet = args.quiet
|
||||
|
||||
# get the kernel under /usr/src/linux
|
||||
kernel = Kernel.current()
|
||||
|
||||
# ensure that a config exists
|
||||
if not kernel.config.exists():
|
||||
raise FileNotFoundError(f"missing config: {kernel.config}")
|
||||
|
||||
# change to source directory
|
||||
os.chdir(kernel.src)
|
||||
|
||||
# ensure that we're in a git repository
|
||||
try:
|
||||
git(["status", "-s"])
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError(e.stderr.decode().strip())
|
||||
|
||||
# ensure that nothing is staged
|
||||
try:
|
||||
git(["diff", "--cached", "--exit-code", "--quiet"])
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError("please commit or stash staged changes")
|
||||
|
||||
# get git root directory
|
||||
gitroot = pathlib.Path(
|
||||
git(["rev-parse", "--show-toplevel"]).stdout.decode().strip()
|
||||
)
|
||||
|
||||
# add unstaged config removals
|
||||
removals = [
|
||||
gitroot / (l.rsplit(maxsplit=1)[1])
|
||||
for l in
|
||||
git(["-P", "diff", "--name-status"]).stdout.decode().splitlines()
|
||||
if l.startswith("D") and "usr/src/linux" in l and ".config" in l
|
||||
]
|
||||
for r in removals: git(["rm", r])
|
||||
|
||||
config_changed = True
|
||||
|
||||
# check if current config is tracked already
|
||||
try:
|
||||
git(["ls-files", "--error-unmatch", kernel.config])
|
||||
|
||||
# config is tracked: check for changes
|
||||
try:
|
||||
git(["-P", "diff", "--exit-code", "--quiet", kernel.config])
|
||||
|
||||
# config hasn't changed: only removals remain
|
||||
config_changed = False
|
||||
if removals:
|
||||
msg.write("removed old kernel leftovers")
|
||||
if args.msg: msg.write(f"\n\n{args.msg}")
|
||||
|
||||
# config changed
|
||||
except subprocess.CalledProcessError:
|
||||
git(["add", "-f", kernel.config])
|
||||
msg.write("updated kernel config\n")
|
||||
if args.msg: msg.write(f"\n{args.msg}\n")
|
||||
summarize(
|
||||
git(["diff", kernel.config]).stdout.decode().splitlines()
|
||||
)
|
||||
|
||||
# config isn't tracked: kernel has been updated
|
||||
except subprocess.CalledProcessError:
|
||||
git(["add", "-f", kernel.config])
|
||||
|
||||
# /usr/src/linux/.config.old (previous config stored by make oldconfig)
|
||||
oldconfig = kernel.src / ".config.old"
|
||||
|
||||
# start header
|
||||
msg.write("kernel ")
|
||||
|
||||
# check if .config.old exists
|
||||
if oldconfig.exists():
|
||||
# get previous version from .config.old, which starts as follows:
|
||||
#
|
||||
# Automatically generated file; DO NOT EDIT.
|
||||
# Linux/x86 X.Y.Z-gentoo Kernel Configuration
|
||||
#
|
||||
with oldconfig.open() as f:
|
||||
f.readline()
|
||||
f.readline()
|
||||
oldversion = version(f.readline())
|
||||
if oldversion.minor != kernel.version.minor:
|
||||
msg.write("upgrade")
|
||||
else:
|
||||
msg.write("update")
|
||||
msg.write(f": {oldversion} → {kernel.version.base_version}\n")
|
||||
|
||||
# append user's message
|
||||
if args.msg: msg.write(f"\n{args.msg}\n")
|
||||
|
||||
# append newly added options (stored in .newoptions)
|
||||
newoptions = kernel.src / ".newoptions"
|
||||
if newoptions.exists():
|
||||
msg.write("\nnew:\n")
|
||||
with newoptions.open() as f:
|
||||
for opt in f.readlines():
|
||||
msg.write(f"* {opt.replace('=', ' = ')}")
|
||||
|
||||
# append summary
|
||||
summarize(list(difflib.unified_diff(
|
||||
oldconfig.read_text().splitlines(),
|
||||
kernel.config.read_text().splitlines()
|
||||
)))
|
||||
else:
|
||||
msg.write(f"{kernel.version}")
|
||||
if args.msg: msg.write(f"\n\n{args.msg}")
|
||||
|
||||
# print changes
|
||||
out.einfo("changes to be committed:")
|
||||
for l in removals:
|
||||
out.print(f" {colorize('QAWARN', '-')} {out.hilite(l)}")
|
||||
if config_changed:
|
||||
out.print(f" {colorize('INFO', '+')} {out.hilite(kernel.config)}")
|
||||
|
||||
# print message
|
||||
out.einfo("commit message:")
|
||||
for l in msg.getvalue().splitlines():
|
||||
out.print(f" {l}" if l else "")
|
||||
|
||||
# dry run: revert staged changes
|
||||
if args.dry:
|
||||
git(["restore", "--staged", kernel.config])
|
||||
return
|
||||
|
||||
# commit
|
||||
try:
|
||||
out.ebegin("committing")
|
||||
git(["commit", "-m", msg.getvalue()])
|
||||
out.eend(0)
|
||||
except subprocess.CalledProcessError as e:
|
||||
out.eend(1)
|
||||
raise RuntimeError(e.stderr.decode())
|
||||
|
||||
@cli
|
||||
def update (argv):
|
||||
"""Custom Gentoo EFI stub kernel updater."""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="ekernel",
|
||||
description="Custom Gentoo EFI stub kernel updater.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
metavar="<jobs>",
|
||||
dest="jobs",
|
||||
type=int,
|
||||
default=int(jobs),
|
||||
help=f"number of parallel make jobs (default: {jobs})"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
metavar="<src>",
|
||||
dest="kernel",
|
||||
type=Kernel,
|
||||
default=Kernel.latest(),
|
||||
help="kernel source directory (default: latest)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-k",
|
||||
metavar="<keep>",
|
||||
dest="keep",
|
||||
type=int,
|
||||
default=1,
|
||||
help="keep the previous <num> bootable kernels (default: 1)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
metavar="<msg>",
|
||||
dest="msg",
|
||||
type=str,
|
||||
default="",
|
||||
help="additional information for the commit message"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
dest="quiet",
|
||||
action="store_true",
|
||||
help="be quiet"
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
args.jobs = ["-j", str(args.jobs)]
|
||||
args.src = ["-s", str(args.kernel.src)]
|
||||
args.keep = ["-k", str(args.keep)]
|
||||
args.msg = ["-m", args.msg]
|
||||
args.quiet = ["-q"] if args.quiet else []
|
||||
|
||||
configure(args.quiet + args.src)
|
||||
build(args.quiet + args.jobs + args.src)
|
||||
install(args.quiet + args.src)
|
||||
clean(args.quiet + args.keep)
|
||||
commit(args.quiet + args.msg)
|
25
pyproject.toml
Normal file
25
pyproject.toml
Normal file
@ -0,0 +1,25 @@
|
||||
[build-system]
|
||||
requires = ["setuptools >= 61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "ekernel"
|
||||
dynamic = ["version"]
|
||||
dependencies = ["packaging", "portage"]
|
||||
readme = "README.md"
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
ekernel = "ekernel:update"
|
||||
ekernel-configure = "ekernel:configure"
|
||||
ekernel-build = "ekernel:build"
|
||||
ekernel-install = "ekernel:install"
|
||||
ekernel-clean = "ekernel:clean"
|
||||
ekernel-commit = "ekernel:commit"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "ekernel.__version__"}
|
121
tests/__init__.py
Normal file
121
tests/__init__.py
Normal file
@ -0,0 +1,121 @@
|
||||
import functools
|
||||
import io
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import portage.output
|
||||
from pkgutil import resolve_name as resolve
|
||||
|
||||
import ekernel
|
||||
|
||||
# disable output
|
||||
ekernel.out.quiet = True
|
||||
|
||||
def git (argv: list[str]):
|
||||
return subprocess.run(["git"] + argv, capture_output=True, check=True)
|
||||
|
||||
def capture_stdout (f):
|
||||
"""A decorator for capturing stdout in a io.StringIO object."""
|
||||
@functools.wraps(f)
|
||||
def capture (*args, **kwargs):
|
||||
quiet = ekernel.out.quiet
|
||||
ekernel.out.quiet = False
|
||||
stdout = sys.stdout
|
||||
sys.stdout = io.StringIO()
|
||||
r = f(*args, **kwargs)
|
||||
sys.stdout = stdout
|
||||
ekernel.out.quiet = quiet
|
||||
return r
|
||||
return capture
|
||||
|
||||
def capture_stderr (f):
|
||||
"""A decorator for capturing stderr in a io.StringIO object."""
|
||||
@functools.wraps(f)
|
||||
def capture (*args, **kwargs):
|
||||
quiet = ekernel.out.quiet
|
||||
ekernel.out.quiet = False
|
||||
stderr = sys.stderr
|
||||
sys.stderr = io.StringIO()
|
||||
r = f(*args, **kwargs)
|
||||
sys.stderr = stderr
|
||||
ekernel.out.quiet = quiet
|
||||
return r
|
||||
return capture
|
||||
|
||||
def colorless (f):
|
||||
"""A decorator for disabling portage's colorful output."""
|
||||
@functools.wraps(f)
|
||||
def nocolor (*args, **kwargs):
|
||||
havecolor = portage.output.havecolor
|
||||
portage.output.havecolor = 0
|
||||
r = f(*args, **kwargs)
|
||||
portage.output.havecolor = havecolor
|
||||
return r
|
||||
return nocolor
|
||||
|
||||
class Interceptor:
|
||||
"""Dynamically intercept, trace and/or replace arbitrary function calls."""
|
||||
|
||||
class Tracer:
|
||||
|
||||
def __init__ (self, interceptor, target, log, call):
|
||||
self.name = f"{target.__module__}.{target.__qualname__}"
|
||||
self.parent = resolve(self.name.rsplit(".", 1)[0])
|
||||
self.interceptor = interceptor
|
||||
self.target = target
|
||||
self.log = log
|
||||
self.call = call
|
||||
|
||||
def start (self):
|
||||
def call (*args, **kwargs):
|
||||
if self.log:
|
||||
self.interceptor.trace.append((self, (args, kwargs)))
|
||||
if callable(self.call):
|
||||
return self.call(self, *args, **kwargs)
|
||||
setattr(self.parent, self.target.__name__, call)
|
||||
|
||||
def stop (self):
|
||||
setattr(self.parent, self.target.__name__, self.target)
|
||||
|
||||
def __init__ (self):
|
||||
self.targets = {}
|
||||
self.trace = []
|
||||
|
||||
def __str__ (self):
|
||||
s = io.StringIO()
|
||||
for tracer, (args, kwargs) in self.trace:
|
||||
s.write(f"{tracer.name}\n")
|
||||
for a in args:
|
||||
s.write(f" {a}\n")
|
||||
for k, v in kwargs.items():
|
||||
s.write(f" {k} = {v}\n")
|
||||
return s.getvalue()
|
||||
|
||||
def add (self, target, log=True, call=None):
|
||||
"""
|
||||
Intercept calls to the given function.
|
||||
|
||||
Args:
|
||||
target: the intercepted function object
|
||||
log (bool): trace calls if True
|
||||
call: function to be called instead
|
||||
"""
|
||||
if target in self.targets:
|
||||
raise RuntimeError(f"{self.targets[target].name} already caught")
|
||||
if not callable(target):
|
||||
raise RuntimeError(f"{target.__name__} is not callable")
|
||||
self.targets[target] = self.Tracer(self, target, log, call)
|
||||
|
||||
def remove (self, target):
|
||||
"""Stop intercepting calls to the given function."""
|
||||
if target not in self.targets:
|
||||
raise RuntimeError(f"{target.__name__} not being caught")
|
||||
del self.targets[target]
|
||||
|
||||
def start (self):
|
||||
"""Start intercepting calls to the registered functions."""
|
||||
for tracer in self.targets.values(): tracer.start()
|
||||
|
||||
def stop (self):
|
||||
"""Stop intercepting calls to the registered functions."""
|
||||
for tracer in self.targets.values(): tracer.stop()
|
0
tests/data/__init__.py
Normal file
0
tests/data/__init__.py
Normal file
104
tests/data/kernel.py
Normal file
104
tests/data/kernel.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""Setup the kernel test environment."""
|
||||
import pathlib
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from ekernel import Kernel
|
||||
|
||||
# create temporary directory
|
||||
tmpdir = tempfile.TemporaryDirectory()
|
||||
root = pathlib.Path(tmpdir.name)
|
||||
|
||||
# kernel source directory
|
||||
src = root / "usr/src"
|
||||
|
||||
# kernel source symlink
|
||||
linux = src / "linux"
|
||||
|
||||
# kernel module directory
|
||||
modules = root / "lib/modules"
|
||||
|
||||
# EFI system partition
|
||||
esp = root / "boot/EFI/Gentoo"
|
||||
|
||||
# boot image
|
||||
bootx64 = esp / "bootx64.efi"
|
||||
|
||||
# change Kernel class' root directory
|
||||
Kernel.src = src
|
||||
Kernel.linux = linux
|
||||
Kernel.modules = modules
|
||||
Kernel.esp = esp
|
||||
Kernel.bootx64 = bootx64
|
||||
|
||||
# list of installed kernels
|
||||
kernels = []
|
||||
sources = [
|
||||
src / "linux-5.15.23-gentoo",
|
||||
# all except the lastest have been built
|
||||
src / "linux-5.15.16-gentoo",
|
||||
src / "linux-5.15.3-gentoo",
|
||||
src / "linux-5.15.2-gentoo",
|
||||
src / "linux-5.15.1-gentoo-r1"
|
||||
]
|
||||
|
||||
# currently installed kernel
|
||||
current = sources[1]
|
||||
|
||||
# latest available kernel
|
||||
latest = sources[0]
|
||||
|
||||
# current config
|
||||
oldconfig = f"""\
|
||||
#
|
||||
# Automatically generated file; DO NOT EDIT.
|
||||
# Linux/x86 {current}-gentoo Kernel Configuration
|
||||
#
|
||||
CONFIG_A=y
|
||||
CONFIG_B=y
|
||||
CONFIG_C=y
|
||||
"""
|
||||
|
||||
# new options
|
||||
newoptions = """\
|
||||
CONFIG_D=n
|
||||
CONFIG_E=n
|
||||
CONFIG_F=n
|
||||
"""
|
||||
|
||||
# new config
|
||||
newconfig = """\
|
||||
CONFIG_A=y
|
||||
CONFIG_C=m
|
||||
CONFIG_D=y
|
||||
CONFIG_F=y
|
||||
"""
|
||||
|
||||
def setup ():
|
||||
"""Setup the kernel test environment."""
|
||||
# remove any existing files
|
||||
for p in root.glob("*"):
|
||||
shutil.rmtree(p)
|
||||
|
||||
# create EFI system partition
|
||||
esp.mkdir(parents=True)
|
||||
|
||||
# create Kernels
|
||||
for s in sources: s.mkdir(parents=True)
|
||||
global kernels
|
||||
kernels = [ Kernel(s) for s in sources ]
|
||||
|
||||
# create config and build files, expect for the latest
|
||||
for k in kernels:
|
||||
k.bzImage.parent.mkdir(parents=True)
|
||||
if k.src == latest: continue
|
||||
if k.src == current:
|
||||
k.config.write_text(oldconfig)
|
||||
else:
|
||||
k.config.touch()
|
||||
k.bzImage.touch()
|
||||
k.efi.touch()
|
||||
k.modules.mkdir(parents=True)
|
||||
|
||||
# symlink to old source directory
|
||||
linux.symlink_to(current)
|
87
tests/test_build.py
Normal file
87
tests/test_build.py
Normal file
@ -0,0 +1,87 @@
|
||||
import pathlib
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from ekernel import Kernel
|
||||
from tests import capture_stderr, Interceptor
|
||||
import tests.data.kernel as data
|
||||
|
||||
import ekernel
|
||||
|
||||
def run (*argv):
|
||||
return ekernel.build(list(argv))
|
||||
|
||||
class Tests (unittest.TestCase):
|
||||
|
||||
def setUp (self):
|
||||
# start interceptor
|
||||
self.interceptor = Interceptor()
|
||||
def run (tracer, *args, **kwargs):
|
||||
if args[0][0] == "make" and args[0][1] == "-j":
|
||||
self.kernel.bzImage.touch()
|
||||
self.interceptor.add(subprocess.run, call=run)
|
||||
self.interceptor.start()
|
||||
# setup test environment
|
||||
data.setup()
|
||||
self.kernel = Kernel.latest()
|
||||
self.kernel.config.touch()
|
||||
self.jobs = "4"
|
||||
|
||||
def tearDown (self):
|
||||
# stop interceptor
|
||||
self.interceptor.stop()
|
||||
|
||||
def check_build (self):
|
||||
self.assertEqual(pathlib.Path.cwd(), self.kernel.src)
|
||||
trace_it = iter(self.interceptor.trace)
|
||||
# make -j <jobs>
|
||||
tracer, (args, kwargs) = next(trace_it)
|
||||
self.assertEqual(tracer.name, "subprocess.run")
|
||||
self.assertEqual(args, (["make", "-j", self.jobs],))
|
||||
self.assertEqual(kwargs, {"check": True})
|
||||
self.assertTrue(self.kernel.bzImage.exists())
|
||||
# make modules_install
|
||||
tracer, (args, kwargs) = next(trace_it)
|
||||
self.assertEqual(tracer.name, "subprocess.run")
|
||||
self.assertEqual(args, (["make", "modules_install"],))
|
||||
self.assertEqual(kwargs, {"check": True})
|
||||
|
||||
def test_build (self):
|
||||
self.assertEqual(run("-q"), 0)
|
||||
self.check_build()
|
||||
|
||||
def test_build_jobs (self):
|
||||
self.jobs = "128"
|
||||
self.assertEqual(run("-q", "-j", self.jobs), 0)
|
||||
self.check_build()
|
||||
|
||||
@capture_stderr
|
||||
def test_build_jobs_illegal (self):
|
||||
with self.assertRaises(SystemExit):
|
||||
run("-j", "foo")
|
||||
|
||||
def test_build_source (self):
|
||||
self.kernel = Kernel.current()
|
||||
self.kernel.config.touch()
|
||||
self.assertEqual(run("-q", "-s", str(data.current)), 0)
|
||||
self.check_build()
|
||||
|
||||
@capture_stderr
|
||||
def test_build_source_missing (self):
|
||||
with self.assertRaises(SystemExit):
|
||||
run("-s", str(data.src / "linux-0.0.0-gentoo"))
|
||||
|
||||
def test_build_source_missing_config (self):
|
||||
Kernel.latest().config.unlink()
|
||||
with self.assertRaises(SystemExit):
|
||||
self.assertEqual(run("-q", "-s", str(data.latest)), 1)
|
||||
|
||||
@capture_stderr
|
||||
def test_build_source_illegal (self):
|
||||
with self.assertRaises(SystemExit):
|
||||
run("-s", str(data.root))
|
||||
|
||||
def test_build_jobs_source (self):
|
||||
self.jobs = "128"
|
||||
self.assertEqual(run("-q", "-j", self.jobs, "-s", str(data.latest)), 0)
|
||||
self.check_build()
|
106
tests/test_clean.py
Normal file
106
tests/test_clean.py
Normal file
@ -0,0 +1,106 @@
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from ekernel import Kernel
|
||||
from tests import capture_stdout, colorless, Interceptor
|
||||
import tests.data.kernel as data
|
||||
|
||||
import ekernel
|
||||
|
||||
def run (*argv):
|
||||
return ekernel.clean(list(argv))
|
||||
|
||||
class Tests (unittest.TestCase):
|
||||
|
||||
def setUp (self):
|
||||
# setup test environment
|
||||
data.setup()
|
||||
# update src symlink to new kernel
|
||||
data.linux.unlink()
|
||||
data.linux.symlink_to(data.latest)
|
||||
# initialize git repository
|
||||
os.chdir(data.root)
|
||||
for k in data.kernels[:-2]:
|
||||
if not k.config.exists():
|
||||
k.config.touch()
|
||||
if not k.modules.exists():
|
||||
k.modules.mkdir(parents=True)
|
||||
if not k.efi.exists():
|
||||
k.efi.touch()
|
||||
# start interceptor
|
||||
self.interceptor = Interceptor()
|
||||
self.interceptor.add(subprocess.run, call=True)
|
||||
self.interceptor.start()
|
||||
|
||||
def tearDown (self):
|
||||
# stop interceptor
|
||||
self.interceptor.stop()
|
||||
|
||||
def check_clean (self, keep=1):
|
||||
split = data.kernels.index(Kernel.current()) + keep + 1
|
||||
trace_it = iter(self.interceptor.trace)
|
||||
# mount /boot
|
||||
tracer, (args, kwargs) = next(trace_it)
|
||||
self.assertEqual(tracer.name, "subprocess.run")
|
||||
self.assertEqual(args, (["mount", "/boot"],))
|
||||
self.assertEqual(kwargs, {"capture_output": True, "check": True})
|
||||
# emerge -cq gentoo-sources
|
||||
tracer, (args, kwargs) = next(trace_it)
|
||||
self.assertEqual(tracer.name, "subprocess.run")
|
||||
self.assertEqual(args, (["emerge", "-cq", "gentoo-sources"],))
|
||||
self.assertEqual(kwargs, {})
|
||||
for k in data.kernels[:split]:
|
||||
self.assertTrue(k.src.exists())
|
||||
self.assertTrue(k.modules.exists())
|
||||
self.assertTrue(k.efi.exists())
|
||||
for k in data.kernels[split:]:
|
||||
self.assertFalse(k.src.exists())
|
||||
self.assertFalse(k.modules.exists())
|
||||
self.assertFalse(k.efi.exists())
|
||||
# umount /boot
|
||||
tracer, (args, kwargs) = next(trace_it)
|
||||
self.assertEqual(tracer.name, "subprocess.run")
|
||||
self.assertEqual(args, (["umount", "/boot"],))
|
||||
self.assertEqual(kwargs, {"check": True})
|
||||
|
||||
def test_clean (self):
|
||||
self.assertEqual(run("-q"), 0)
|
||||
self.check_clean()
|
||||
|
||||
def test_clean_missing_efi (self):
|
||||
data.kernels[-1].efi.unlink()
|
||||
self.assertEqual(run("-q"), 0)
|
||||
self.check_clean()
|
||||
|
||||
def test_clean_missing_modules (self):
|
||||
shutil.rmtree(data.kernels[-1].modules)
|
||||
self.assertEqual(run("-q"), 0)
|
||||
self.check_clean()
|
||||
|
||||
def test_clean_keep_2 (self):
|
||||
self.assertEqual(run("-q", "-k", "2"), 0)
|
||||
self.check_clean(2)
|
||||
|
||||
def test_clean_keep_none (self):
|
||||
self.assertEqual(run("-q", "-k", "0"), 0)
|
||||
self.check_clean(0)
|
||||
|
||||
def test_clean_keep_gt_available (self):
|
||||
self.assertEqual(run("-q", "-k", "10"), 0)
|
||||
self.check_clean(10)
|
||||
|
||||
@colorless
|
||||
@capture_stdout
|
||||
def test_clean_dry_run (self):
|
||||
self.assertEqual(run("-n"), 0)
|
||||
for src in data.sources:
|
||||
self.assertTrue(src.exists())
|
||||
expected = io.StringIO()
|
||||
expected.write(f" * the following kernels will be removed:\n")
|
||||
for k in data.kernels[2:]:
|
||||
expected.write(f" ✗ {k.src.name}\n")
|
||||
self.assertEqual(sys.stdout.getvalue(), expected.getvalue())
|
223
tests/test_commit.py
Normal file
223
tests/test_commit.py
Normal file
@ -0,0 +1,223 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from ekernel import Kernel
|
||||
from tests import git, capture_stdout, capture_stderr, colorless
|
||||
import tests.data.kernel as data
|
||||
|
||||
import ekernel
|
||||
|
||||
def run (*argv):
|
||||
return ekernel |