diff --git a/.gitea/workflows/build-image.yaml b/.gitea/workflows/build-image.yaml new file mode 100644 index 0000000..2cf3925 --- /dev/null +++ b/.gitea/workflows/build-image.yaml @@ -0,0 +1,110 @@ +name: build-image + +on: + push: + branches: [main] + tags: ["v*.*.*"] + +# Builds raw disk images of the two default-tier editions (see +# cook/recipes_manifest.yaml) with build-image.sh, i.e. +# `livemedia-creator --no-virt --make-disk`. Other tiers (guest, experimental) +# are built on demand with `./build-image.sh --tier `. +# +# The build job runs on the registered `fedora:host` runner (a Fedora VM, jobs +# run as root), not in a container: --make-disk needs real loop devices and a +# live systemd-udevd. The runner needs lorax-lmc-novirt, qemu-img, make, +# python3-pip, pykickstart and xz installed. + +jobs: + validate: + runs-on: fedora + defaults: + run: + shell: bash + container: + image: git.phyllo.me/devops/fedora-runner-image:latest + + steps: + - uses: https://git.phyllo.me/devops/checkout@v5 + with: + fetch-depth: 0 + + - name: Install Python deps + run: pip install -r cook/requirements.txt + + - name: Generate, lint, and validate dishes + run: | + cd cook + make all + + build-image: + needs: validate + runs-on: fedora + strategy: + fail-fast: false + # One image at a time: each build takes loop devices, RAM and tens of GiB + max-parallel: 1 + matrix: + include: + - edition: phyllomeos + root_size: 16384 + - edition: phyllomeos-headless + root_size: 8192 + defaults: + run: + shell: bash + env: + # /tmp is a small tmpfs on the runner; livemedia-creator writes the whole + # disk image under $TMPDIR + TMPDIR: /var/tmp + + steps: + - uses: https://git.phyllo.me/devops/checkout@v5 + with: + fetch-depth: 0 + + - name: Check runner prerequisites + run: | + missing=0 + for tool in livemedia-creator qemu-img make pip xz sudo; do + command -v "$tool" >/dev/null || { echo "missing on runner: $tool"; missing=1; } + done + [ -e /dev/loop-control ] || { echo "missing on runner: /dev/loop-control"; missing=1; } + [ "$missing" = 0 ] || exit 1 + + - name: Install Python deps + run: pip install -r cook/requirements.txt + + - name: Build raw image + run: | + # Editions are named "_.cfg"; the trailing "_" keeps + # "phyllomeos" from matching "phyllomeos-headless". + (cd cook && make all) + dish="$(basename "$(ls cook/dishes/${{ matrix.edition }}_*.cfg)" .cfg)" + echo "DISH=$dish" >> "$GITHUB_ENV" + ./build-image.sh --dish "$dish" --root-size ${{ matrix.root_size }} + xz -T0 "build/$dish.img" + + - name: Upload image and kickstart as artifact + if: github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.edition }} + path: | + build/${{ env.DISH }}.img.xz + cook/dishes/${{ env.DISH }}.cfg + if-no-files-found: error + + - name: Publish image and kickstart as release + if: startsWith(github.ref, 'refs/tags/') + uses: https://git.phyllo.me/devops/action-gh-release@v2 + with: + files: | + build/${{ env.DISH }}.img.xz + cook/dishes/${{ env.DISH }}.cfg + draft: false + prerelease: false + + - name: Clean up build products + if: always() + run: rm -rf build /var/tmp/lmc-result.* diff --git a/.gitea/workflows/build-iso.yaml b/.gitea/workflows/build-iso.yaml deleted file mode 100644 index 9dedb12..0000000 --- a/.gitea/workflows/build-iso.yaml +++ /dev/null @@ -1,82 +0,0 @@ -name: build-iso - -on: - push: - branches: [main] - tags: ["v*.*.*"] - -env: - FEDORA_VERSION: "44" - KICKSTART_FILE: live-desktop_44_livefs_grub_gnome_guest-agents_live - -jobs: - validate: - runs-on: fedora - defaults: - run: - shell: bash - container: - image: git.phyllo.me/devops/fedora-runner-image:latest - - steps: - - uses: https://git.phyllo.me/devops/checkout@v5 - with: - fetch-depth: 0 - - - name: Install Python deps - run: pip install -r cook/requirements.txt - - - name: Generate, lint, and validate dishes - run: | - cd cook - make all - - build-iso: - needs: validate - runs-on: fedora - defaults: - run: - shell: bash - container: - image: git.phyllo.me/devops/fedora-runner-image:latest - privileged: true - - steps: - - uses: https://git.phyllo.me/devops/checkout@v5 - with: - fetch-depth: 0 - - - name: Install Python deps - run: pip install -r cook/requirements.txt - - - name: Install build tools - run: dnf -y install lorax-lmc-novirt pykickstart livecd-tools - - - name: Generate dishes - run: | - cd cook - make all - - - name: Build ISO with livemedia-creator - run: | - for i in $(seq 0 63); do mknod -m 0660 /dev/loop$i b 7 $i 2>/dev/null || true; done - mknod -m 0660 /dev/loop-control b 7 0 2>/dev/null || true - ks="$(mktemp --suffix=.cfg)" - grep -v -E '^(text|cmdline|graphical)([[:space:]]|$)' "cook/dishes/${{ env.KICKSTART_FILE }}.cfg" > "$ks" - livemedia-creator --ks "$ks" --no-virt --resultdir build --project phyllomeos --make-iso --volid phyllomeos --iso-only --iso-name ${{ env.KICKSTART_FILE }}-${{ env.FEDORA_VERSION }}.iso --releasever ${{ env.FEDORA_VERSION }} --macboot - - - name: Upload ISO as artifact - if: github.ref == 'refs/heads/main' - uses: actions/upload-artifact@v3 - with: - name: ${{ env.KICKSTART_FILE }}-${{ env.FEDORA_VERSION }}.iso - path: build/${{ env.KICKSTART_FILE }}-${{ env.FEDORA_VERSION }}.iso - if-no-files-found: error - - - name: Publish ISO as release - if: startsWith(github.ref, 'refs/tags/') - uses: https://git.phyllo.me/devops/action-gh-release@v2 - with: - files: build/${{ env.KICKSTART_FILE }}-${{ env.FEDORA_VERSION }}.iso - draft: false - prerelease: false \ No newline at end of file diff --git a/README.md b/README.md index 63c13b4..350e216 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Provided that some dependencies are met (`libvirt` is running on your computer, chmod +x deploy.sh ``` -- Execute it and pick a dish, e.g. `desktop_44_standard_grub_gnome_guest-agents`, when prompted: +- Execute it and pick a dish when prompted (by default the two Phyllome OS editions; see [Editions and tiers](#editions-and-tiers)). The example below is from an earlier, larger matrix: ``` ./deploy.sh @@ -35,9 +35,28 @@ Allocating 'desktop_44_standard_grub_gnome_guest-agents.img' Creating domain... ``` -`deploy.sh` regenerates the `recipes` and `dishes` (they are build products) before deploying. +`deploy.sh` regenerates the `recipes` and `dishes` (they are build products) before deploying. Pass `--tier guest` to deploy a plain Fedora server or desktop VM on a Phyllome OS host instead. After a successfull installation, the virtual machine will shutdown and be ready to use when powered on again. +## Editions and tiers + +The repo ships a recipe to deploy Phyllome OS on a target host, not a general-purpose kickstart collection. Only two dishes are generated by default (Fedora 44, UEFI, systemd-boot, both with hardware support and guest agents so they also run as VMs): + +| Dish (prefix) | What it is | +|---|---| +| `phyllomeos` | Phyllome OS with a GUI (GNOME + virt-manager) | +| `phyllomeos-headless` | Headless Phyllome OS | + +Every group in `cook/recipes_manifest.yaml` belongs to a *tier*, and only the requested tier is generated: + +| Tier | Contents | Build with | +|---|---|---| +| `default` | the two editions above | `make all` | +| `guest` | `guest-server`, `guest-desktop`: Fedora VMs to deploy on a Phyllome OS host | `make all TIER=guest` | +| `experimental` | single hand-written variants to try new dishes (BIOS/GRUB, encrypted root, rawhide) | `make all TIER=experimental` | + +`TIER=all` (or `TIER="default guest"`) combines tiers; `deploy.sh` and `build-image.sh` take `--tier`. Lint always covers every tier, so inactive tiers cannot rot. To test a new dish, add one variant (no list-valued matrices) to an `experimental` group. + ## Building a disk image (no libvirt/virt-install) `deploy.sh` drives a live `virt-install`/libvirt session. If you just want a @@ -48,11 +67,11 @@ use `build-image.sh` instead. It runs `livemedia-creator --no-virt bootable ISO: ``` -./build-image.sh [--as qcow2] [--root-size MiB] [--extra-ks FILE] +./build-image.sh [--dish NAME] [--tier TIER] [--as qcow2] [--root-size MiB] [--extra-ks FILE] ``` -It picks a dish the same way `deploy.sh` does and writes the result under -`./build/`. Requires `lorax-lmc-novirt` (heavier than `deploy.sh`'s plain +It picks a dish the same way `deploy.sh` does (or takes `--dish NAME`) and writes the result under +`./build/`. The `build-image` CI workflow runs it for both default editions on every push to `main`, and on `v*.*.*` tags it attaches the compressed raw images (`.img.xz`) and the flattened kickstart files to the release. Requires `lorax-lmc-novirt` (heavier than `deploy.sh`'s plain QEMU/libvirt prerequisites, not installed by `deploy/install-prerequisites-on-linux.sh`) and root — the `--no-virt` disk install needs real loop-device access (`/dev/loop-control`), which a @@ -123,7 +142,7 @@ cd cook && make all Alternatively, for a bespoke dish not part of the matrix, append the ingredient directly to an existing recipe and re-flatten it: ``` -echo "%include ../ingredients/extra-luanti.ks # Sandbox video game engine" >> cook/recipes/desktop_44_standard_grub_gnome.cfg +echo "%include ../ingredients/extra-luanti.ks # Sandbox video game engine" >> cook/recipes/.cfg cd cook && make generate ``` @@ -135,12 +154,13 @@ cd cook && make generate cd cook && make inventory ``` -- Define a new edition (a group named e.g. `server`) in `cook/recipes_manifest.yaml` with the desired variants; the cartesian product of its list values generates one dish per combination. +- Define a new group in `cook/recipes_manifest.yaml` with a `tier` (use `experimental` for anything that should not be built by default); the cartesian product of its list values generates one dish per combination, so keep it to a single variant. ### Useful targets (in `cook/`) ``` -make all # generate recipes + dishes, lint, and validate (default) +make all # generate the default tier's dishes, lint, and validate +make all TIER=guest # ... or the guest / experimental / all tiers make lint # check the manifest/templates without writing files make validate # validate existing dishes make inventory # print the ingredient catalog derived from the templates diff --git a/build-image.sh b/build-image.sh index a956669..e37ad67 100755 --- a/build-image.sh +++ b/build-image.sh @@ -6,7 +6,12 @@ # (e.g. `virtpull local ./build/.img --wonder-vm NAME`) instead of a # bootable ISO. # -# Usage: ./build-image.sh [--as qcow2] [--root-size MiB] [--extra-ks FILE] +# Usage: ./build-image.sh [--dish NAME] [--tier TIER] [--as qcow2] +# [--root-size MiB] [--extra-ks FILE] +# +# --dish skips the interactive picker (used by CI); --tier selects which +# manifest tier of dishes to generate (default: default; see +# cook/recipes_manifest.yaml). # # Requires: lorax-lmc-novirt (livemedia-creator), qemu-img, and root (the # --no-virt disk install needs real loop-device access via @@ -31,10 +36,20 @@ set -euo pipefail AS_FORMAT="" +DISH="" +TIER="${TIER:-default}" ROOT_SIZE=8192 EXTRA_KS="" while [[ $# -gt 0 ]]; do case "$1" in + --dish) + DISH="$2" + shift 2 + ;; + --tier) + TIER="$2" + shift 2 + ;; --as) AS_FORMAT="$2" shift 2 @@ -49,7 +64,7 @@ while [[ $# -gt 0 ]]; do ;; *) echo "Unknown argument: $1" - echo "Usage: $0 [--as qcow2] [--root-size MiB] [--extra-ks FILE]" + echo "Usage: $0 [--dish NAME] [--tier TIER] [--as qcow2] [--root-size MiB] [--extra-ks FILE]" exit 1 ;; esac @@ -81,15 +96,23 @@ if [[ $EUID -ne 0 ]] && ! sudo -n true 2>/dev/null; then fi # Regenerate recipes and dishes before building (they are build products) -if ! (cd cook && make all); then +if ! (cd cook && make all TIER="$TIER"); then echo "Failed to generate dishes in cook/ (see 'make all' in cook/)" exit 1 fi -# Pick a dish (shared with deploy/deploy-distro.sh) -# shellcheck source=./deploy/select-dish.sh -source "$(dirname "$0")/deploy/select-dish.sh" -dish="$selected_dish" +# Pick a dish: --dish, or interactively (shared with deploy/deploy-distro.sh) +if [[ -n "$DISH" ]]; then + dish="${DISH%.cfg}" + if [[ ! -f "cook/dishes/${dish}.cfg" ]]; then + echo "Dish not found: cook/dishes/${dish}.cfg (is --tier '$TIER' right?)" + exit 1 + fi +else + # shellcheck source=./deploy/select-dish.sh + source "$(dirname "$0")/deploy/select-dish.sh" + dish="$selected_dish" +fi # Derive the target release, same convention as # deploy/deploy-distro.sh's find_fedora_iso() (used for --releasever, which diff --git a/cook/Makefile b/cook/Makefile index 42a9236..fce47cf 100644 --- a/cook/Makefile +++ b/cook/Makefile @@ -1,6 +1,9 @@ .PHONY: help generate lint validate inventory test install-deps clean clean-recipes clean-dishes all PYTHON ?= python3 +# Manifest tier(s) to generate: default, guest, experimental, all (space-separated) +TIER ?= default +TIER_ARGS = $(foreach t,$(TIER),--tier $(t)) default: all @@ -9,6 +12,7 @@ help: @echo "" @echo "Available targets:" @echo " all - Generate recipes and dishes, lint, and validate (default)" + @echo " TIER=default|guest|experimental|all picks the manifest tier(s)" @echo " generate - Same as 'all'" @echo " lint - Lint manifest and templates without writing files" @echo " validate - Validate existing dishes without regenerating" @@ -22,13 +26,13 @@ help: all: generate generate: - @$(PYTHON) generate_recipe.py + @$(PYTHON) generate_recipe.py $(TIER_ARGS) lint: @$(PYTHON) generate_recipe.py --no-generate --no-validate validate: - @$(PYTHON) generate_recipe.py --no-generate --no-lint + @$(PYTHON) generate_recipe.py $(TIER_ARGS) --no-generate --no-lint inventory: @$(PYTHON) generate_recipe.py --inventory diff --git a/cook/generate_recipe.py b/cook/generate_recipe.py index 228e99e..516ff14 100644 --- a/cook/generate_recipe.py +++ b/cook/generate_recipe.py @@ -204,9 +204,42 @@ def validate_dish(dish_path): return False, str(exc) +DEFAULT_TIER = 'default' + + +def group_tier(group): + """Return the tier of a manifest group (groups default to 'default').""" + return group.get('tier', DEFAULT_TIER) + + +def select_groups(manifest, tiers): + """Return the manifest groups belonging to the requested tiers. + + tiers is a collection of tier names; 'all' selects every group. + """ + groups = manifest.get('recipes', []) + if 'all' in tiers: + return groups + return [g for g in groups if group_tier(g) in tiers] + + +def clean_output(directory): + """Remove previously generated .cfg files so stale dishes never linger.""" + if not os.path.isdir(directory): + return + for entry in os.listdir(directory): + if entry.endswith('.cfg'): + os.remove(os.path.join(directory, entry)) + + def generate(manifest_path, templates_path, recipes_dir, dishes_dir, - ingredients_dir, do_generate=True, do_lint=True, do_validate=True): - """Full cooking pipeline. Returns an exit code (0 on success).""" + ingredients_dir, do_generate=True, do_lint=True, do_validate=True, + tiers=(DEFAULT_TIER,)): + """Full cooking pipeline. Returns an exit code (0 on success). + + Only groups in the requested tiers are written; lint always covers the + whole manifest so inactive tiers cannot rot. + """ manifest = load_yaml(manifest_path) templates = load_yaml(templates_path) ingredients_root = os.path.join(os.path.dirname(os.path.abspath(manifest_path)), @@ -225,8 +258,10 @@ def generate(manifest_path, templates_path, recipes_dir, dishes_dir, if do_generate: os.makedirs(recipe_dir, exist_ok=True) os.makedirs(dish_dir, exist_ok=True) + clean_output(recipe_dir) + clean_output(dish_dir) - for group in manifest.get('recipes', []): + for group in select_groups(manifest, tiers): group_name = group.get('name', 'unknown') for config in group.get('variants', []): for variant in expand_variants(config): @@ -315,6 +350,9 @@ def main(argv=None): help='output directory for flattened dishes (default: dishes)') parser.add_argument('--ingredients-dir', default='ingredients', help='ingredient fragments directory (default: ingredients)') + parser.add_argument('--tier', action='append', metavar='TIER', + help="manifest tier to generate: default, guest, experimental " + "or all; repeatable (default: default)") parser.add_argument('--no-generate', action='store_true', help='do not write recipes or dishes (lint/validate only)') parser.add_argument('--no-lint', action='store_true', @@ -342,7 +380,8 @@ def main(argv=None): args.ingredients_dir, do_generate=not args.no_generate, do_lint=not args.no_lint, - do_validate=not args.no_validate) + do_validate=not args.no_validate, + tiers=tuple(args.tier or (DEFAULT_TIER,))) if __name__ == '__main__': diff --git a/cook/ingredients/hypervisor/cpu-agnostic.ks b/cook/ingredients/hypervisor/cpu-agnostic.ks new file mode 100644 index 0000000..2c083fa --- /dev/null +++ b/cook/ingredients/hypervisor/cpu-agnostic.ks @@ -0,0 +1,33 @@ +# CPU-agnostic optimization for hypervisor (Intel and AMD) +# +# One ingredient for both CPU vendors, so a single dish can be installed on either: +# * intel_iommu=on is ignored by AMD kernels (amd_iommu is enabled by default). +# * "options kvm_intel"/"options kvm_amd" only apply to the module that actually loads. + +%post --nochroot --log=/mnt/sysimage/root/hypervisor-cpu-post.log # Beginning of %post section. Those commands are executed outside the chroot environment. Logging is enabled to help with post-installation troubleshooting + +# Kernel arguments cannot be attached to a single canonical location at %post time: +# anaconda always creates /etc/kernel/cmdline (kernel-install source for both +# systemd-boot and GRUB+BLS layouts), but GRUB+BLS boot entries have already been +# generated from it by the kernel package scriptlets, and non-BLS flows still read +# /etc/default/grub. Update every surface idempotently, guarded by file existence. + +IOMMU_ARGS="intel_iommu=on iommu=pt rd.driver.pre=vfio-pci" + +if [ -f /mnt/sysimage/etc/kernel/cmdline ]; then # kernel-install source (systemd-boot now, GRUB+BLS for future kernels) + grep -q "iommu=pt" /mnt/sysimage/etc/kernel/cmdline || echo " $IOMMU_ARGS" >> /mnt/sysimage/etc/kernel/cmdline # Append IOMMU arguments once +fi + +if [ -f /mnt/sysimage/etc/default/grub ]; then # GRUB: arguments for grub.cfg-based boot flows + grep -q "iommu=pt" /mnt/sysimage/etc/default/grub || sed -i "s/\(quiet\)/\1 $IOMMU_ARGS/i" /mnt/sysimage/etc/default/grub # Append IOMMU arguments once +fi + +for bls_entry in /mnt/sysimage/boot/loader/entries/*.conf; do # Already-generated boot entries share one "options" line shape + [ -e "$bls_entry" ] || continue # Nothing matched: the glob stays literal + grep -q "iommu=pt" "$bls_entry" || sed -i "s/^\(options .*\)/\1 $IOMMU_ARGS/" "$bls_entry" # Patch the options line of each entry once +done + +grep -q "kvm_intel" /mnt/sysimage/etc/modprobe.d/kvm.conf 2>/dev/null || echo "options kvm_intel nested=1" >> /mnt/sysimage/etc/modprobe.d/kvm.conf # Add support for nested virtualization on Intel CPUs, exactly once +grep -q "kvm_amd" /mnt/sysimage/etc/modprobe.d/kvm.conf 2>/dev/null || echo "options kvm_amd nested=1" >> /mnt/sysimage/etc/modprobe.d/kvm.conf # Add support for nested virtualization on AMD CPUs, exactly once + +%end # End of the %post section diff --git a/cook/recipe_templates.yaml b/cook/recipe_templates.yaml index a0c3ad3..bf372c3 100644 --- a/cook/recipe_templates.yaml +++ b/cook/recipe_templates.yaml @@ -68,6 +68,7 @@ features: - packages/virtual-machine-manager/packages.ks - packages/virtual-machine-manager/post-scripts.ks hypervisor_type: + any: hypervisor/cpu-agnostic.ks amdcpu: hypervisor/amdcpu.ks intelcpu: hypervisor/intelcpu.ks intelgpu: hypervisor/intelgpu.ks diff --git a/cook/recipes_manifest.yaml b/cook/recipes_manifest.yaml index 315edf2..20203ac 100644 --- a/cook/recipes_manifest.yaml +++ b/cook/recipes_manifest.yaml @@ -2,52 +2,87 @@ # Define all recipe variants using ingredients from recipe_templates.yaml. # Edit this file to add new variants, then run: make all # +# Tiers: every group belongs to a tier (default: "default"). Only the requested +# tier is generated, so the default build stays tiny: +# default -- the two shipping editions (phyllomeos, phyllomeos-headless) +# guest -- Fedora VM guests to deploy on a Phyllome OS host +# experimental -- one-off variants for testing new dishes +# Build a tier with: make all TIER=guest (or: TIER="default guest", TIER=all) +# Lint always covers every tier. +# # Syntax: -# - Single values: repository: "43" -# - List values: repository: ["43", "44", "rawhide"] (expands to multiple variants) +# - Single values: repository: "44" +# - List values: repository: ["43", "44"] (expands to multiple variants) # - Booleans: hardware-support: true / false (feature flags) # -# NOTE: List values create a cartesian product. -# Example: repository: ["43", "44", "rawhide"] + storage: ["standard", "encrypted"] -# creates 6 variants: (43,standard), (43,encrypted), (44,standard), (44,encrypted), -# (rawhide,standard), (rawhide,encrypted) +# NOTE: List values create a cartesian product. Keep it out of the default and +# guest tiers; prefer one hand-written variant per experimental dish. # # NOTE: The 'name' field prefixes every generated dish/recipe filename, e.g. -# the group below yields dishes like desktop_43_gnome_standard_grub_guest-agents.cfg. +# the "phyllomeos" group yields phyllomeos_44_standard_systemd-boot_...cfg. recipes: - # Desktop variants - - name: desktop - variants: - - repository: ["43", "44", "rawhide"] - desktop: gnome - storage: ["standard", "encrypted"] - bootloader: ["grub", "systemd-boot"] - hardware-support: [true, false] - guest-agents: [true, false] + # --- default tier: the two shipping editions ------------------------------- - # Server variants - - name: server + # Phyllome OS with a GUI (GNOME + virt-manager) + - name: phyllomeos + tier: default variants: - - repository: ["44"] + - repository: "44" + desktop: gnome storage: standard bootloader: systemd-boot + security: enabled + initial-setup: gnome + hardware-support: true + guest-agents: true + hypervisor: desktop + hypervisor_type: any + + # Headless Phyllome OS + - name: phyllomeos-headless + tier: default + variants: + - repository: "44" + storage: standard + bootloader: systemd-boot + security: enabled + initial-setup: server + hardware-support: true + guest-agents: true + hypervisor: base + hypervisor_type: any + + # --- guest tier: VMs to deploy on a Phyllome OS host ------------------------- + + - name: guest-server + tier: guest + variants: + - repository: "44" + storage: standard + bootloader: systemd-boot + security: enabled + initial-setup: server hardware-support: false guest-agents: true - initial-setup: server - # Hypervisor variants - - name: hypervisor + - name: guest-desktop + tier: guest variants: - - repository: ["44"] + - repository: "44" + desktop: gnome storage: standard bootloader: systemd-boot - hardware-support: true - guest-agents: false - hypervisor: base - hypervisor_type: intelcpu - # Desktop-hypervisor variants - - name: desktop-hypervisor + security: enabled + initial-setup: gnome + hardware-support: false + guest-agents: true + + # --- experimental tier: single variants, no matrices ------------------------- + + # BIOS (legacy firmware) laptop: GPT + biosboot partition, GRUB + - name: exp-bios-desktop-hypervisor + tier: experimental variants: - repository: "44" desktop: gnome @@ -58,14 +93,32 @@ recipes: hardware-support: true guest-agents: false hypervisor: desktop - hypervisor_type: intelcpu + hypervisor_type: any - # Live desktop ISO variants (built with livemedia-creator --make-iso) - - name: live-desktop + # Encrypted root + - name: exp-encrypted-headless + tier: experimental variants: - repository: "44" - desktop: gnome - storage: livefs - bootloader: grub + storage: encrypted + bootloader: systemd-boot + security: enabled + initial-setup: server + hardware-support: true guest-agents: true - live: true \ No newline at end of file + hypervisor: base + hypervisor_type: any + + # Rawhide + - name: exp-rawhide-headless + tier: experimental + variants: + - repository: rawhide + storage: standard + bootloader: systemd-boot + security: enabled + initial-setup: server + hardware-support: true + guest-agents: true + hypervisor: base + hypervisor_type: any diff --git a/cook/tests/test_generator.py b/cook/tests/test_generator.py index 6e490ef..071e5b5 100644 --- a/cook/tests/test_generator.py +++ b/cook/tests/test_generator.py @@ -278,4 +278,77 @@ def test_generate_writes_recipes_and_dishes(tmp_path, templates): assert len(list(recipes.glob("*.cfg"))) == 1 assert len(list(dishes.glob("*.cfg"))) == 1 dish = (dishes / "desktop_43_standard_grub_gnome.cfg").read_text() - assert "%include" not in dish \ No newline at end of file + assert "%include" not in dish + +# --------------------------------------------------------------------------- +# tiers and the real manifest +# --------------------------------------------------------------------------- + +def real_manifest(): + return gen.load_yaml(os.path.join(COOK_DIR, "recipes_manifest.yaml")) + + +def dish_names(manifest, templates, tiers): + return sorted( + gen.render_filename(templates, group["name"], variant) + for group in gen.select_groups(manifest, tiers) + for config in group["variants"] + for variant in gen.expand_variants(config)) + + +def test_select_groups_defaults_untiered_groups_to_default(): + manifest = {"recipes": [{"name": "a"}, {"name": "b", "tier": "guest"}]} + assert [g["name"] for g in gen.select_groups(manifest, ("default",))] == ["a"] + assert [g["name"] for g in gen.select_groups(manifest, ("guest",))] == ["b"] + assert [g["name"] for g in gen.select_groups(manifest, ("all",))] == ["a", "b"] + + +def test_real_manifest_lints_clean_across_all_tiers(templates): + problems = gen.lint_manifest(real_manifest(), templates, + os.path.join(COOK_DIR, "ingredients")) + assert problems == [] + + +def test_default_tier_is_the_two_editions(templates): + names = dish_names(real_manifest(), templates, ("default",)) + assert len(names) == 2 + assert sorted(n.split("_")[0] for n in names) == ["phyllomeos", "phyllomeos-headless"] + + +def test_guest_tier_is_server_and_desktop(templates): + names = dish_names(real_manifest(), templates, ("guest",)) + assert sorted(n.split("_")[0] for n in names) == ["guest-desktop", "guest-server"] + assert all("guest-agents" in n for n in names) + + +def test_shipping_dishes_contents(templates): + manifest = real_manifest() + for group in gen.select_groups(manifest, ("default",)): + variant = gen.expand_variants(group["variants"][0])[0] + fragments, problems = gen.collect_fragments(templates, variant) + assert problems == [] + assert "hypervisor/cpu-agnostic.ks" in fragments + assert "bootloader/systemd-boot.ks" in fragments + assert "storage/standard.ks" in fragments + assert ("desktop/gnome/config.ks" in fragments) == (group["name"] == "phyllomeos") + + +def test_generate_only_writes_requested_tier_and_clears_stale(tmp_path): + (tmp_path / "ingredients").symlink_to(os.path.join(COOK_DIR, "ingredients"), + target_is_directory=True) + recipes = tmp_path / "recipes" + dishes = tmp_path / "dishes" + recipes.mkdir() + dishes.mkdir() + (dishes / "stale.cfg").write_text("stale") + (recipes / "stale.cfg").write_text("stale") + templates_path = os.path.join(COOK_DIR, "recipe_templates.yaml") + manifest_path = os.path.join(COOK_DIR, "recipes_manifest.yaml") + + code = gen.generate(manifest_path, templates_path, str(recipes), str(dishes), + "ingredients", tiers=("guest",)) + assert code == 0 + names = sorted(p.name.split("_")[0] for p in dishes.glob("*.cfg")) + assert names == ["guest-desktop", "guest-server"] + assert not (dishes / "stale.cfg").exists() + assert not (recipes / "stale.cfg").exists() diff --git a/deploy.sh b/deploy.sh index 2144f26..3b2b62b 100755 --- a/deploy.sh +++ b/deploy.sh @@ -11,8 +11,26 @@ execute_script() { return 0 # Indicate success } +# Usage: ./deploy.sh [--tier default|guest|experimental|all] +# Tier of dishes to generate and offer (default: default). Use "guest" to +# deploy a plain Fedora server/desktop VM on a Phyllome OS host. +TIER="${TIER:-default}" +while [[ $# -gt 0 ]]; do + case "$1" in + --tier) + TIER="$2" + shift 2 + ;; + *) + echo "Unknown argument: $1" + echo "Usage: $0 [--tier default|guest|experimental|all]" + exit 1 + ;; + esac +done + # Generate recipes and dishes before deployment (they are build products) -if ! (cd cook && make all); then +if ! (cd cook && make all TIER="$TIER"); then echo "Failed to generate dishes in cook/ (see 'make all' in cook/)" exit 1 fi