Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .claude/skills/build-pipeline/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
name: build-pipeline
description: DistinctionOS image build architecture — execution order, Containerfile mounts, OCI cache artifacts, and build-container gotchas. Use when editing the Containerfile, anything in build_files/, the GitHub Actions workflows, or diagnosing a failed image build.
---

# DistinctionOS Build Pipeline

## Execution order (Containerfile RUN chain — the single source of truth)

| Step | Script | Purpose | Failure mode |
|------|--------|---------|--------------|
| 1 | `00-kernel.sh` | Swap stock kernel → kernel-cachyos-lto | Hard fail (`set -e`) |
| 2 | `01-kernel-modules.sh` | Regenerate initramfs via dracut | Hard fail |
| 3 | `02-build.sh` | Package install from manifests | **Best-effort** — deliberately no `set -e`; tracks FAILED_PACKAGES, only a missing `steam` fails the build |
| 4 | `03-cache-install.sh` | RPMs from `distinctionos-cache` OCI artifact | — |
| 5 | `04-force-install.sh` | `rpm --force --nodeps` RPMs from OCI artifact | — |
| 6 | `05-remote-grabber.sh` | GNOME Shell extensions (commit-pinned) | Hard fail on any extension |
| 7 | `06-fix-opt.sh` | /opt persistence via tmpfiles.d | Hard fail |
| 8 | `07-config.sh` | Services, shell defaults, misc | Hard fail |
| 9 | `08-validate.sh` | Environment sanity checks | Soft by default (`VALIDATION_SOFT=1`) |

If you add/remove/reorder a step: update the Containerfile, this table,
and README.md in the same commit.

## Containerfile mount gotchas

- `/tmp` is a **tmpfs mount** — contents vanish; never stage files there
that must reach the image. Pre-cached RPMs live in `/var/tmp/` for this
exact reason.
- `/var/cache` and `/var/log` are **BuildKit cache mounts** — they persist
across builds. This is why 08-validate.sh wipes the ldconfig aux-cache;
treat anything read from these paths as potentially stale.
- Build scripts are bind-mounted at `/ctx/`, not copied into the image.
Manifests are therefore at `/ctx/manifests/`.
- An optional GitHub token is mounted at `/run/secrets/github_token`
(use the `github_api_get` helper in 02-build.sh, never raw curl to
api.github.com).

## Package data lives in manifests

`build_files/manifests/*.list` — one package per line, `#` comments.
`coprs.list` format: `owner/project pkg [pkg...]`. To add a package, edit
the manifest; touch 02-build.sh only for new *behaviour*.

## CI facts

- `paths-ignore` must NOT include `system_files/**` — those files are baked
into the image.
- Image builds with `sudo buildah bud`; the rechunker and cosign signing
run afterwards. Scheduled rebuild every 5 days.
- Cache and force-install RPMs come from separate workflows
(`build-cache.yml`, `build-force-install.yml`) pushed to GHCR; `:latest`
is rolling, `:YYYYMMDD` is pinned.
40 changes: 40 additions & 0 deletions .claude/skills/keep-it-simple/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
name: keep-it-simple
description: Guardrail against over-engineering. Use at the START of any task that involves writing or refactoring code, designing a fix, or proposing an implementation — before generating the solution.
---

# Keep It Simple

This is a single-maintainer hobby OS, not a platform team's monorepo. Every
line added is a line the maintainer must understand at 2am when a build
breaks. Optimise for *deletability*, not extensibility.

## Before writing any code, answer these

1. What is the smallest diff that solves the stated problem?
2. Can an existing function/pattern/manifest absorb this instead of a new one?
3. Is any part of my plan solving a problem that wasn't asked about?
If yes — cut it, or mention it in one sentence at the end instead.

## Hard limits

- **No new abstractions** (wrapper functions, config layers, plugin systems,
generic frameworks) unless the user explicitly asked for one.
- **No speculative flexibility** — no "in case you later want to..."
parameters, no handling of inputs that cannot occur.
- **No drive-by refactors.** Fix the thing asked. Note other issues in one
line; do not touch them.
- **>30 lines or >2 files → stop.** Present a ≤5-line plan and wait.
- **Error handling proportionate to consequence**: a cosmetic step gets
`|| log_warning`, not a retry framework.

## Smells that mean you are overcomplicating

- A helper function with exactly one call site.
- An if/else ladder for cases the build can never produce.
- Introducing a new file when 10 lines in an existing one would do.
- Rewriting working code to be "cleaner" while fixing an unrelated bug.
- Explaining the solution takes longer than reading the diff.

When in doubt, ship the boring version. The clever version can always be
requested; the boring version rarely needs to be reverted.
48 changes: 48 additions & 0 deletions .claude/skills/shell-conventions/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: shell-conventions
description: DistinctionOS shell scripting house style — logging functions, error-handling strategy, shellcheck requirements, and patterns to reuse. Use when writing or modifying any .sh file in build_files/ or system_files/.
---

# DistinctionOS Shell Conventions

## Non-negotiables

1. **Shebang**: `#!/usr/bin/bash`
2. **Must pass**: `shellcheck -S error -e SC1091 <file>` — CI gates on this.
3. **Build scripts** source the shared library first:
`source /ctx/95-utility-functions.sh`
4. **Error strategy is deliberate and per-script**:
- `set -euo pipefail` for scripts where any failure should abort.
- `set -uo pipefail` (no `-e`) ONLY for best-effort installers
(02-build.sh). Do not "fix" this by adding `-e`.

## Use the library — do not reinvent

Logging: `log_header`, `log_section`, `log_info`, `log_success`,
`log_warning`, `log_error`. Lifecycle: `script_start`, `script_complete`.
Helpers: `create_dir_with_log`, `counter_display`, `run_with_log`.

Never `echo` raw status lines in build scripts; never invent a new logging
scheme. If a helper is missing, add it to 95-utility-functions.sh rather
than defining it locally in two places.

## Established patterns to reuse (don't redesign)

- **Resilient install**: bulk attempt → individual fallback → record in
SUCCEEDED_PACKAGES / FAILED_PACKAGES. See `install_packages_resilient`.
- **dnf output**: append to `$DNF_LOG`, surface with `dnf_log_tail` on
failure. Never `&>/dev/null` a dnf call.
- **GitHub API**: always via `github_api_get` (handles token + retries).
- **Downloads**: `curl -fL --retry 3 --retry-delay 2`, then verify the
artifact before use; failures log a warning, not silence.
- **Package data**: belongs in `build_files/manifests/`, read via
`read_manifest`.

## Style

- 2-space indent in build_files, 4-space in 05-remote-grabber.sh (match
the file you're in).
- Section banners: `# ===...===` for major, `# ───...───` for minor.
- Quote all expansions; arrays for command construction
(`local -a cmd=(...)` then `"${cmd[@]}"`).
- `readonly` for constants; `local` for everything inside functions.
4 changes: 0 additions & 4 deletions .github/force-install-builder/packages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@
# audacity-freeworld | dnf | rpmfusion-free,rpmfusion-free-updates

mutter-devel | dnf | terra
libavcodec-freeworld | dnf | rpmfusion-free,rpmfusion-free-updates
gstreamer1-plugins-bad-freeworld | dnf | rpmfusion-free,rpmfusion-free-updates
mesa-vulkan-drivers-freeworld | dnf | rpmfusion-free,rpmfusion-free-updates
mesa-va-drivers-freeworld | dnf | rpmfusion-free,rpmfusion-free-updates
vvenc-libs | dnf | rpmfusion-free,rpmfusion-free-updates
gnome-rounded-blur | github-build | kancko/gnome-rounded-blur
glycin-fix-bc7 | github-release | phantomcortex/glycin
152 changes: 152 additions & 0 deletions .github/workflows/_build-oci-artifact.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
name: Build OCI artifact (internal)

# Internal reusable workflow. Shared body of build-cache.yml and
# build-force-install.yml — those workflows are thin callers that pass
# the artifact-specific inputs (image, builder context, version-check
# tag) and inherit secrets. Not meant to be triggered directly; invoke a
# caller workflow instead.

on:
workflow_call:
inputs:
image:
description: 'Full registry path of the artifact to publish (no tag).'
type: string
required: true
builder_dir:
description: 'Directory containing the artifact Containerfile.'
type: string
required: true
version_check_tag:
description: 'Local docker tag for the version-check stage build.'
type: string
required: true
max_age_days:
description: 'Rebuild if the existing artifact is older than this many days.'
type: number
default: 15
force_rebuild:
description: 'Rebuild even if versions match and age <= max_age_days.'
type: boolean
default: false

permissions:
contents: read
packages: write

jobs:
build:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Install tooling
run: |
sudo apt-get update -q
sudo apt-get install -y -q skopeo jq

- name: Log in to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Inspect published artifact
id: current
env:
IMAGE: ${{ inputs.image }}
run: |
if skopeo inspect "docker://${IMAGE}:latest" > current.json 2>/dev/null; then
jq -S -c '.Labels["dev.distinctionos.versions"] // "{}" | fromjson? // {}' current.json > current-versions.json
jq -r '.Created' current.json > current-created.txt
echo "found=true" >> "$GITHUB_OUTPUT"
echo "Current artifact:"
echo " Created: $(cat current-created.txt)"
echo " Versions: $(cat current-versions.json)"
else
echo '{}' > current-versions.json
echo '1970-01-01T00:00:00Z' > current-created.txt
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No artifact published at ${IMAGE}:latest yet"
fi

- name: Query upstream versions
id: upstream
env:
BUILDER_DIR: ${{ inputs.builder_dir }}
CHECK_TAG: ${{ inputs.version_check_tag }}
run: |
docker build \
--target version-check \
-t "${CHECK_TAG}" \
-f "${BUILDER_DIR}/Containerfile" \
"${BUILDER_DIR}"
docker run --rm "${CHECK_TAG}" > upstream-versions.json
echo "Upstream versions: $(cat upstream-versions.json)"

- name: Decide whether to rebuild
id: decide
env:
MAX_AGE_DAYS: ${{ inputs.max_age_days }}
run: |
BUILD=false
REASON=""

if [[ "${{ inputs.force_rebuild }}" == "true" ]]; then
BUILD=true; REASON="force_rebuild input is true"
elif [[ "${{ steps.current.outputs.found }}" == "false" ]]; then
BUILD=true; REASON="no existing artifact"
elif ! diff -q current-versions.json upstream-versions.json >/dev/null 2>&1; then
BUILD=true; REASON="upstream versions changed"
echo "Diff (current → upstream):"
diff -u current-versions.json upstream-versions.json || true
else
NOW=$(date +%s)
CREATED=$(date -d "$(cat current-created.txt)" +%s)
AGE_DAYS=$(( (NOW - CREATED) / 86400 ))
if [[ $AGE_DAYS -gt $MAX_AGE_DAYS ]]; then
BUILD=true; REASON="artifact is ${AGE_DAYS}d old (max ${MAX_AGE_DAYS}d)"
else
REASON="artifact is ${AGE_DAYS}d old, versions match; skipping"
fi
fi

echo "build=${BUILD}" >> "$GITHUB_OUTPUT"
echo "reason=${REASON}" >> "$GITHUB_OUTPUT"
echo "::notice::Decision: build=${BUILD} (${REASON})"

- name: Prepare build metadata
if: steps.decide.outputs.build == 'true'
id: meta
run: |
VERSIONS=$(jq -c '.' upstream-versions.json)
DATE=$(date -u +%Y%m%d)
echo "versions=${VERSIONS}" >> "$GITHUB_OUTPUT"
echo "date=${DATE}" >> "$GITHUB_OUTPUT"

- name: Set up Docker Buildx
if: steps.decide.outputs.build == 'true'
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4

- name: Build and push artifact
if: steps.decide.outputs.build == 'true'
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with:
context: ${{ inputs.builder_dir }}
file: ${{ inputs.builder_dir }}/Containerfile
target: artifact
push: true
tags: |
${{ inputs.image }}:latest
${{ inputs.image }}:${{ steps.meta.outputs.date }}
labels: |
dev.distinctionos.versions=${{ steps.meta.outputs.versions }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Report skip
if: steps.decide.outputs.build == 'false'
run: echo "::notice::No rebuild needed — ${{ steps.decide.outputs.reason }}"
Loading
Loading