diff --git a/ekernel.py b/ekernel.py index 38072fb..68be099 100644 --- a/ekernel.py +++ b/ekernel.py @@ -21,7 +21,7 @@ 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)) +out.emph = lambda s: colorize("HILITE", str(s)) # disable colorization for pipes and redirects if not sys.stdout.isatty(): @@ -56,7 +56,7 @@ class Kernel: raise ValueError(f"illegal source: {src}") from e self.config = self.src / ".config" self.bzImage = self.src / "arch/x86_64/boot/bzImage" - self.bkp = efi.boot.parent / f"gentoo-{self.version.base_version}.efi" + self.bkp = efi.img.parent / f"gentoo-{self.version.base_version}.efi" self.modules = self.modules / f"{self.version.base_version}-gentoo" def __eq__ (self, other): @@ -121,18 +121,17 @@ def cli (f): return handler def efi (f): - """Decorator locating and mounting the ESP through efivars.""" + """Decorator locating and mounting ESP through efivars.""" efi.skip = False - # system partition + # boot partition efi.esp = pathlib.Path("/boot") - # bootloader (stub kernel) - efi.boot = efi.esp / "EFI/Gentoo/bootx64.efi" + # boot image + efi.img = efi.esp / "EFI/Gentoo/bootx64.efi" # backup entry data efi.bkp = {} # analyze boot entries and ensure access to the currently running image @functools.wraps(f) def locator (*args, **kwargs): - # skip detection if efi.skip: return f(*args, **kwargs) efi.skip = True @@ -142,47 +141,45 @@ def efi (f): capture_output=True, check=True ) - entries = mgr.stdout.decode().splitlines() - bootnum = "NaN" - for l in entries: + lines = mgr.stdout.decode().splitlines() + num = "NaN" + for l in lines: if l.startswith("BootCurrent"): - bootnum = l.split()[1] + num = l[13:17] break - # find currently running boot entry / loader - def parse (entry): - # label - i = l.find(" ") + 1 - j = l.find("\t", i) - label = l[i:j] - # loader - i = l.find("File", j) - if i < 0: - raise RuntimeError(f"error locating bootloader:\n{l}") + # find currently running entry/image + def loader (line, start=9): + i = line.find("File", start) + if i < 0: raise RuntimeError(f"error locating boot image:\n{line}") i += 6 - j = l.find(")", i) - loader = pathlib.Path(l[i:j].replace("\\", "/")) - return label, loader - for l in entries: - if l.startswith(f"Boot{bootnum}"): - label, loader = parse(l) - efi.label = label - efi.bkp["label"] = f"{label} (fallback)" + return pathlib.Path(l[i:line.find(")", i)].replace("\\", "/")) + for l in lines: + if l.startswith(f"Boot{num}"): + i = l.find(" ") + 1 + j = l.find("\t", i) + efi.label = l[i:j] + efi.bkp["label"] = f"{efi.label} (fallback)" + img = loader(l, j) break - # find bootnum of backup entry - for l in entries: + # find fallback entry/image + for l in lines: if efi.bkp["label"] in l: - efi.bkp["bootnum"] = l[4:8] + efi.bkp["num"] = l[4:8] + efi.bkp["img"] = loader(l) break # mount esp mounted = False - if not efi.boot.exists(): + if not efi.img.exists(): # find mountpoint for l in pathlib.Path("/etc/fstab").read_text().splitlines(): if not l.startswith("#"): for p in ["/boot", "/efi"]: if p in l: + # update paths efi.esp = pathlib.Path(p) - efi.boot = efi.esp / loader + efi.img = efi.esp / img + if efi.bkp and "img" in efi.bkp: + efi.bkp["img"] = efi.esp / efi.bkp["img"] break else: continue break @@ -199,7 +196,7 @@ def efi (f): msg = e.stderr.decode().strip() if f"already mounted on {efi.esp}" not in msg: raise RuntimeError(e.stderr.decode().splitlines()[0]) - assert efi.boot.exists() + assert efi.img.exists() try: return f(*args, **kwargs) finally: @@ -308,19 +305,19 @@ def configure (argv): # make oldconfig if not kernel.config.exists() and oldconfig.exists(): # copy oldconfig - out.einfo(f"copying {out.hilite(oldconfig)}") + out.einfo(f"copying {out.emph(oldconfig)}") shutil.copy(oldconfig, kernel.config) # store newly added options - out.einfo(f"running {out.hilite('make listnewconfig')}") + out.einfo(f"running {out.emph('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')}") + out.einfo(f"running {out.emph('make oldconfig')}") subprocess.run(["make", "oldconfig"], check=True) # make menuconfig elif not args.list: - out.einfo(f"running {out.hilite('make menuconfig')}") + out.einfo(f"running {out.emph('make menuconfig')}") subprocess.run(["make", "menuconfig"], check=True) # check if we should print new options @@ -401,7 +398,7 @@ def build (argv): os.chdir(kernel.src) # build and install modules - out.einfo(f"building {out.hilite(kernel.src.name)}") + out.einfo(f"building {out.emph(kernel.src.name)}") subprocess.run(["make", "-j", str(args.jobs)], check=True) out.einfo("installing modules") subprocess.run(["make", "modules_install"], check=True) @@ -476,21 +473,21 @@ def install (argv): if not kernel.bzImage.exists(): raise FileNotFoundError(f"missing bzImage {kernel.bzImage}") - # create backup boot entry + # create fallback boot entry if args.bkp: # path to backup image bkp = None # find the currently running kernel's backup image - boot_bytes = efi.boot.read_bytes() - for f in efi.boot.parent.glob("gentoo*.efi"): + boot_bytes = efi.img.read_bytes() + for f in efi.img.parent.glob("gentoo*.efi"): if f.read_bytes() == boot_bytes: bkp = f break # not found else: name = f"gentoo-{version(platform.release()).base_version}.efi" - bkp = efi.boot.parent / name - shutil.copy(efi.boot, bkp) + bkp = efi.img.parent / name + shutil.copy(efi.img, bkp) # get ESP disk and partition number dev = subprocess.run( ["findmnt", "-rno", "SOURCE", str(efi.esp)], @@ -502,11 +499,11 @@ def install (argv): re.search(r"([/a-z]+)(\d+)", dev.stdout.decode()).groups() ) # remove previous entry - if "bootnum" in efi.bkp: + if "num" in efi.bkp: subprocess.run([ "efibootmgr", "-q", - "-b", efi.bkp["bootnum"], + "-b", efi.bkp["num"], "-B" ], check=True) # create entry @@ -519,11 +516,12 @@ def install (argv): "-L", efi.bkp["label"], "-l", str(bkp) ], check=True) + efi.bkp["img"] = bkp # update symlink to the new source directory out.einfo( "updating symlink " - f"{out.hilite(kernel.linux)} → {out.hilite(kernel.src)}" + f"{out.emph(kernel.linux)} → {out.emph(kernel.src)}" ) subprocess.run( ["eselect", "kernel", "set", kernel.src.name], @@ -531,11 +529,11 @@ def install (argv): ) # copy boot image - out.einfo(f"creating boot image {out.hilite(efi.boot)}") - shutil.copy(kernel.bzImage, efi.boot) + out.einfo(f"creating boot image {out.emph(efi.img)}") + shutil.copy(kernel.bzImage, efi.img) # create backup - out.einfo(f"creating backup image {out.hilite(kernel.bkp)}") + out.einfo(f"creating backup image {out.emph(kernel.bkp)}") shutil.copy(kernel.bzImage, kernel.bkp) # rebuild external modules @@ -620,30 +618,43 @@ def clean (argv): if d not in keep["modules"] ] - # collect boot loaders - keep["boot loaders"] = {k.bkp for k in keep["kernels"]} - rm["boot loaders"] = [ + # collect boot images + keep["images"] = {k.bkp for k in keep["kernels"]} + rm["images"] = [ f - for f in efi.boot.parent.glob("gentoo-*") - if f not in keep["boot loaders"] + for f in efi.img.parent.glob("gentoo-*") + if f not in keep["images"] ] # run depclean - out.einfo(f"running {out.hilite('emerge -cq gentoo-sources')}") + out.einfo(f"running {out.emph('emerge -cq gentoo-sources')}") if not args.dry: subprocess.run(["emerge", "-cq", "gentoo-sources"]) - # remove leftovers + # remove files for k, v in rm.items(): out.einfo(f"deleting {k}:") for p in v: - out.print(f" {colorize('BAD', '✗')} {out.hilite(p)}") + out.print(f" {colorize('BAD', '✗')} {out.emph(p)}") if args.dry: continue if p.is_dir(): shutil.rmtree(p) else: p.unlink() + # remove defunct fallback boot entry + if efi.bkp and "img" in efi.bkp: + bkp = efi.bkp["img"] + if not bkp.exists() or bkp in rm["images"]: + out.einfo(f"deleting boot entry {out.emph(efi.bkp['label'])}") + if not args.dry: + subprocess.run([ + "efibootmgr", + "-q", + "-b", efi.bkp["num"], + "-B" + ], check=True) + @cli def commit (argv): """ @@ -845,9 +856,9 @@ def commit (argv): if removals or config_changed: out.einfo("changes to be committed:") for l in removals: - out.print(f" {colorize('QAWARN', '-')} {out.hilite(l)}") + out.print(f" {colorize('QAWARN', '-')} {out.emph(l)}") if config_changed: - out.print(f" {colorize('INFO', '+')} {out.hilite(kernel.config)}") + out.print(f" {colorize('INFO', '+')} {out.emph(kernel.config)}") # print message if msg: diff --git a/tests/data/kernel.py b/tests/data/kernel.py index dea96f2..b2237ea 100644 --- a/tests/data/kernel.py +++ b/tests/data/kernel.py @@ -20,7 +20,7 @@ linux = src / "linux" # kernel module directory modules = tmp / "lib/modules" -# EFI system partition +# EFI system partition (/boot -> /tmp) esp = tmp.parents[-2] # boot image @@ -79,15 +79,17 @@ def efi (f): "Timeout: 1 seconds\n" "BootOrder: 0001,0000\n" "Boot0000* Windows\tHD()/File()\n" - "Boot0001* Gentoo\tHD()/File(\\EFI\\Gentoo\\bootx64.efi)\n" + f"Boot0001* Gentoo\tHD()/File(\\EFI\\Gentoo\\{boot.name})\n" "Boot0002* Gentoo (ignore)\tHD()/File()\n" - "Boot0003* Gentoo (fallback)\tHD()/File()\n" + "Boot0003* Gentoo (fallback)\tHD()" + f"/File(\\EFI\\Gentoo\\{kernels[2].bkp.name})\n" .encode() ) elif args[0][0] == "mount": ekernel.efi.esp = esp - ekernel.efi.boot = boot + ekernel.efi.img = boot boot.write_bytes(str(kernels[1].bkp).encode()) + ekernel.efi.bkp["img"] = boot.parent / ekernel.efi.bkp["img"].name return f(t, *args, **kwargs) return runner @@ -104,7 +106,7 @@ def setup (): # change EFI paths ekernel.efi.esp = esp - ekernel.efi.boot = boot + ekernel.efi.img = boot # create EFI system partition boot.parent.mkdir(parents=True) diff --git a/tests/test_clean.py b/tests/test_clean.py index c9e6981..1e2df97 100644 --- a/tests/test_clean.py +++ b/tests/test_clean.py @@ -59,6 +59,18 @@ class Tests (unittest.TestCase): self.assertEqual(tracer.name, "subprocess.run") self.assertEqual(args, (["emerge", "-cq", "gentoo-sources"],)) self.assertEqual(kwargs, {}) + if keep < 3: + # efibootmgr -b 0003 -B + tracer, (args, kwargs) = next(trace_it) + self.assertEqual(tracer.name, "subprocess.run") + self.assertEqual(args, (["efibootmgr", "-q", "-b", "0003", "-B"],)) + self.assertEqual(kwargs, {"check": True}) + # umount /boot + tracer, (args, kwargs) = next(trace_it) + self.assertEqual(tracer.name, "subprocess.run") + self.assertEqual(args, (["umount", "/tmp"],)) + self.assertEqual(kwargs, {"check": True}) + # check files for k in data.kernels[:keep]: self.assertTrue(k.src.exists()) self.assertTrue(k.modules.exists()) @@ -67,11 +79,6 @@ class Tests (unittest.TestCase): self.assertFalse(k.src.exists()) self.assertFalse(k.modules.exists()) self.assertFalse(k.bkp.exists()) - # umount /boot - tracer, (args, kwargs) = next(trace_it) - self.assertEqual(tracer.name, "subprocess.run") - self.assertEqual(args, (["umount", "/tmp"],)) - self.assertEqual(kwargs, {"check": True}) def test_clean (self): self.assertEqual(run("-q"), 0) @@ -87,9 +94,9 @@ class Tests (unittest.TestCase): 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_3 (self): + self.assertEqual(run("-q", "-k", "3"), 0) + self.check_clean(3) def test_clean_keep_none (self): with self.assertRaises(SystemExit): @@ -118,7 +125,7 @@ class Tests (unittest.TestCase): rm = { "sources": [k.src for k in kernels], "modules": [k.modules for k in kernels], - "boot loaders": [k.bkp for k in kernels] + "images": [k.bkp for k in kernels] } expected = io.StringIO() expected.write(" * running emerge -cq gentoo-sources\n") @@ -126,4 +133,5 @@ class Tests (unittest.TestCase): expected.write(f" * deleting {k}:\n") for p in v: expected.write(f" ✗ {p}\n") + expected.write(" * deleting boot entry Gentoo (fallback)\n") self.assertEqual(sys.stdout.getvalue(), expected.getvalue()) diff --git a/tests/test_install.py b/tests/test_install.py index f4cbba9..969547a 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -99,7 +99,7 @@ class Tests (unittest.TestCase): self.assertEqual(args, (["umount", "/tmp"],)) self.assertEqual(kwargs, {"check": True}) # check generated files - self.assertTrue(ekernel.efi.boot.exists()) + self.assertTrue(ekernel.efi.img.exists()) self.assertTrue(self.kernel.bkp.exists()) def test_install (self): diff --git a/tests/test_update.py b/tests/test_update.py index 5a0363e..408d0eb 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -62,7 +62,7 @@ class Tests (unittest.TestCase): self.assertTrue(self.oldconfig.exists()) self.assertTrue(self.latest.config.exists()) # install - self.assertTrue(ekernel.efi.boot.exists()) + self.assertTrue(ekernel.efi.img.exists()) self.assertTrue(self.latest.bkp.exists()) # clean for k in data.kernels[2:]: