soios/os/build.py

221 lines
8.5 KiB
Python
Executable File

#!/bin/env python3
# Build a live ISO.
# Run this from an empty working directory,
# or a directory where you have run this before to reuse caches.
import argparse
import subprocess
import pathlib
import tomllib
import hashlib
import datetime
import urllib.request
DISTRIBUTION = "bookworm"
VARIANT_LABEL = {
"contestant": "contest",
"training-live": "live",
"training-installer": "install",
}
VARIANT_BOOT_OPTIONS = {
"contestant": [
dict(label="SOI contest", cmdline="boot=live toram=filesystem.squashfs"),
dict(label="SOI contest, run from drive", cmdline="boot=live"),
dict(label="SOI contest, fail-safe mode", cmdline="@LB_BOOTAPPEND_LIVE_FAILSAFE@"),
],
"training-live": [
dict(label="SOI live system, run from RAM", cmdline="boot=live toram=filesystem.squashfs"),
dict(label="SOI live system, run from drive", cmdline="boot=live"),
dict(label="SOI live system, fail-safe mode", cmdline="@LB_BOOTAPPEND_LIVE_FAILSAFE@"),
],
"training-installer": [],
}
VARIANT_EXTRA_LB_CONFIG = {
"training-installer": [
"--debian-installer", "live",
# Linux headers are needed for VirtualBox DKMS.
"--linux-packages", "linux-image linux-headers",
],
}
VARIANT_EXTRA_BOOTSTRAP = {
# Fasttrack keyring is needed for VirtualBox.
"training-installer": ",fasttrack-archive-keyring",
}
DOWNLOADS = [
dict(
name="soi-header.tar.gz",
url="https://git.soi.ch/SOI/soi-header/archive/19ddcef24eb55bdb5ddb817c1d91bfa04c8cb8dd.tar.gz",
sha256="38042587982af4e9431aea461e5c345bde358bcc79f0a0eadcf5b3ed77aeb8ab",
),
# From https://soi.ch/wiki/soi-codeblocks/#install-the-soi-project-template
dict(
name="soi_template_codeblocks_ubuntu.zip",
url="https://soi.ch/media/files/soi_template_codeblocks_ubuntu_RzdvSho.zip",
sha256="3f4cae26bbb0cbdfd4cf9f94bfce14988e395f847948d9b349c12d0b980386e9",
),
]
def run(args, check=True, **kwargs):
print(f"> {' '.join(args)}")
subprocess.run(args, check=check, **kwargs)
def mkdir(dirname):
pathlib.Path(dirname).mkdir(parents=True, exist_ok=True)
def edit_file(filename, fn):
with open(filename) as f:
content = f.read()
content = fn(content)
with open(filename, "w") as f:
f.write(content)
def sha256sum(filename):
with open(filename, "rb", buffering=0) as f:
return hashlib.file_digest(f, "sha256").hexdigest()
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"variant",
choices=["contestant", "training-live", "training-installer"],
help="Variant to build.",
)
args = parser.parse_args()
script_dir = pathlib.Path(__file__).parent.resolve()
with open(script_dir / "config/config.toml", "rb") as f:
config = tomllib.load(f)
# Remove files generated by previous build, but keep cache.
run("lb clean".split())
run("rm -rf .build config udeb-build".split())
# Download files.
mkdir("downloads")
for download in DOWNLOADS:
filename = f'downloads/{download["name"]}'
try:
if sha256sum(filename) == download["sha256"]:
continue
except FileNotFoundError:
pass
print(f'> Downloading {download["url"]}')
urllib.request.urlretrieve(download["url"], filename)
if sha256sum(filename) != download["sha256"]:
raise Exception(f"Downloaded file {filename} has wrong hash.")
# Create base configuration directory.
run(
[
"lb", "config",
"--clean",
"--ignore-system-defaults",
"--mode", "debian",
"--distribution", DISTRIBUTION,
"--archive-areas", "main contrib non-free non-free-firmware",
"--firmware-chroot", "false",
"--firmware-binary", "false",
"--apt-recommends", "false",
# isc-dhcp-client and ifupdown are obsoleted by network-manager,
# but they still have Priority: important.
# We need ca-certificates for fetching https packages repos.
"--debootstrap-options", "--exclude=isc-dhcp-client,isc-dhcp-common,ifupdown --include=ca-certificates" +
VARIANT_EXTRA_BOOTSTRAP.get(args.variant, ""),
"--loadlin", "false",
"--iso-volume", f"SOI {VARIANT_LABEL[args.variant]} @ISOVOLUME_TS@",
"--bootappend-live", "boot=live toram=filesystem.squashfs",
]
+ VARIANT_EXTRA_LB_CONFIG.get(args.variant, [])
)
# Add our own configuration on top.
run(["cp", "-rT", f"{script_dir}/layers/participant", "config"])
if args.variant != "training-installer":
run(["cp", "-rT", f"{script_dir}/layers/live", "config"])
run(["cp", "-rT", f"{script_dir}/layers/{args.variant}", "config"])
if args.variant == "training-installer":
# Insert admin password into preseed.
edit_file("config/includes.installer/preseed.cfg",
lambda s: s.replace("@install_admin_password@", config["install_admin_password"]))
# Copy inventory file.
mkdir("config/includes.binary/install")
run(["cp", f"{script_dir}/config/installer-inventory.txt", "config/includes.binary/install/inventory.txt"])
# Insert build date in login screen logo.
edit_file("config/includes.chroot/usr/local/share/images/login-screen-logo.svg",
lambda s: s.replace("@date@", datetime.date.today().isoformat()))
# Build and install custom udeb packages for installer.
run(["cp", "-rT", f"{script_dir}/installer-udeb", "udeb-build"])
run("dpkg-buildpackage --build=all".split(), cwd="udeb-build/inventory-hostname")
mkdir("config/packages.binary")
run("cp udeb-build/inventory-hostname_0_all.udeb config/packages.binary/".split())
# Copy the source lists. The installer deletes everything in sources.list.d,
# so we need to copy them somewhere else and restore them after the install.
for listpath in pathlib.Path('config/archives').glob('*.list.chroot'):
run(["cp", str(listpath), f"config/includes.chroot/usr/local/share/target-sources/{listpath.name.removesuffix('.chroot')}"])
elif args.variant == "contestant":
# Insert root password into hook script.
edit_file("config/hooks/live/2010-contestant.hook.chroot",
lambda s: s.replace("@contestant_root_password@", config["contestant_root_password"]))
# Copy authorized_keys.
mkdir("config/includes.chroot/root/.ssh")
run(["cp", f"{script_dir}/config/contestant_authorized_keys", "config/includes.chroot/root/.ssh/authorized_keys"])
edit_file("config/includes.chroot/etc/NetworkManager/system-connections/contest.nmconnection",
lambda s: s.replace("@wifi_password@", config["contestant_wifi_password"]))
# Configure boot options.
grub_boot_options = '\n'.join(
f'menuentry "{option["label"]}" {{\n'
f' linux @KERNEL_LIVE@ {option["cmdline"]}\n'
f' initrd @INITRD_LIVE@\n'
f'}}\n'
for option in VARIANT_BOOT_OPTIONS[args.variant]
)
with open("/usr/share/live/build/bootloaders/grub-pc/grub.cfg") as f:
grub_cfg = f.read()
grub_cfg = grub_cfg.replace("@LINUX_LIVE@", grub_boot_options)
with open("config/bootloaders/grub-pc/grub.cfg", "w") as f:
f.write(grub_cfg)
syslinux_boot_options = ''.join(
f"label live-{i}\n"
f"\tmenu label {option['label']}\n"
+ (f"\tmenu default\n" if i == 0 else "") +
f"\tlinux @LINUX@\n"
f"\tinitrd @INITRD@\n"
f"\tappend {option['cmdline'].replace('@LB_BOOTAPPEND_LIVE_FAILSAFE@', '@APPEND_LIVE_FAILSAFE@')}\n"
f"\n"
for i, option in enumerate(VARIANT_BOOT_OPTIONS[args.variant])
)
mkdir("config/bootloaders/syslinux_common")
with open("config/bootloaders/syslinux_common/live.cfg.in", "w") as f:
f.write(syslinux_boot_options)
# Install soi header.
mkdir("config/includes.chroot/usr/local/include")
run(["tar", "--overwrite", "-xf", "downloads/soi-header.tar.gz", "-C", "config/includes.chroot/usr/local/include", "--strip-components=2", "soi-header/include/"])
# Install codeblocks template.
mkdir("config/includes.chroot/usr/share/codeblocks/templates/wizard")
run(["unzip", "-o", "downloads/soi_template_codeblocks_ubuntu.zip", "-d", "config/includes.chroot/usr/share/codeblocks/templates/wizard/"])
# Start the build.
run("lb build".split())
if __name__ == "__main__":
main()