Fiw-Gentoo-Dots/lib/boot.py
Fi3w0 0a2590256e
All checks were successful
Check and mirror to GitHub / checks (push) Successful in 25s
Check and mirror to GitHub / github-mirror (push) Successful in 9s
Sync custom kernel to CachyOS 7.2.10-1 and fix overlay and ESP refresh
- Kernel snapshot, patch and guard now match the running 7.2.10-fiw-nyx-v1.2.
- Overlay install replaces the tree instead of merging, so removed ebuilds
  cannot linger next to a newer Manifest; the old tree is restored on failure.
- Limine refresh removes ESP kernel folders the managed menu no longer lists.
- ESP discovery checks fstab FAT mounts, triggers x-systemd.automount and
  ignores the autofs placeholder.
2026-10-15 23:34:55 +02:00

512 lines
25 KiB
Python

#!/usr/bin/env python3
"""Preview and deploy a separate amd64 UEFI Limine or GRUB installation."""
import argparse
import hashlib
import json
import os
import platform
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
STATE = Path('/etc/fiw-gentoo-dots/boot.json')
MANAGED = Path('/var/lib/Fiw-Gentoo-Dots')
BEGIN = '# >>> Fiw-Gentoo-Dots kernels'
END = '# <<< Fiw-Gentoo-Dots kernels'
EFI_DIRECTORY = 'EFI/Fiw-Gentoo'
GRUB_DIRECTORY = Path('/boot/fiw-gentoo')
def mount_info(path, exact=False):
result = subprocess.run(['findmnt', '--json', '--mountpoint' if exact else '--target',
str(path), '--output', 'TARGET,SOURCE,FSTYPE,UUID,OPTIONS'],
text=True, capture_output=True)
if result.returncode:
raise RuntimeError('Not a mounted filesystem: ' + str(path))
# An x-systemd.automount point lists its autofs placeholder first.
mounted = [fs for fs in json.loads(result.stdout)['filesystems'] if fs['fstype'] != 'autofs']
if not mounted:
raise RuntimeError('Not a mounted filesystem: ' + str(path))
return mounted[-1]
def fstab_vfat():
result = subprocess.run(['findmnt', '--fstab', '--json', '--types', 'vfat', '--output', 'TARGET'],
text=True, capture_output=True)
if result.returncode or not result.stdout.strip():
return []
return [Path(fs['target']) for fs in json.loads(result.stdout)['filesystems']]
def discover_esp(path=None):
candidates = [Path(path)] if path else [Path('/efi'), Path('/boot/efi'), Path('/boot')]
if not path:
candidates += [target for target in fstab_vfat() if target not in candidates]
for candidate in candidates:
# Accessing an automount point mounts it; findmnt alone does not.
try:
os.listdir(candidate)
except OSError:
pass
try:
info = mount_info(candidate, exact=True)
except RuntimeError:
continue
if info['fstype'] not in ('vfat', 'fat', 'fat32'):
continue
source = re.sub(r'\[.*\]$', '', info['source'])
device = Path(source).resolve()
partition = Path('/sys/class/block') / device.name / 'partition'
if not partition.is_file():
continue
# Require the GPT ESP type, not an arbitrary mounted FAT data partition.
result = subprocess.run(['lsblk', '--noheadings', '--nodeps', '--output', 'PARTTYPE', str(device)],
text=True, capture_output=True)
esp_type = 'c12a7328f81f11d2ba4b00a0c93ec93b' # GPT ESP type, without separators
if result.returncode or result.stdout.strip().lower().replace('-', '') != esp_type:
continue
parent = (Path('/sys/class/block') / device.name).resolve().parent.name
identifier = subprocess.run(['lsblk', '--noheadings', '--nodeps', '--output', 'PARTUUID', str(device)], text=True, capture_output=True)
return {'partuuid': identifier.stdout.strip().lower(), 'path': str(candidate.resolve()), 'device': str(device),
'disk': '/dev/' + parent, 'partition': int(partition.read_text())}
raise RuntimeError('Select a mounted GPT EFI System Partition with --esp. No ESP was found.')
def kernel_images(boot=Path('/boot'), exclude=None):
found = {}
for pattern, prefix in [('vmlinuz-*', 'vmlinuz-'), ('kernel-*', 'kernel-')]:
for path in boot.glob(pattern):
version = path.name[len(prefix):]
if path.is_file() and re.fullmatch(r'[A-Za-z0-9_.+-]+', version) and version != exclude:
initrds = [boot / ('initramfs-' + version + '.img'), boot / ('initrd-' + version),
boot / ('initrd.img-' + version)]
found.setdefault(version, {'version': version, 'kernel': str(path),
'initrd': next((str(p) for p in initrds if p.is_file()), None)})
# systemd BLS installations store a linux/initrd pair below their entry ID.
for path in boot.glob('*/*/linux'):
version = path.parent.name
if path.is_file() and re.fullmatch(r'[A-Za-z0-9_.+-]+', version) and version != exclude:
initrd = path.parent / 'initrd'
found.setdefault(version, {'version': version, 'kernel': str(path),
'initrd': str(initrd) if initrd.is_file() else None})
for path in Path('/usr/lib/modules').glob('*/vmlinuz'):
version = path.parent.name
if boot == Path('/boot') and path.is_file() and version != exclude and re.fullmatch(r'[A-Za-z0-9_.+-]+', version):
initrd = boot / ('initramfs-' + version + '.img')
found.setdefault(version, {'version': version, 'kernel': str(path),
'initrd': str(initrd) if initrd.is_file() else None})
versions = subprocess.run(['sort', '-V'], input='\n'.join(found), text=True,
capture_output=True, check=True).stdout.splitlines()[::-1]
versions.sort(key=lambda v: not any(word in v.lower() for word in ('fiw', 'cachy')))
return [found[v] for v in versions]
def kernel_cmdline(root):
config = Path('/etc/kernel/cmdline')
raw = config.read_text().strip() if config.is_file() else Path('/proc/cmdline').read_text().strip()
tokens = shlex.split(raw)
tokens = [token for token in tokens if not token.startswith(('BOOT_IMAGE=', 'initrd=', 'root='))]
if not root.get('uuid'):
raise RuntimeError('Cannot identify the target root filesystem UUID.')
tokens.insert(0, 'root=UUID=' + root['uuid'])
if root['fstype'] == 'btrfs' and not any(t.startswith('rootflags=') for t in tokens):
flags = [flag for flag in root.get('options', '').split(',') if flag.startswith('subvol=')]
if flags:
tokens.append('rootflags=' + flags[0])
text = ' '.join(tokens)
if any(char in text for char in ('\n', '\r', '\0')):
raise RuntimeError('Invalid kernel command line.')
return text
def secure_boot():
files = list(Path('/sys/firmware/efi/efivars').glob('SecureBoot-*'))
return bool(files and len(files[0].read_bytes()) > 4 and files[0].read_bytes()[4] == 1)
def limine_loader():
for path in [Path('/usr/share/limine/BOOTX64.EFI'), Path('/usr/share/limine/limine-uefi-cd.bin')]:
# A CD image is not an EFI executable; only actual PE EFI files qualify.
if path.is_file() and path.open('rb').read(2) == b'MZ':
return path
for path in Path('/usr/share/limine').rglob('BOOTX64.EFI'):
if path.is_file():
return path
raise RuntimeError('Limine BOOTX64.EFI is missing; install sys-boot/limine with uefi-x86-64.')
def render_limine(images, cmdline):
lines = [BEGIN]
for image in images:
version = image['version']
label = 'custom' if any(word in version.lower() for word in ('fiw', 'cachy')) else 'generic'
lines += [f'/Gentoo {version} ({label})', ' protocol: linux',
f' path: boot():/{EFI_DIRECTORY}/kernels/{version}/vmlinuz',
f' module_path: boot():/{EFI_DIRECTORY}/kernels/{version}/initramfs.img',
' cmdline: ' + cmdline, '']
return '\n'.join(lines + [END]) + '\n'
def managed_block(text):
if text.count(BEGIN) != text.count(END) or text.count(BEGIN) > 1:
raise RuntimeError('Malformed managed Limine block; preserving the config.')
if BEGIN not in text:
return None
start = text.index(BEGIN)
end = text.index(END, start) + len(END)
return text[start:end] + '\n'
def merge_limine(text, block):
old = managed_block(text)
if old is None:
return text.rstrip() + '\n\n' + block if text.strip() else 'timeout: 5\n\n' + block
return text.replace(old.rstrip('\n'), block.rstrip('\n'), 1)
def file_hash(path):
return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None
def block_hash(block):
return hashlib.sha256(block.encode()).hexdigest() if block else None
def install_config(old):
values = {'layout': 'compat', 'initrd_generator': 'dracut', 'uki_generator': 'none'}
lines, seen = [], set()
for line in old.splitlines():
key = line.split('=', 1)[0].strip()
if key in values:
if key not in seen:
lines.append(key + '=' + values[key])
seen.add(key)
else:
lines.append(line)
lines += [key + '=' + value for key, value in values.items() if key not in seen]
return '\n'.join(lines) + '\n'
def plan(mode, esp=None, exclude=None):
if mode == 'keep':
return {'mode': 'keep', 'changes': []}
if mode not in ('limine', 'grub'):
raise ValueError('Unknown bootloader')
if platform.machine() not in ('x86_64', 'amd64') or not Path('/sys/firmware/efi').is_dir():
raise RuntimeError('Automatic boot deployment supports amd64 UEFI installations.')
root = mount_info('/')
if root['fstype'] not in ('ext4', 'btrfs') or root['source'].startswith('/dev/mapper/'):
raise RuntimeError('Automatic boot deployment currently supports plain ext4/Btrfs roots. Keep the existing loader for encrypted or other layouts.')
source = re.sub(r'\[.*\]$', '', root['source'])
device_type = subprocess.run(['lsblk', '--noheadings', '--nodeps', '--output', 'TYPE', source], text=True, capture_output=True)
if device_type.returncode or device_type.stdout.strip() not in ('part', 'disk'):
raise RuntimeError('Automatic boot deployment needs a plain disk/partition root; keep the existing loader for this storage layout.')
target = discover_esp(esp)
images = kernel_images(exclude=exclude)
if not images:
raise RuntimeError('No installed kernel/initramfs candidates found under /boot. Install a distribution kernel first.')
text = kernel_cmdline(root)
result = {'mode': mode, 'esp': target, 'root_filesystem': root['fstype'],
'cmdline': text, 'images': images, 'secure_boot': secure_boot(),
'firmware': 'preserve current entries/order unless explicitly selected',
'kernel_refresh_hook': '/etc/kernel/install.d/96-fiw-dots-boot.install'}
if mode == 'limine':
result['loader'] = str(limine_loader())
result['config'] = str(Path(target['path']) / EFI_DIRECTORY / 'limine.conf')
result['proposed_entries'] = render_limine(images, text)
else:
result['config'] = str(GRUB_DIRECTORY / 'grub/grub.cfg')
result['commands'] = [['grub-install', '--target=x86_64-efi', '--efi-directory=' + target['path'],
'--boot-directory=' + str(GRUB_DIRECTORY), '--bootloader-id=Fiw-Gentoo', '--no-nvram'],
['grub-mkconfig', '-o', result['config']]]
prior = Path('/etc/kernel/install.conf').read_text() if Path('/etc/kernel/install.conf').is_file() else ''
result['installkernel_config'] = install_config(prior)
for image in images:
if not image['initrd']:
image['generate_initrd'] = '/boot/initramfs-' + image['version'] + '.img'
return result
def backup(path, root):
if path.exists() or path.is_symlink():
target = root / str(path.absolute()).lstrip('/')
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, target, follow_symlinks=False)
def write(path, content, backups, mode=0o644):
backup(path, backups)
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_name('.' + path.name + '.fiw-dots-tmp')
tmp.write_text(content)
tmp.chmod(mode)
tmp.replace(path)
def grub_config(output, images, exclude=None):
env = dict(os.environ, GRUB_TOP_LEVEL=images[0]['kernel'])
command = ['grub-mkconfig', '-o', str(output)]
if not exclude:
subprocess.run(command, env=env, check=True)
return
# Removal hooks can run before the kernel file disappears. Filter discovery
# through GRUB's library override, without moving installed boot artifacts.
library = Path('/usr/share/grub')
source = (library / 'grub-mkconfig_lib').read_text()
source, count = re.subn(r'\bgrub_file_is_not_garbage\s*\(\s*\)',
'fiw_dots_file_is_not_garbage ()', source, count=1)
if count != 1:
raise RuntimeError('Cannot filter this GRUB library; preserving the boot menu.')
paths = ['/boot/kernel-' + exclude] + [prefix + exclude for prefix in
('/boot/vmlinuz-', '/vmlinuz-', '/boot/vmlinux-', '/vmlinux-')]
source += '\ngrub_file_is_not_garbage () {\n case "$1" in\n ' + '|'.join(
shlex.quote(path) for path in paths) + ') return 1 ;;\n esac\n fiw_dots_file_is_not_garbage "$@"\n}\n'
with tempfile.TemporaryDirectory(prefix='fiw-dots-grub-') as directory:
root = Path(directory)
for path in library.iterdir():
if path.name != 'grub-mkconfig_lib':
(root / path.name).symlink_to(path)
(root / 'grub-mkconfig_lib').write_text(source)
subprocess.run(command, env=dict(env, pkgdatadir=directory), check=True)
def refresh(saved, backups, exclude=None, ask=None):
saved.pop('pending_config', None)
# Recheck the mount every time; never write into an unmounted ESP directory.
target = discover_esp(Path(saved['esp']))
images = kernel_images(exclude=exclude)
if not images:
raise RuntimeError('No remaining kernels; preserving the boot menu.')
for image in images:
if not image['initrd']:
if not shutil.which('dracut'):
raise RuntimeError('Missing initramfs for ' + image['version'] + '; install sys-kernel/dracut.')
path = Path('/boot/initramfs-' + image['version'] + '.img')
subprocess.run(['dracut', '--force', str(path), image['version']], check=True)
image['initrd'] = str(path)
if saved['mode'] == 'grub':
config = GRUB_DIRECTORY / 'grub/grub.cfg'
old_hash = file_hash(config)
output = config
edited = old_hash and old_hash != saved.get('config_hash')
replace = edited and ask and ask('Existing GRUB menu differs from the saved version. Replace it with the generated menu?',
['replace', 'keep'], 'keep') == 'replace'
if edited and not replace:
output = config.with_name(config.name + '.new.' + datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ'))
else:
backup(config, backups)
output.parent.mkdir(parents=True, exist_ok=True)
for image in images:
source = Path(image['kernel'])
if source.parent != Path('/boot'):
dest = Path('/boot') / ('kernel-' + image['version'])
if file_hash(dest) != file_hash(source):
backup(dest, backups)
shutil.copy2(source, dest)
source = Path(image['initrd'])
dest = Path('/boot') / ('initramfs-' + image['version'] + '.img')
if source != dest and not dest.exists():
shutil.copy2(source, dest)
scratch = output.with_name('.' + output.name + '.tmp')
try:
grub_config(scratch, images, exclude)
scratch.replace(output)
finally:
if scratch.exists():
scratch.unlink()
if output != config:
saved['pending_config'] = str(output)
print('Preserved edited GRUB config; review ' + str(output))
else:
saved['config_hash'] = file_hash(config)
return saved
folder = Path(target['path']) / EFI_DIRECTORY
required = sum(Path(source).stat().st_size for image in images
for source, name in [(image['kernel'], 'vmlinuz'), (image['initrd'], 'initramfs.img')]
if file_hash(folder / 'kernels' / image['version'] / name) != file_hash(Path(source)))
if shutil.disk_usage(target['path']).free < required + 16 * 1024 * 1024:
raise RuntimeError('Not enough free space on the ESP for the selected kernels and initramfs files.')
for image in images:
directory = folder / 'kernels' / image['version']
directory.mkdir(parents=True, exist_ok=True)
for source, name in [(image['kernel'], 'vmlinuz'), (image['initrd'], 'initramfs.img')]:
dest = directory / name
if file_hash(dest) != file_hash(Path(source)):
backup(dest, backups)
scratch = dest.with_name('.' + name + '.tmp')
shutil.copy2(source, scratch)
scratch.replace(dest)
config = folder / 'limine.conf'
old = config.read_text() if config.is_file() else ''
prior = managed_block(old)
block = render_limine(images, saved['cmdline'])
output = merge_limine(old, block)
edited = prior and block_hash(prior) != saved.get('block_hash')
replace = edited and ask and ask('Existing Limine entries differ from the saved version. Replace the managed entries?',
['replace', 'keep'], 'keep') == 'replace'
if edited and not replace:
proposal = config.with_name(config.name + '.new.' + datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ'))
write(proposal, output, backups)
saved['pending_config'] = str(proposal)
print('Preserved edited Limine entries; review ' + str(proposal))
else:
if output != old:
write(config, output, backups)
saved['block_hash'] = block_hash(block)
# Only once the menu no longer lists them: drop removed kernels' copies.
prune_kernels(folder / 'kernels', {image['version'] for image in images})
return saved
def prune_kernels(directory, keep):
"""Remove ESP kernel folders that refresh created and the menu no longer uses."""
if not directory.is_dir():
return
for path in directory.iterdir():
if path.name in keep or path.is_symlink() or not path.is_dir():
continue
names = {child.name for child in path.iterdir()}
# Leave anything that is not purely this tool's vmlinuz/initramfs copy.
if names <= {'vmlinuz', 'initramfs.img', '.vmlinuz.tmp', '.initramfs.img.tmp'}:
shutil.rmtree(path)
print('Removed unused ESP kernel copy: ' + path.name)
def confirm(prompt, choices, default):
if not sys.stdin.isatty():
raise RuntimeError('Interactive confirmation required: ' + prompt)
while True:
value = input(prompt + ' [' + '/'.join(choices) + '] (default ' + default + '): ').strip().lower() or default
if value in choices:
return value
def firmware_commands(target, mode, loader_path, choice, listing):
if choice == 'keep':
return []
label = 'Fiw-Gentoo-' + mode
matches = []
for line in listing.splitlines():
match = re.match(r'^Boot([0-9A-Fa-f]{4})\*?\s+' + re.escape(label) + r'\s', line)
if match and target.get('partuuid') and target['partuuid'] in line.lower() and loader_path.lower().replace('/', '\\') in line.lower():
matches.append(match.group(1))
if matches:
if choice == 'add':
return []
order = re.search(r'^BootOrder:\s*(.*)$', listing, re.M)
sequence = [matches[0]] + [item for item in (order.group(1).split(',') if order else [])
if item.upper() != matches[0].upper()]
return [['efibootmgr', '--bootorder', ','.join(sequence)]]
return [['efibootmgr', '--create' if choice == 'default' else '--create-only',
'--disk', target['disk'], '--part', str(target['partition']),
'--label', label, '--loader', loader_path.replace('/', '\\')]]
def deploy(mode, esp=None, ask=confirm):
if mode == 'keep':
print('Keeping the current bootloader.')
return {'status': 'skipped'}
if os.geteuid() != 0:
raise RuntimeError('Boot deployment needs root. Run sudo ./install --deploy-bootloader --selection <file>.')
proposed = plan(mode, esp)
if proposed['secure_boot']:
raise RuntimeError('Secure Boot is active. This unsigned deployment requires a separately configured signing setup.')
for command in (['grub-install', 'grub-mkconfig'] if mode == 'grub' else []):
if not shutil.which(command):
raise RuntimeError('Missing command: ' + command)
if any(not image['initrd'] for image in proposed['images']) and not shutil.which('dracut'):
raise RuntimeError('Missing initramfs generator: dracut')
print(json.dumps(proposed, indent=2))
firmware = ask('UEFI entry: preserve current order, add a menu entry, or make this loader default?',
['keep', 'add', 'default'], 'keep')
if firmware != 'keep' and not shutil.which('efibootmgr'):
raise RuntimeError('Install sys-boot/efibootmgr before registering a firmware entry.')
loader_path = '/EFI/Fiw-Gentoo/' + ('BOOTX64.EFI' if mode == 'limine' else 'grubx64.efi')
listing = subprocess.run(['efibootmgr', '--verbose'], text=True, capture_output=True, check=True).stdout if firmware != 'keep' else ''
commands = firmware_commands(proposed['esp'], mode, loader_path, firmware, listing)
if commands:
print('Proposed firmware commands: ' + json.dumps(commands))
if ask('Deploy the previewed bootloader and kernel refresh hook?', ['deploy', 'cancel'], 'cancel') != 'deploy':
return {'status': 'cancelled'}
stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ')
backups = MANAGED / 'backups' / ('boot-' + stamp)
folder = Path(proposed['esp']['path']) / EFI_DIRECTORY
if folder.exists():
shutil.copytree(folder, backups / 'efi-directory', symlinks=True)
if GRUB_DIRECTORY.exists() and mode == 'grub':
shutil.copytree(GRUB_DIRECTORY, backups / 'grub-directory', symlinks=True)
prior = json.loads(STATE.read_text()) if STATE.is_file() else {}
saved = {'mode': mode, 'esp': proposed['esp']['path'], 'cmdline': proposed['cmdline']}
if prior.get('mode') == mode and prior.get('esp') == saved['esp']:
saved.update({key: prior[key] for key in ('config_hash', 'block_hash') if key in prior})
folder.mkdir(parents=True, exist_ok=True)
if mode == 'limine':
loader = folder / 'BOOTX64.EFI'
backup(loader, backups)
shutil.copy2(proposed['loader'], loader)
loader_path = '/EFI/Fiw-Gentoo/BOOTX64.EFI'
else:
subprocess.run(proposed['commands'][0], check=True)
loader_path = '/EFI/Fiw-Gentoo/grubx64.efi'
saved = refresh(saved, backups, ask=ask)
pending = saved.pop('pending_config', None)
if pending and firmware != 'keep':
commands = []
firmware = 'keep'
print('Kept the existing boot menu; firmware entries and order remain unchanged. Review ' + pending)
# Native installkernel provides versioned /boot kernel/initrd files; our
# final hook refreshes only the chosen loader's own configuration.
write(Path('/etc/kernel/install.conf'), proposed['installkernel_config'], backups)
script = Path('/usr/local/libexec/fiw-dots-boot')
backup(script, backups)
script.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(Path(__file__), script)
script.chmod(0o755)
hook = '#!/bin/sh\ncase "$1" in\n add) exec /usr/local/libexec/fiw-dots-boot --refresh ;;\n remove) exec /usr/local/libexec/fiw-dots-boot --refresh --exclude-version "$2" ;;\nesac\n'
write(Path('/etc/kernel/install.d/96-fiw-dots-boot.install'), hook, backups, 0o755)
# Traditional Gentoo installkernel uses postinst.d rather than install.d.
legacy = '#!/bin/sh\nexec /usr/local/libexec/fiw-dots-boot --refresh\n'
write(Path('/etc/kernel/postinst.d/96-fiw-dots-boot'), legacy, backups, 0o755)
write(STATE, json.dumps(saved, indent=2) + '\n', backups, 0o600)
for command in commands:
subprocess.run(command, check=True)
report = MANAGED / ('boot-' + stamp + '.json')
report.parent.mkdir(parents=True, exist_ok=True)
report.write_text(json.dumps({'plan': proposed, 'firmware_choice': firmware,
'backups': str(backups)}, indent=2) + '\n')
print('Boot deployment finished. Report: ' + str(report))
return {'status': 'partial' if pending else 'completed', 'mode': mode, 'firmware_choice': firmware,
'review': [pending] if pending else [],
'report': str(report), 'backups': str(backups)}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--refresh', action='store_true')
parser.add_argument('--exclude-version')
args = parser.parse_args()
if not args.refresh:
parser.error('Use ./install --boot-plan or --deploy-bootloader for initial setup.')
if os.geteuid() != 0:
raise RuntimeError('Kernel refresh requires root.')
if not STATE.is_file():
return
saved = json.loads(STATE.read_text())
stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ')
backups = MANAGED / 'backups' / ('boot-refresh-' + stamp)
updated = refresh(saved, backups, args.exclude_version)
updated.pop('pending_config', None)
write(STATE, json.dumps(updated, indent=2) + '\n', backups, 0o600)
if __name__ == '__main__':
try:
main()
except (ValueError, OSError, RuntimeError, subprocess.CalledProcessError) as error:
print('Boot refresh: ' + str(error), file=sys.stderr)
sys.exit(1)