{"id":51742836,"url":"https://github.com/iwillig/dev-linux","last_synced_at":"2026-07-18T15:19:56.589Z","repository":{"id":365315741,"uuid":"1271315229","full_name":"iwillig/dev-linux","owner":"iwillig","description":null,"archived":false,"fork":false,"pushed_at":"2026-07-01T16:08:10.000Z","size":8589,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-04T00:29:05.973Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Dockerfile","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/iwillig.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-16T14:41:13.000Z","updated_at":"2026-07-01T16:08:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/iwillig/dev-linux","commit_stats":null,"previous_names":["iwillig/dev-linux"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/iwillig/dev-linux","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iwillig%2Fdev-linux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iwillig%2Fdev-linux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iwillig%2Fdev-linux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iwillig%2Fdev-linux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/iwillig","download_url":"https://codeload.github.com/iwillig/dev-linux/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iwillig%2Fdev-linux/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35621765,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-18T02:00:07.223Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-18T15:19:55.805Z","updated_at":"2026-07-18T15:19:56.581Z","avatar_url":"https://github.com/iwillig.png","language":"Dockerfile","funding_links":[],"categories":[],"sub_categories":[],"readme":"# dev-linux\n\nA custom Fedora Atomic image based on [Bluefin\nDX](https://projectbluefin.io/), optimized for development on a\nFramework Intel laptop.\n\nBluefin DX is itself built on **Fedora Silverblue** — Fedora's atomic,\nimmutable desktop variant. The base OS lives on a read-only `ostree` commit;\n`bootc` layers container images on top of it, and updates replace the whole\nimage atomically instead of patching packages in place. That gives this\nproject instant rollback (`bootc rollback`), an image that's identical on\nevery machine it's deployed to, and a build that's just a `Containerfile` —\nno snowflake state to drift out of sync.\n\nThis repo also works as a template for the general workflow: define your\ndesired system as a `Containerfile` (or fragments, as below) layered on any\n`bootc`-enabled Fedora Atomic base, build it with `podman`/`buildah`, test it\nin a throwaway container or VM before trusting it, and ship it via\n`bootc switch`/`bootc update`. The same pattern extends to CoreOS, other\n`ublue-os` images, or a from-scratch atomic image of your own.\n\n## What's included\n\n- **Base**: `ghcr.io/ublue-os/bluefin-dx:stable` — GNOME + dev tooling baseline (Fedora 44)\n- **Shell**: fish (default), with starship prompt\n- **Terminal**: Alacritty, Zellij\n- **Editors**: Emacs, Neovim\n- **Dev tools**: gcc/make/gdb (Development Tools group), pandoc, aspell, fd, bat, eza, zoxide, fzf, jq, yq, httpie, zellij\n- **Browsers**: Firefox, Nyxt\n- **Apps**: 1Password\n- **Fonts**: JetBrains Mono Nerd Font, Cascadia Code, Inter — with tuned subpixel rendering\n- **Framework extras**: thermald, fprintd (fingerprint reader), powertop\n\n---\n\n## Design goal: safe, agent-driven iteration\n\nThe point of building on an atomic, immutable base isn't just rollback safety —\nit's that the *entire system* is described as version-controlled config, which\nmeans an AI coding agent (or you, moving fast) can propose a change, and that\nchange can be linted and tested before it ever touches a machine you actually\nboot into. Nothing is installed by hand; if it's not in `Containerfile.d/` or\n`config/files/`, it doesn't exist in the next build. That constraint is what\nmakes the loop below safe to run unattended: every change is a diff, every\ndiff is checked twice, and the worst case is `bootc rollback`.\n\n```mermaid\nflowchart TD\n    A[Edit Containerfile.d fragment or config/files] --\u003e B[\"just assemble\"]\n    B --\u003e C[\"just lint (ShellCheck)\"]\n    C --\u003e D[\"just build \u0026\u0026 just test (ShellSpec, throwaway container)\"]\n    D -- fails --\u003e A\n    D -- passes --\u003e E[git push]\n    E --\u003e F[\"CI: re-assemble, diff-check, lint, build, ShellSpec\"]\n    F -- fails --\u003e A\n    F -- passes on main --\u003e G[\"image pushed to ghcr.io/iwillig/dev-linux\"]\n    G --\u003e H[\"sudo bootc update \u0026\u0026 sudo reboot\"]\n    H --\u003e I{Working well?}\n    I -- yes --\u003e A\n    I -- no --\u003e J[\"sudo bootc rollback \u0026\u0026 sudo reboot\"]\n    J --\u003e A\n```\n\nEvery change gets tested twice before it's live: once locally against a\nthrowaway container, once more in CI against the exact image that would ship.\nThe only step that isn't automatically checked is `bootc update` itself on\nreal hardware — and even that is one command away from undone.\n\n---\n\n## How this project works\n\nThe `Containerfile` is generated from small, purpose-scoped fragments that get\nassembled, tested, and shipped by CI. The pieces:\n\n### `Containerfile.d/*.containerfile` — the actual build recipe\n\nEach fragment layers one concern on top of `ghcr.io/ublue-os/bluefin-dx:stable`,\nnumbered so they apply in a predictable order:\n\n| Fragment | What it adds |\n|---|---|\n| `000-header` | Base image + tag args (`bluefin-dx:stable`) |\n| `010-hardware` | Framework-specific packages: `thermald`, `fprintd` (fingerprint reader), `powertop` |\n| `020-devtools` | Compiler toolchain (`development-tools` group), Emacs, Neovim, tmux, pandoc, LaTeX, fish, Alacritty, Firefox, CLI staples (ripgrep, fzf, bat, eza, jq, gh, ShellCheck, …), and dotfiles tooling (`stow`, `gitleaks`) |\n| `030-cli-tools` | Tools not packaged for Fedora: zellij, lazygit, fastfetch, starship, the `whis` voice-to-text CLI, `trufflehog` secret scanner |\n| `040-browsers` | Nyxt (Lisp-extensible browser), installed from its AppImage tarball |\n| `050-package-managers` | Homebrew and SDKMAN!, installed into `/var` so they survive `bootc` updates and are writable by the `wheel` group without sudo |\n| `060-languages` | Node/npm-based tooling (Claude Code CLI, TypeScript + LSP, the Pi coding agent), Clojure, and Rust (Fedora's `rustc`/`cargo`, not rustup) |\n| `070-apps` | Handy and Whis desktop — push-to-talk speech-to-text GUIs |\n| `080-gis` | Spatial/GIS stack: GDAL, Mapnik, QGIS, GRASS, PROJ, GEOS, SpatiaLite |\n| `090-wine` | Wine + winetricks for running Windows binaries |\n| `100-fonts` | JetBrains Mono, Cascadia Code, Noto Emoji, Inter, and the Nerd Font-patched JetBrains Mono |\n| `110-config` | Copies `config/files/` into the image root and enables the first-boot 1Password install service |\n| `120-sway` | Sway tiling WM stack (waybar, wofi, mako, kanshi, swaylock/idle/bg) alongside GNOME — GDM picks it up automatically as a login session |\n| `121-elementary-theme` | Builds the elementary GTK stylesheet from source (not packaged for Fedora) |\n| `125-llama-cpp` | Local LLM inference via prebuilt `llama.cpp` CPU binaries, pinned to a specific release tag |\n\nRun `just assemble` to concatenate these into the real `Containerfile` (the\ngenerated file carries a header telling you not to edit it directly). CI\nre-runs `just assemble` and fails the build if the checked-in `Containerfile`\ndoesn't match — so the fragments are always the source of truth.\n\n### `config/files/` — a filesystem overlay\n\nMirrors the target root filesystem (`etc/`, `usr/share/`) and gets `COPY`'d\nwholesale into the image by `110-config.containerfile`. This is where\ndeclarative config lives instead of `RUN sed`/`echo` hacks: fish integration\nsnippets for Homebrew/SDKMAN, fontconfig tuning, GTK settings, the kanshi\noutput-switching rules, the full waybar config (modules + CSS tokens), sway\nkeybindings, swaylock/gammastep config, and the desktop wallpaper.\n\n### Dotfiles: system config vs. personal config\n\n`config/files/` (above) only covers *system-wide* config — the stuff baked\ninto the public image at `/etc` and `/usr/share`. It deliberately never\nholds anything personal or secret, because this repo's image is pushed\npublic to GHCR.\n\nPersonal, per-user config (editor settings, terminal themes, shell\naliases, API tokens) lives in a **separate, private** dotfiles repo,\nmanaged with [GNU Stow](https://www.gnu.org/software/stow/) — installed\nas a system package by `020-devtools.containerfile` so it's always\navailable, without this repo ever needing to see what's inside it.\n\nThe pattern:\n\n- **Keep the dotfiles repo private on GitHub.** It's the only thing\n  standing between your editor config and your API tokens, SSH config,\n  and shell history conventions. Clone it over SSH\n  (`git@github.com:\u003cyou\u003e/dotfiles.git`), not HTTPS — SSH auth to a\n  private repo doesn't depend on a PAT sitting in a credential helper.\n- **Lay it out as Stow packages.** Each top-level directory mirrors\n  `$HOME` from the point where it should be symlinked, e.g.\n  `fish/.config/fish/config.fish` or `emacs/.emacs.d/init.el`. Running\n  `stow fish` from the repo root symlinks `fish/.config/fish` into\n  `~/.config/fish`; `stow -D fish` removes the symlinks. This keeps the\n  repo itself as the single source of truth — `~/.config/fish/config.fish`\n  is a symlink, not a copy, so edits in place are edits to the repo.\n- **Never put secrets in the repo, even though it's private.** Private\n  repos still end up in local clones, backups, and (if a repo is ever\n  made public by mistake) full history. Reference secrets through\n  [1Password's CLI](https://developer.1password.com/docs/cli/) instead:\n  `set -x HF_TOKEN (op read \"op://Personal/HF_TOKEN/credential\")` in\n  `fish/config.fish`, for example. The repo holds the *lookup*, never the\n  *value*. 1Password (`070-apps.containerfile`) and its SSH agent\n  integration are already part of this image for exactly this reason.\n- **Gitignore anything that isn't config.** Editors and agent tools\n  routinely drop caches, session logs, and backup files inside the same\n  directories your dotfiles live in (`.bak` files, `sessions/`,\n  `*-cache.json`, `npm/`). None of that belongs in version control —\n  add it to the dotfiles repo's `.gitignore` before the first `git add`,\n  not after something sensitive slips in.\n- **Scan before you push.** `gitleaks` (added in `020-devtools.containerfile`,\n  alongside `stow`) scans a repo's working tree *and* full history for\n  credential-shaped strings. Run `gitleaks git .` from the dotfiles repo\n  periodically, or wire it in as a pre-commit hook\n  (`gitleaks protect --staged`), so an accidentally hardcoded token gets\n  caught locally instead of after `git push`.\n\n### `justfile` — the command surface\n\nTask runner for every workflow in this repo: assembling and building the\nimage (`build`, `build-fresh`), poking at it (`shell`, `check-fonts`,\n`check-packages`), linting the repo's shell scripts (`lint`), running the\ntest suite (`test`, `test-fast`), swapping your live Framework laptop onto a\nlocally-built image (`local-switch`), managing the QEMU test VM (`vm-*`),\nwriting installer ISOs to USB (`usb-list`, `usb-write`), and cutting releases\n(`release`, `download-release`).\n\n### `spec/` — ShellSpec test suite\n\nBehavioral tests that run *inside* the built container image (via\n`vendor/shellspec`, a vendored copy of the [ShellSpec](https://shellspec.info/)\nBDD framework) to confirm the build actually produced a working system:\neditors and CLI tools are on `PATH` and report sane `--version` output,\nfonts are registered with fontconfig, Sway/waybar/swaylock are installed,\nlanguage runtimes are pinned to expected versions, llama.cpp's binaries and\nbundled shared libraries are in place, and config files (default shell,\nHomebrew/SDKMAN ownership) landed correctly.\n\n### `.github/workflows/` — CI/CD\n\n- **`build.yml`** — on every push/PR to `main` (and weekly on a cron, to catch\n  upstream Bluefin drift): assembles the Containerfile, verifies it's in sync,\n  lints the repo's shell scripts with ShellCheck (`just lint`), builds the\n  image, runs the full ShellSpec suite against it, and (main branch only)\n  pushes to `ghcr.io/iwillig/dev-linux`.\n- **`build-disk-images.yml`** — on a `v*` tag push (or manual dispatch):\n  rebuilds and pushes the OCI image, then runs `bootc-image-builder` twice to\n  produce a bootable `qcow2` (for QEMU testing) and an Anaconda `iso` (for USB\n  installs), compresses them, and attaches both to a GitHub Release.\n\n### `vm/`\n\nWorking directory for QEMU test-VM artifacts (disk images, downloaded ISOs,\nOVMF firmware vars) created by the `just vm-*` recipes — gitignored, not part\nof the build itself.\n\n---\n\n## Installing on the Framework laptop\n\nThe recommended path uses a custom installer ISO built from your exact\nOCI image via\n[`bootc-image-builder`](https://github.com/osbuild/bootc-image-builder). You\nboot from it and the Anaconda installer puts your image directly onto\ndisk — no internet required on the target machine after that.\n\n### Step 1 — Make the GHCR image public\n\nGo to **github.com/iwillig/dev-linux → Packages → dev-linux → Package settings → Change visibility → Public**.\n\nThis is required for `bootc-image-builder` and `bootc` to pull the image without credentials.\n\n### Step 2 — Build and download the installer ISO\n\nTag a release to trigger the CI build:\n\n```bash\njust release v0.1.0\n```\n\nCI will build the OCI image, run `bootc-image-builder` to produce a custom Anaconda ISO, and attach it to the GitHub Release. Once the run finishes (~15 min), download it:\n\n```bash\njust download-release   # saves to vm/\n```\n\nOr download manually from the [Releases page](https://github.com/iwillig/dev-linux/releases).\n\n### Step 3 — Write the ISO to USB\n\n```bash\n# Find your USB device\njust usb-list\n\n# Write the ISO (replace /dev/disk4 with your USB device)\nsudo dd if=vm/dev-linux-v0.1.0.iso of=/dev/disk4 bs=4m status=progress\n```\n\n\u003e On macOS use `just usb-write /dev/disk4` — it handles unmounting and uses the raw device automatically.\n\n### Step 4 — Install\n\n1. Plug USB into Framework, power on, press F12 for boot menu\n2. Select the USB drive\n3. Follow the Anaconda installer — partition as you like, set username/password\n4. Reboot — you're running your custom image\n\n### Step 5 — Updating after installation\n\nWhenever you add or change packages, push to `main`, wait for CI to finish, then on the Framework:\n\n```bash\nsudo bootc update\nsudo reboot\n```\n\n`bootc update` pulls only the changed layers from GHCR (fast after the\nfirst pull), stages the new image alongside the running one, and\nactivates it on next boot. Your data in `/home` is untouched.\n\nTo check whether an update is available without applying it:\n\n```bash\nsudo bootc status\n```\n\nTo roll back to the previous image if something goes wrong:\n\n```bash\nsudo bootc rollback\nsudo reboot\n```\n\n---\n\n## Alternative: bootc switch from stock Silverblue\n\nIf you already have Fedora Silverblue installed, you can rebase to\nthis image directly without a custom ISO:\n\n```bash\nsudo bootc switch ghcr.io/iwillig/dev-linux:latest\nsudo reboot\n```\n\nAfter the initial switch, future updates work the same way:\n\n```bash\nsudo bootc update\nsudo reboot\n```\n\n---\n\n## Local development\n\nRequires `podman` and `just`. Works on both Linux (native) and macOS.\n\n```bash\njust build          # build amd64 image locally via podman\njust shell          # open bash inside the built image\njust test           # smoke-test: verify key commands and fonts\njust check-fonts    # list installed fonts\njust check-packages # list installed packages\n```\n\n### Testing on Linux (live system)\n\nSince you're already running dev-linux, the fastest feedback loop is to build\nlocally and switch the running system to your changes:\n\n```bash\njust local-switch   # build → export → sudo bootc switch (staged, not yet active)\nsudo reboot         # activate the new image\n```\n\nIf something breaks after rebooting:\n\n```bash\nsudo bootc rollback\nsudo reboot\n```\n\n`bootc` keeps the previous image around, so rollback is instant.\n\n### Testing on macOS\n\nRequires `podman` (`brew install podman`) and `just`.\n\n---\n\n## QEMU testing\n\nTest the image in an isolated VM before installing on bare metal. Downloads a\nstock Fedora Silverblue ISO, installs it into a QEMU disk, then you rebase to\nyour custom image. On Linux the VM uses KVM hardware acceleration; on macOS it\nfalls back to TCG software emulation.\n\n```bash\njust vm-download-iso   # one-time: download Fedora 44 Silverblue ISO (~2.5 GB)\njust vm-create         # one-time: create 60 GB disk (+ init OVMF_VARS on Linux)\njust vm-install        # one-time: boot installer, follow Anaconda\njust vm-run            # start the VM\njust vm-ssh            # SSH into the running VM\n\n# Inside the VM, switch to your image:\nsudo bootc switch ghcr.io/iwillig/dev-linux:latest\nsudo reboot\n```\n\nTo test a locally-built image without pushing to GHCR:\n\n```bash\njust vm-snapshot          # save a rollback point\njust vm-load-local        # push local image into VM via SSH\n# Inside VM:\nsudo bootc switch --transport containers-storage localhost/dev-linux:local\nsudo reboot\n```\n\n### Alternatively: use the CI-built qcow2\n\nSkip the Silverblue install step entirely by downloading the pre-built\nqcow2 from a release:\n\n```bash\njust download-release   # downloads and decompresses the qcow2 into vm/\njust vm-run             # boot straight into your image\n```\n\n---\n\n## Releasing\n\n```bash\njust release v0.2.0\n```\n\nThis tags the commit and pushes the tag. GitHub Actions then:\n1. Builds the OCI image and pushes it to `ghcr.io/iwillig/dev-linux:latest`\n2. Runs `bootc-image-builder` to produce `dev-linux-v0.2.0.iso` and `dev-linux-v0.2.0.qcow2.zst`\n3. Attaches both to a GitHub Release\n\nYou can also trigger the disk image build manually from the Actions tab (useful for testing the ISO without tagging).\n\n---\n\n## Displays\n\nThe laptop panel (`eDP-1`) auto-disables whenever the ViewSonic XG3220\nexternal monitor is connected, so the external monitor is the only active\ndisplay rather than extending/mirroring — handled by `kanshi`\n(`/etc/kanshi/config`), matched by monitor make/model/serial rather than\nconnector name so it keeps working regardless of which port/dock it's\nplugged into.\n\n- **sway**: automatic via kanshi. Manual override: `mod+shift+i` (laptop\n  only) / `mod+shift+o` (external only), or `kanshictl switch laptop|external`\n  from a terminal.\n- **GNOME**: kanshi requires the wlr-output-management protocol, which\n  mutter doesn't implement, so switching there is manual — use Settings →\n  Displays, or `gnome-monitor-config list` / `set` from a terminal.\n\nTo support a different external monitor, update the `output \"...\"` match\nstring in `/etc/kanshi/config` — get the exact make/model/serial via\n`swaymsg -t get_outputs`.\n\n---\n\n## Known issues\n\n### Zoom screen sharing fails on sway (\"No supported targets specified\")\n\nSharing a screen or window in Zoom silently fails to start. The portal logs\nthe error:\n\n```\njournalctl --user -u xdg-desktop-portal-wlr\n[ERROR] - wlroots: No supported targets specified\n```\n\nThis is an upstream bug in `xdg-desktop-portal-wlr` 0.8.1/0.8.2 (the version\nshipped in Fedora 44):\n[emersion/xdg-desktop-portal-wlr#379](https://github.com/emersion/xdg-desktop-portal-wlr/issues/379).\n`SelectSources` defaults its requested type-mask to `0` instead of `MONITOR`\nwhen the caller omits the `types` option, so the type intersection is always\nempty and the call fails — regardless of whether you pick a monitor or a\nwindow in the share dialog. It is not specific to Zoom; any client that omits\n`types` (per the portal spec, which allows this) hits it.\n\nAs of 2026-06-20, the latest upstream release (0.8.3) does not include a fix.\nA one-line patch is posted on the issue and has been community-confirmed to\nwork, but isn't merged yet. Revisit once upstream merges a fix or Fedora\npicks up a patched build — until then, no workaround is applied in this\nimage.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiwillig%2Fdev-linux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fiwillig%2Fdev-linux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiwillig%2Fdev-linux/lists"}