PageSourceSearch

https://penguins-eggs.net/assets/js/9ceef6d4.946c77c3.js

js penguins-eggs.net collected 2026-10-03 23:14:40 UTC 17,036 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkpenguins_blog=globalThis.webpackChunkpenguins_blog||[]).push([[4389],{17198(e,s,n){n.r(s),n.d(s,{assets:()=>c,contentTitle:()=>d,default:()=>h,frontMatter:()=>o,metadata:()=>t,toc:()=>l});const t=JSON.parse('{"id":"developer-manual/architecture/planner","title":"\u2699\ufe0f The Engineer: planner (Plan Compilation)","description":"The planner package is the bridge between the abstraction of the Brain (rendered by the parser) and the executors (oa in C, coa ell in Go). Its job is to compile the user\'s intentions into the Single Source of Truth: /tmp/coa/oa-plan.json, a JIT \\"compiler\\" running right before takeoff.","source":"@site/penguins-eggs/3-developer-manual/architecture/7-planner.md","sourceDirName":"3-developer-manual/architecture","slug":"/developer-manual/architecture/planner","permalink":"/penguins-eggs/developer-manual/architecture/planner","draft":false,"unlisted":false,"tags":[],"version":"current","sidebarPosition":7,"frontMatter":{},"sidebar":"penguinsEggsSidebar","previous":{"title":"sysinstall: Architecture of the Unified Installer","permalink":"/penguins-eggs/developer-manual/architecture/installer"},"next":{"title":"\ud83e\udd16 Architecture: Model Context Protocol (MCP) Server","permalink":"/penguins-eggs/developer-manual/architecture/mcp"}}');var i=n(11684),r=n(10506);const o={},d="\u2699\ufe0f The Engineer: planner (Plan Compilation)",c={},l=[{value:"\ud83e\uddf1 1. The Task Format: <code>OATask</code> (the embedding trick)",id:"-1-the-task-format-oatask-the-embedding-trick",level:2},{value:"\u2699\ufe0f 2. The Compiler: <code>GeneratePlan()</code>",id:"\ufe0f-2-the-compiler-generateplan",level:2},{value:"<code>mount_logic</code> \u2192 environment bootstrap",id:"mount_logic--environment-bootstrap",level:3},{value:"<code>users</code> \u2192 two concrete tasks (<code>oa-users.go</code>)",id:"users--two-concrete-tasks-oa-usersgo",level:3},{value:"<code>umount</code> \u2192 the guaranteed cleanup",id:"umount--the-guaranteed-cleanup",level:3},{value:"<code>--crypted</code> \u2192 LUKS pipeline injection (<code>luks-steps.go</code>)",id:"--crypted--luks-pipeline-injection-luks-stepsgo",level:3},{value:"<code>xorriso</code> \u2192 parameter injection + <code>.disk</code> metadata",id:"xorriso--parameter-injection--disk-metadata",level:3},{value:"\ud83d\uded1 The breakpoint (<code>--stop-after</code>)",id:"-the-breakpoint---stop-after",level:3},{value:"\ud83d\udd27 The normalization pass",id:"-the-normalization-pass",level:3},{value:"\ud83d\udc1e <code>--debug</code>",id:"---debug",level:3},{value:"\ud83e\uddf9 3. Data Safety: <code>GenerateExcludeList()</code>",id:"-3-data-safety-generateexcludelist",level:2},{value:"\ud83d\udca1 Pro insight",id:"-pro-insight",level:3}];function a(e){const s={code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",hr:"hr",li:"li",ol:"ol",p:"p",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(s.header,{children:(0,i.jsxs)(s.h1,{id:"\ufe0f-the-engineer-planner-plan-compilation",children:["\u2699\ufe0f The Engineer: ",(0,i.jsx)(s.code,{children:"planner"})," (Plan Compilation)"]})}),"\n",(0,i.jsxs)(s.p,{children:["The ",(0,i.jsx)(s.code,{children:"planner"})," package is the bridge between the abstraction of the Brain (rendered by the ",(0,i.jsx)(s.code,{children:"parser"}),") and the executors (",(0,i.jsx)(s.code,{children:"oa"})," in C, ",(0,i.jsx)(s.code,{children:"coa ell"})," in Go). Its job is to compile the user's intentions into the ",(0,i.jsx)(s.em,{children:"Single Source of Truth"}),": ",(0,i.jsx)(s.code,{children:"/tmp/coa/oa-plan.json"}),', a JIT "compiler" running right before takeoff.']}),"\n",(0,i.jsxs)(s.p,{children:["This package executes no command on the system: it ",(0,i.jsx)(s.em,{children:"prepares the ground"})," so that the engines can work without surprises."]}),"\n",(0,i.jsx)(s.hr,{}),"\n",(0,i.jsxs)(s.h2,{id:"-1-the-task-format-oatask-the-embedding-trick",children:["\ud83e\uddf1 1. The Task Format: ",(0,i.jsx)(s.code,{children:"OATask"})," (the embedding trick)"]}),"\n",(0,i.jsxs)(s.p,{children:["In ",(0,i.jsx)(s.code,{children:"types.go"})," the planner defines ",(0,i.jsx)(s.code,{children:"OATask"})," by ",(0,i.jsx)(s.strong,{children:"embedding"})," ",(0,i.jsx)(s.code,{children:"parser.Step"})," as an anonymous field: thanks to JSON flattening, the task automatically inherits ",(0,i.jsx)(s.code,{children:"name"}),", ",(0,i.jsx)(s.code,{children:"module"}),", ",(0,i.jsx)(s.code,{children:"chroot"})," and ",(0,i.jsx)(s.code,{children:"params"})," from the Brain, and the planner only adds the technical fields the engines need at runtime:"]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"work_dir"})," \u2014 the workspace root (e.g. ",(0,i.jsx)(s.code,{children:"/home/eggs"}),"), used by the native ",(0,i.jsx)(s.code,{children:"umount"})," module;"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"live_root"})," \u2014 the resolved live filesystem path (",(0,i.jsx)(s.code,{children:"<work_dir>/liveroot"}),"), used by ",(0,i.jsx)(s.code,{children:"users"})," and by every chrooted Go worker;"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.code,{children:"type"}),", ",(0,i.jsx)(s.code,{children:"opts"}),", ",(0,i.jsx)(s.code,{children:"readonly"})," \u2014 legacy mount metadata kept for the transition."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["The full plan (",(0,i.jsx)(s.code,{children:"OAPlan"}),") wraps the task array together with the ",(0,i.jsx)(s.code,{children:"settings"})," and the ",(0,i.jsx)(s.code,{children:"is_github_action"})," flag."]}),"\n",(0,i.jsx)(s.hr,{}),"\n",(0,i.jsxs)(s.h2,{id:"\ufe0f-2-the-compiler-generateplan",children:["\u2699\ufe0f 2. The Compiler: ",(0,i.jsx)(s.code,{children:"GeneratePlan()"})]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"GeneratePlan()"})," iterates over ",(0,i.jsx)(s.code,{children:"profile.Remaster"})," and switches on each step's ",(0,i.jsx)(s.code,{children:"module"}),". Most steps pass through untouched \u2014 the planner just injects ",(0,i.jsx)(s.code,{children:"live_root"})," \u2014 but a few modules are ",(0,i.jsx)(s.em,{children:"expanded"}),":"]}
1),"\n",(0,i.jsxs)(s.h3,{id:"mount_logic--environment-bootstrap",children:[(0,i.jsx)(s.code,{children:"mount_logic"})," \u2192 environment bootstrap"]}),"\n",(0,i.jsxs)(s.p,{children:["The single abstract directive becomes one ",(0,i.jsx)(s.code,{children:"shell"})," task invoking ",(0,i.jsx)(s.code,{children:"/etc/penguins-eggs.d/scripts/bootstrap-liveroot.sh <work_dir> <is_github_action>"}),", the script that builds the whole staging tree (liveroot, overlays, bind mounts, API filesystems)."]}),"\n",(0,i.jsxs)(s.h3,{id:"users--two-concrete-tasks-oa-usersgo",children:[(0,i.jsx)(s.code,{children:"users"})," \u2192 two concrete tasks (",(0,i.jsx)(s.code,{children:"oa-users.go"}),")"]}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.code,{children:"create-live-home"})})," (",(0,i.jsx)(s.code,{children:"shell"}),"): creates the home directory and populates it from ",(0,i.jsx)(s.code,{children:"/etc/skel"}),"."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.code,{children:"inject-live-users"})})," (",(0,i.jsx)(s.code,{children:"users"}),", native C): injects the identities. The live user comes from ",(0,i.jsx)(s.code,{children:"settings"})," (fallback ",(0,i.jsx)(s.code,{children:"live"}),"); the password is hashed ",(0,i.jsx)(s.strong,{children:"in Go"})," by ",(0,i.jsx)(s.code,{children:"hashPassword()"})," \u2014 already-hashed values (",(0,i.jsx)(s.code,{children:"$6$\u2026"}),") pass through, plaintext is hashed with ",(0,i.jsx)(s.code,{children:"openssl passwd -6"}),", and an empty password falls back to the default ",(0,i.jsx)(s.code,{children:"eggs"})," hash. The groups of the host user are mirrored onto the live user via ",(0,i.jsx)(s.code,{children:"utils.GetUserGroups()"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(s.h3,{id:"umount--the-guaranteed-cleanup",children:[(0,i.jsx)(s.code,{children:"umount"})," \u2192 the guaranteed cleanup"]}),"\n",(0,i.jsxs)(s.p,{children:["Compiled into the ",(0,i.jsx)(s.code,{children:"cleanup"})," task carrying ",(0,i.jsx)(s.code,{children:"work_dir"}),", executed natively by the C engine."]}),"\n",(0,i.jsxs)(s.h3,{id:"--crypted--luks-pipeline-injection-luks-stepsgo",children:[(0,i.jsx)(s.code,{children:"--crypted"})," \u2192 LUKS pipeline injection (",(0,i.jsx)(s.code,{children:"luks-steps.go"}),")"]}),"\n",(0,i.jsxs)(s.p,{children:["When the mode is ",(0,i.jsx)(s.code,{children:"crypted"}),", the planner intercepts three standard steps and replaces or extends them:"]}),"\n",(0,i.jsxs)(s.ol,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.code,{children:"initramfs"})})," \u2192 replaced by ",(0,i.jsx)(s.code,{children:"luks-initrd-prep"}),": injects ",(0,i.jsx)(s.code,{children:"boot-encrypted-root.sh"})," into the live initramfs hooks, writes a dummy ",(0,i.jsx)(s.code,{children:"/etc/crypttab"})," to pull in ",(0,i.jsx)(s.code,{children:"cryptsetup"}),", and regenerates the initrd with LUKS support."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.code,{children:"copy-kernel-initrd"})})," \u2192 replaced by ",(0,i.jsx)(s.code,{children:"luks-kernel-copy"}),": copies the LUKS-aware initrd instead of the standard one."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.code,{children:"mksquashfs"})})," \u2192 kept, but immediately followed by ",(0,i.jsx)(s.code,{children:"luks-wrap-squashfs"}),": the plain ",(0,i.jsx)(s.code,{children:"filesystem.squashfs"})," is wrapped inside a LUKS2 ext4 container (",(0,i.jsx)(s.code,{children:"root.img"}),"). The passphrase is passed via ",(0,i.jsx)(s.code,{children:"printf | cryptsetup --key-file -"})," and never touches the disk."]}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["The boot parameters are also updated to include ",(0,i.jsx)(s.code,{children:"live-media"})," so the live-boot stack knows to look for the LUKS container. The ",(0,i.jsx)(s.code,{children:"autologin-gui"})," step is skipped (no live user in crypted mode). Currently Debian family only."]}),"\n",(0,i.jsxs)(s.h3,{id:"xorriso--parameter-injection--disk-metadata",children:[(0,i.jsx)(s.code,{children:"xorriso"})," \u2192 parameter injection + ",(0,i.jsx)(s.code,{children:".disk"})," metadata"]}),"\n",(0,i.jsxs)(s.p,{children:["The planner resolves ",(0,i.jsx)(s.code,{children:"params.output_file"})," (the final ISO path computed from the distro identity) and ",(0,i.jsx)(s.code,{children:"params.source_dir"})," (",(0,i.jsx)(s.code,{children:"<work_dir>/isodir"}),"). Right before it, a ",(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.code,{children:"coa-dot-disk"})})," shell task is inserted: it generates the ",(0,i.jsx)(s.code,{children:".disk"})," metadata directory (info, UUID from the kernel, timestamp) following the Debian live-boot standard."]}),"\n",(0,i.jsxs)(s.h3,{id:"-the-breakpoint---stop-after",children:["\ud83d\uded1 The breakpoint (",(0,i.jsx)(s.code,{children:"--stop-after"}),")"]}),"\n",(0,i.jsxs)(s.p,{children:["If a breakpoint is set, once the named step has been compiled the planner ",(0,i.jsx)(s.em,{children:"discards"})," every subsequent step \u2014 with one vital exception: the ",(0,i.jsx)(s.code,{children:"cleanup"})," task is always kept, guaranteeing the host is never left with orphaned mounts. The environment stops mid-flight, mounted and ready for manual inspection."]}),"\n",(0,i.jsx)(s.h3,{id:"-the-normalization-pass",children:"\ud83d\udd27 The normalization pass"}),"\n",(0,i.jsxs)(s.p,{children:["After the loop, a final pass enforces coherence: every task that declares a ",(0,i.jsx)(s.code,{children:"work_dir"})," gets the real workspace path, every task that declares (or needs, because ",(0,i.jsx)(s.code,{children:"chroot: true"}),") a ",(0,i.jsx)(s.code,{children:"live_root"})," gets ",(0,i.jsx)(s.code,{children:"<work_dir>/liveroot"}),". The Brain never has to spell out absolute paths."]}),"\n",(0,i.jsxs)(s.h3,{id:"---debug",children:["\ud83d\udc1e ",(0,i.jsx)(s.code,{children:"--debug"})]}),"\n",(0,i.jsxs)(s.p,{children:["With the debug flag, the plan is pretty-printed to the terminal and ",(0,i.jsx)(s.code,{children:"coa"})," exits without remastering \u2014 the fastest way to inspect exactly what ",(0,i.jsx)(s.code,{children:"oa"})," would receive."]}),"\n",(0,i.jsxs)(s.p,{children:["The result is written by ",(0,i.jsx)(s.code,{children:"savePlan()"})," to ",(0,i.jsx)(s.code,{children:"/tmp/coa/oa-plan.json"}),"."]}),"\n",(0,i.jsx)(s.hr,{}),"\n",(0,i.jsxs)(s.h2,{id:"-3-data-safety-generateexcludelist",children:["\ud83e\uddf9 3. Data Safety: ",(0,i.jsx)(s.code,{children:"GenerateExcludeList()"})]}),"\n",(0,i.jsxs)(s.p,{children:["Called by ",(0,i.jsx)(s.code,{children:"remaster"})," before planning, it generates ",(0,i.jsx)(s.code,{children:"/tmp/coa/excludes.list"})," \u2014 what ",(0,i.jsx)(s.code,{children:"mksquashfs"})," must ",(0,i.jsx)(s.em,{children:"not"})," put into the ISO:"]}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Virtual and temporary filesystems:"})," ",(0,i.jsx)(s.code,{children:"dev"}),", ",(0,i.jsx)(s.code,{children:"proc"}),", ",(0,i.jsx)(s.code,{children:"sys"}),", ",(0,i.jsx)(s.code,{children:"run"}),", ",(0,i.jsx)(s.code,{children:"tmp"}),", ",(0,i.jsx)(s.code,{children:"var/tmp"})," (including hidden files via the ",(0,i.jsx)(s.code,{children:".??*"})," pattern), plus the workspace itself (",(0,i.jsx)(s.code,{children:"home/eggs/.overlay"}),", ",(0,i.jsx)(s.code,{children:"home/eggs/isodir"}),", previous ",(0,i.jsx)(s.code,{children:"*.iso"}),")."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"System identity:"})," ",(0,i.jsx)(s.code,{children:"etc/fstab"}),", ",(0,i.jsx)(s.code,{children:"etc/mtab"}),", host SSH keys (",(0,i.jsx)(s.code,{children:"etc/ssh/ssh_host_*"}),"), saved network connections (",(0,i.jsx)(s.code,{children:"NetworkManager/system-connections/*"})," and its ",(0,i.jsx)(s.code,{children:"secret_key"}),"), persistent udev rules, swapfile."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Package caches:"})," ",(0,i.jsx)(s.code,{children:"apt"})," archives and the heavy ",(0,i.jsx)(s.code,{children:"*.bin"})," indexes, ",(0,i.jsx)(s.code,{children:"pacman/pkg"}),", ",(0,i.jsx)(s.code,{children:"dnf"}),"."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"The Debian cryptdisks hack:"}
1)," a single wildcard (",(0,i.jsx)(s.code,{children:"etc/rc*.d/*cryptdisks*"}),") \u2014 ",(0,i.jsx)(s.code,{children:"mksquashfs -wildcards"})," does the scanning, no Go code needed."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Privacy by mode:"})," in standard mode ",(0,i.jsx)(s.code,{children:"root/*"})," (hidden files included) is razed; with ",(0,i.jsx)(s.code,{children:"--clone"}),"/",(0,i.jsx)(s.code,{children:"--crypted"})," user data survives but shell histories, trash bins and browser caches are still purged."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"GitHub Actions slimming:"})," on a runner, ",(0,i.jsx)(s.code,{children:"usr"}),", ",(0,i.jsx)(s.code,{children:"var"})," and ",(0,i.jsx)(s.code,{children:"opt"})," are stripped \u2014 the structural smoketest exercises the whole chain (mksquashfs, xorriso, umount) in minutes, producing a non-functional but valid ISO."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"User exclusions:"})," ",(0,i.jsx)(s.code,{children:"/etc/penguins-eggs.d/custom.exclude.list"}),", if present, is sanitized (comments skipped, leading slashes removed) and appended."]}),"\n"]}),"\n",(0,i.jsx)(s.hr,{}),"\n",(0,i.jsx)(s.h3,{id:"-pro-insight",children:"\ud83d\udca1 Pro insight"}),"\n",(0,i.jsxs)(s.p,{children:["The planner is the last point where ",(0,i.jsx)(s.em,{children:"decisions"})," are made. Downstream of ",(0,i.jsx)(s.code,{children:"oa-plan.json"})," there is only execution: ",(0,i.jsx)(s.code,{children:"oa"})," walks the array and routes each task by ",(0,i.jsx)(s.code,{children:"module"})," \u2014 natively in C or to ",(0,i.jsx)(s.code,{children:"coa ell"}),". If something behaves unexpectedly, ",(0,i.jsx)(s.code,{children:"eggs remaster --debug"})," shows you the exact contract handed to the engines."]})]})}function h(e={}){const{wrapper:s}={...(0,r.R)(),...e.components};return s?(0,i.jsx)(s,{...e,children:(0,i.jsx)(a,{...e})}):a(e)}},10506(e,s,n){n.d(s,{R:()=>o,x:()=>d});var t=n(12888);const i={},r=t.createContext(i);function o(e){const s=t.useContext(r);return t.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function d(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),t.createElement(r.Provider,{value:s},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.