#!/usr/bin/env python import argparse import magic import os import shutil import subprocess import sys from pathlib import Path from typing import Any, Dict, List, Tuple BOOT_FILES = { 'config': Path('config-gentoo'), 'initramfs': Path('initramfs-gentoo.img'), 'kernel': Path('vmlinuz-gentoo'), 'system_map': Path('System.map-gentoo'), } SRC_DIR = Path('/usr/src') BOOT_DIR = Path('/boot') def backup_kernel(boot_dir: Path, files: Dict[str, Path]) -> None: for f in files.values(): src = boot_dir/f dst = boot_dir/(f.name + '.old') print(f'Backing-up {src} to {dst}') shutil.copy2(src, dst) def rollback_impl(boot_dir: Path, files: Dict[str, Path]) -> None: for f in files.values(): src = boot_dir/(f.name + '.old') dst = boot_dir/f print(f'Restoring {src} to {dst}') shutil.copy2(src, dst) def update_config(old_dir: Path, new_dir: Path, make_cmd: List[str]) -> None: if old_dir == new_dir: return old_config = old_dir/'.config' new_config = new_dir/'.config' while new_config.is_file(): response = input('New config present. Overwrite? [y/N]').strip().lower() if response == 'y': break elif response == '' or response == 'n': return else: print("unrecognized option {}", response) print(f'Copying config from {old_config} to {new_config}') shutil.copy2(old_config, new_config) print(f'Setting symlink to {new_dir}') subprocess.run(['eselect', 'kernel', 'set', new_dir.name]) print(f'Migrating config options') migrate = make_cmd + ['-C', new_dir.as_posix(), 'oldconfig'] subprocess.run(migrate) menuconfig = make_cmd + ['-C', new_dir.as_posix(), 'menuconfig'] while True: subprocess.run(menuconfig) response = input('Stop editing? [Y/n]').strip().lower() if response == '' or response == 'y': break elif response == 'n': continue else: print("unrecognized option {}", response) def compile_kernel(new_dir: Path, make_cmd: List[str]) -> None: cc = make_cmd + ['-C', new_dir.as_posix()] subprocess.run(cc) def install_kernel(kernel_dir: Path, boot_dir: Path, boot_files: Dict[str, Path], make_cmd: List[str]) -> None: make_files = { 'config': Path('.config'), 'system_map': Path('System.map'), 'kernel': Path('arch/x86/boot/bzImage') } config = (kernel_dir/make_files['config']).as_posix() # subprocess.run(make_cmd.extend(['-C', kernel_dir, 'install'])) # this would create unwanted entries common_keys = make_files.keys() & boot_files.keys() for key in common_keys: src = kernel_dir/make_files[key] dst = boot_dir/boot_files[key] print(f'Installing {src} to {dst}') shutil.copy2(src, dst) install_modules = make_cmd + ['-C', kernel_dir.as_posix(), 'modules_install'] subprocess.run(install_modules) genkernel = ['genkernel', f'--kernel-config={config}', '--microcode', 'initramfs'] subprocess.run(genkernel) def linux_folder(src_dir: Path, version: str) -> Path: revision = '' version = version.split('-') if len(version) > 1: revision = '-' + version[1] version = version[0] return (src_dir / (f'linux-{version}-gentoo{revision}')) def module_check(boot_dir: Path, boot_files: Dict[str, Path]) -> Tuple[bool, Path]: kernel_name = boot_files['kernel'].name + '.old' old_kernel: Path = boot_dir/kernel_name magic_list = magic.from_file(old_kernel).split() version = Path(magic_list[magic_list.index('version') + 1]) modules = Path('/lib/modules')/version if not modules.exists(): return (False, version) return (True, version) def rollback_kernel(boot_dir: Path, boot_files: Dict[str, Path], _args: Any) -> None: check = module_check(boot_dir, boot_files) if not check[0]: err = f'Module directory not found for {check[1]}.\nRefusing to proceed.' raise RuntimeError(err) rollback_impl(boot_dir, boot_files) def update_kernel(boot_dir: Path, boot_files: Dict[str, Path], args: Any) -> None: old_dir = linux_folder(SRC_DIR, args.old_version) new_dir = linux_folder(SRC_DIR, args.new_version) new_version = new_dir.name make_cmd = ['make', f'-j{len(os.sched_getaffinity(0))}'] clang_env = ['CC=clang', 'LD=ld.lld', 'LLVM=1', 'LLVM_IAS=1'] if args.llvm: make_cmd.extend(clang_env) none_selected = not (args.backup or args.config or args.compile or args.install or args.rollback) if none_selected: backup_kernel(BOOT_DIR, BOOT_FILES) update_config(old_dir, new_dir, make_cmd) compile_kernel(new_dir, make_cmd) install_kernel(new_dir, BOOT_DIR, BOOT_FILES, make_cmd) if args.backup: backup_kernel(BOOT_DIR, BOOT_FILES) if args.config: update_config(old_dir, new_dir, make_cmd) if args.compile: compile_kernel(new_dir, make_cmd) if args.install: install_kernel(new_dir, BOOT_DIR, BOOT_FILES, make_cmd) def main() -> None: parser = argparse.ArgumentParser(description='Convenience for manual kernel updates') subparsers = parser.add_subparsers() rollback = subparsers.add_parser('rollback') rollback.set_defaults(func=rollback_kernel) update = subparsers.add_parser('update', usage=f'{sys.argv[0]} update 5.15.12 5.16.3', ) update.add_argument( '--llvm', '-l', action='store_true', help="Use clang/llvm to compile kernel") update.add_argument( '--backup', '-b', action='store_true', help="Backup old kernel files as .old") update.add_argument( '--rollback', '-r', action='store_true', help='Restore .old kernel files as main boot choice') update.add_argument( '--config', '-C', action='store_true', help='Migrate config from old kernel') update.add_argument( '--compile', '-c', action='store_true', help='Compile new kernel') update.add_argument( '--install', '-i', action='store_true', help='Install new kernel') update.add_argument('old_version', help='Old kernel version') update.add_argument('new_version', help='New kernel version') update.set_defaults(func=update_kernel) args = parser.parse_args() args.func(BOOT_DIR, BOOT_FILES, args) if __name__ == "__main__": main()