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
10 changes: 10 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,13 @@ verify-stateful-drivers:

verify-cups-patch-chain:
tests/cups-patch-chain.sh

verify:
just validate
just verify-cups-patch-chain
just verify-core
just verify-payload
just verify-raster-drivers
just verify-packaged-drivers
just verify-stateful-drivers
tests/appliance-parity.sh
63 changes: 63 additions & 0 deletions docs/oci-physical-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# OCI physical printer validation

`just verify` proves the automated appliance contract with synthetic jobs. It does not prove USB enumeration, network discovery, printer firmware behavior, ink or toner output, media handling, or device-specific color. Record those results only after testing real hardware.

## Automated baseline

Run:

```bash
just verify
```

The x86_64 image has a 500 MiB (524,288,000-byte) uncompressed ceiling. The implementation baseline measured 449,525,807 bytes on 2026-09-16. `tests/appliance-parity.sh` measures the local image through Podman and fails above that ceiling. The gate also checks OCI metadata, payload inventory, absence of development content, interpreter policy, and every ELF dependency.

The OCI driver contract is the complete list under [Contained Printer Drivers](../README.md#contained-printer-drivers-in-the-snap), not a sample. Despite the historical heading, the FSDK image must retain every listed Ghostscript/Foomatic driver and every named external driver family. The parity gate requires each legacy Ghostscript name as either a compiled Ghostscript device or an exact Foomatic PPD entry, then separately checks all external driver, filter, backend, and PPD-provider families.

## USB printer

1. Build the exact revision under test with `just build`.
2. Connect and power on the printer. Confirm the host sees it before starting the container.
3. Start the appliance with host networking, persistent state, and USB access:

```bash
mkdir -p .state/physical-usb
podman unshare chown -R 65532:65532 .state/physical-usb
podman run --rm --name ghostscript-printer-app-usb \
--network host \
--device /dev/bus/usb \
--group-add keep-groups \
-e PORT=18080 \
-v "$PWD/.state/physical-usb:/var/lib/ghostscript-printer-app:Z" \
ghcr.io/projectbluefin/ghostscript-printer-app:build
```

4. Open `http://127.0.0.1:18080`, add the discovered USB device, and select its intended driver rather than a generic substitute.
5. Print the built-in test page. Confirm that the job completes, paper and resolution match the selected options, graphics and text are complete, and the device reports no protocol or filter error.
6. Restart the same command and confirm that the printer and any edited profile or configuration remain present.

If rootless device access is denied, fix host udev/group permissions. Do not validate with a privileged container because that hides the shipping access model.

## Network printer

1. Build the exact revision under test with `just build`.
2. Start the appliance without USB access:

```bash
mkdir -p .state/physical-network
podman unshare chown -R 65532:65532 .state/physical-network
podman run --rm --name ghostscript-printer-app-network \
--network host \
-e PORT=18081 \
-v "$PWD/.state/physical-network:/var/lib/ghostscript-printer-app:Z" \
ghcr.io/projectbluefin/ghostscript-printer-app:build
```

3. Open `http://127.0.0.1:18081`. Confirm DNS-SD discovery when the printer advertises itself; otherwise add its `socket://`, `ipp://`, or `ipps://` address manually.
4. Select the intended driver and print the built-in test page.
5. Confirm completed job state, physical output, selected media/resolution, and absence of backend or filter errors.
6. Restart with the same state directory and repeat one print to prove persisted configuration.

## Result record

Record the image digest, commit, date, printer make/model, connection type, device URI, selected driver, tested options, restart result, and observed output. Mark USB and network separately. A synthetic CI pass must never be recorded as physical validation.
31 changes: 31 additions & 0 deletions docs/superpowers/plans/2026-09-16-fsdk-appliance-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# FSDK Appliance Parity Implementation Plan

**Goal:** Provide one local command that proves the complete FSDK OCI appliance contract against the real built image, while keeping physical printer validation explicit and honest.

**Architecture:** Keep the existing slice-specific smoke tests as the owners of conversion and lifecycle behavior. Add one final image-audit script for cross-cutting invariants: OCI metadata, uncompressed size, runtime bloat, interpreter/toolchain policy, complete ELF closure, and the advertised backend/filter/driver/PPD-family inventory. Expose an aggregate `just verify` recipe that runs graph validation, every slice gate, and the final audit. Strip the residual FSDK debug payload during OCI layer assembly rather than adding another compose layer.

## Task 1: Enforce final image invariants

- [x] Remove `/usr/lib/debug` from the assembled OCI layer; keep licenses and runtime data.
- [x] Add `tests/appliance-parity.sh` with a 500 MiB uncompressed x86_64 image ceiling and exact OCI user, entrypoint, architecture, source, license, title, application-version, and FSDK-version checks.
- [x] Reject package managers, compilers, build systems, headers, debug files, non-license test trees, and static archives in the final image.
- [x] Require Bash and Python as the only application interpreters, and reject Perl, Ruby, Node, Lua, and Tcl runtimes.
- [x] Walk every ELF file in the running image and fail if any shared-library dependency is unresolved.

## Task 2: Enforce advertised payload parity

- [x] Require the complete CUPS backend set used by the appliance: DNS-SD, IPP/IPPS, LPD, SNMP, socket, and USB.
- [x] Require every README-advertised Ghostscript/Foomatic driver plus executables and CUPS filters for HPIJS, pnm2ppa, pxljr, foo2zjs, SpliX, brlaser, fxlinuxprint, c2esp, rastertosag-gdi, Dymo, P-Touch, c2050, cjet, min12xxw, m2300w, CUPS, and cups-filters.
- [x] Require every PPD provider family: core cups-filters/Foomatic/manufacturer data plus pxljr, foo2zjs, SpliX, brlaser, fxlinuxprint, c2esp, rastertosag-gdi, Dymo, P-Touch, OKI, and m2300w.
- [x] Add `just verify` as the single aggregate command running graph validation, the CUPS patch-chain proof, all real-image behavior gates, and the final parity audit.

## Task 3: Document physical validation

- [x] Add `docs/oci-physical-validation.md` with separate USB and network-printer procedures, the automated size ceiling, expected discovery/printing observations, and a result-record template.
- [x] State explicitly that CI and local synthetic gates do not prove physical printer behavior.

## Task 4: Verify and publish

- [x] Run `just verify`, workflow lint, shell syntax checks, and `git diff --check`.
- [x] Review the full diff from `feat/fsdk-stateful-drivers` and resolve all blocking findings.
- [x] Resolve issue 07, commit and push `feat/fsdk-appliance-parity`, and open a stacked PR based on `feat/fsdk-stateful-drivers`.
2 changes: 2 additions & 0 deletions elements/oci/ghostscript-printer-app.bst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ config:
'nonroot:x:65532:' \
>> /layer/etc/group
rm -rf /layer/run/dbus /layer/run/avahi-daemon /layer/run/ghostscript-printer-app
rm -rf /layer/usr/lib/debug
rm -f /layer/usr/bin/cupsd /layer/usr/bin/ippeveprinter
chmod 0777 /layer/run
install -d -m 1777 /layer/tmp
install -d -m 0777 \
Expand Down
212 changes: 212 additions & 0 deletions tests/appliance-parity.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#!/usr/bin/env bash
set -euo pipefail

podman_binary="$(command -v podman)"
if ! "$podman_binary" info >/dev/null 2>&1; then
podman() { sudo "$podman_binary" "$@"; }
fi

image="ghcr.io/projectbluefin/ghostscript-printer-app:build"
size_limit_bytes="${IMAGE_SIZE_LIMIT_BYTES:-524288000}"

just build

advertised_ghostscript_drivers="$(python3 - <<'PY'
import pathlib
import re

readme = pathlib.Path("README.md").read_text()
match = re.search(
r"### Contained Printer Drivers.*?- \*\*Ghostscript built-in\*\*:\s*```(.*?)```",
readme,
re.DOTALL,
)
if match is None:
raise SystemExit("FAIL: README Ghostscript driver inventory is missing")
print(" ".join(match.group(1).replace(",", " ").split()))
PY
)"

fsdk_version="$(python3 - <<'PY'
import pathlib
import re

junction = pathlib.Path("elements/freedesktop-sdk.bst").read_text()
match = re.search(r"ref: freedesktop-sdk-(.+?)-0-g[0-9a-f]{40}$", junction, re.MULTILINE)
if match is None:
raise SystemExit("FAIL: pinned freedesktop-sdk release is missing")
print(match.group(1))
PY
)"

size_bytes="$(podman image inspect "$image" --format '{{.Size}}')"
if ((size_bytes > size_limit_bytes)); then
printf 'FAIL: uncompressed image is %s bytes; limit is %s bytes\n' "$size_bytes" "$size_limit_bytes" >&2
exit 1
fi

case "$(uname -m)" in
x86_64) expected_arch=amd64 ;;
aarch64) expected_arch=arm64 ;;
*) printf 'FAIL: unsupported verification architecture %s\n' "$(uname -m)" >&2; exit 1 ;;
esac

test "$(podman image inspect "$image" --format '{{.Architecture}}')" = "$expected_arch"
test "$(podman image inspect "$image" --format '{{.Config.User}}')" = 65532:65532
test "$(podman image inspect "$image" --format '{{json .Config.Entrypoint}}')" = '["/usr/bin/catatonit","--","/usr/bin/bash","/usr/libexec/ghostscript-printer-app/container-entrypoint"]'
test "$(podman image inspect "$image" --format '{{index .Config.Labels "org.opencontainers.image.title"}}')" = ghostscript-printer-app
test "$(podman image inspect "$image" --format '{{index .Config.Labels "org.opencontainers.image.source"}}')" = https://github.com/projectbluefin/ghostscript-printer-app
test "$(podman image inspect "$image" --format '{{index .Config.Labels "org.opencontainers.image.licenses"}}')" = Apache-2.0
application_version="$(podman run --rm --entrypoint /usr/bin/ghostscript-printer-app "$image" --version)"
test "$(podman image inspect "$image" --format '{{index .Config.Labels "org.opencontainers.image.version"}}')" = "$application_version"
test "$(podman image inspect "$image" --format '{{index .Config.Labels "io.projectbluefin.fsdk.version"}}')" = "$fsdk_version"

podman run --rm --user 0:0 --entrypoint /usr/bin/bash \
-e ADVERTISED_GHOSTSCRIPT_DRIVERS="$advertised_ghostscript_drivers" \
"$image" -c '
set -euo pipefail

backends=(dnssd ipp ipps lpd snmp socket usb)
for backend in "${backends[@]}"; do
test -x "/usr/lib/cups/backend/$backend"
done

filters=(
c2esp c2espC command2esp command2foo2lava-pjl
foomatic-rip gstoraster pdftopdffx pdftops
pstoqpdl raster2dymolm raster2dymolw rastertobrlaser
rastertoepson rastertoescpx rastertohp rastertolabel
rastertookidotmatrix rastertookimonochrome rastertopclx
rastertoptch rastertoqpdl rastertosag-gdi
)
for filter in "${filters[@]}"; do
test -x "/usr/lib/cups/filter/$filter"
done

commands=(
c2050 cjet foo2zjs-wrapper gs hpijs m2300w-wrapper
min12xxw pnm2ppa psnup ijs_pxljr
)
for command in "${commands[@]}"; do
command -v "$command" >/dev/null
done

ppd_providers=(
KodakESP_16.drv KodakESP_C_07.drv brlaser.drv
cups-filters-ppds dymo-ppds foo2zjs-ppds foomatic-ppds
fxlinuxprint-ppds m2300w-ppds manufacturer-ppds oki-ppds
ptouch-ppds pxljr-ppds rastertosag-gdi-ppds splix-ppds
)
for provider in "${ppd_providers[@]}"; do
test -e "/usr/share/ppd/$provider"
done

provider_contains() {
local path="$1" marker="${2,,}" contents
if [[ -x "$path" ]]; then
contents="$("$path" list)"
else
contents="$(cat "$path")"
fi
if [[ "${contents,,}" != *"$marker"* ]]; then
printf "FAIL: %s does not contain advertised family %s\n" "$path" "$2" >&2
exit 1
fi
}
provider_contains /usr/share/ppd/KodakESP_16.drv Kodak
provider_contains /usr/share/ppd/KodakESP_C_07.drv Kodak
provider_contains /usr/share/ppd/brlaser.drv Brother
provider_contains /usr/share/ppd/cups-filters-ppds "PCL 6 CUPS"
provider_contains /usr/share/ppd/dymo-ppds Dymo
provider_contains /usr/share/ppd/foo2zjs-ppds Minolta
provider_contains /usr/share/ppd/foomatic-ppds Foomatic
provider_contains /usr/share/ppd/fxlinuxprint-ppds "Fuji Xerox"
provider_contains /usr/share/ppd/m2300w-ppds "KONICA MINOLTA"
for manufacturer in Gestetner InfoPrint Infotec Lanier NRG Ricoh Savin Samsung; do
provider_contains /usr/share/ppd/manufacturer-ppds "$manufacturer"
done
provider_contains /usr/share/ppd/oki-ppds Oki
provider_contains /usr/share/ppd/ptouch-ppds Brother
provider_contains /usr/share/ppd/pxljr-ppds "HP Color LaserJet"
provider_contains /usr/share/ppd/rastertosag-gdi-ppds Ricoh
provider_contains /usr/share/ppd/splix-ppds Samsung
provider_contains /usr/share/cups/drv/sample.drv Intellitech
provider_contains /usr/share/cups/drv/sample.drv Zebra

devices=" $(gs -h 2>&1 | tr "\n" " ") "
foomatic_entries="$(/usr/share/ppd/foomatic-ppds list)"
read -r -a ghostscript_drivers <<< "$ADVERTISED_GHOSTSCRIPT_DRIVERS"
((${#ghostscript_drivers[@]} > 0))
for driver in "${ghostscript_drivers[@]}"; do
if [[ "$devices" != *" $driver "* && "$foomatic_entries" != *"-$driver.ppd\""* ]]; then
printf "FAIL: advertised Ghostscript driver %s has no device or PPD entry\n" "$driver" >&2
exit 1
fi
done

command -v bash >/dev/null
command -v python3 >/dev/null
for interpreter in perl ruby node lua tclsh wish; do
! command -v "$interpreter" >/dev/null 2>&1
done
for tool in apt apt-get apk dnf dpkg pacman rpm pip pip3 cc c++ gcc g++ clang make cmake meson ninja pkg-config autoconf automake libtool ld ar as nm objcopy ranlib strip; do
! command -v "$tool" >/dev/null 2>&1
done

python3 - <<"PY"
import os
import subprocess
import sys

forbidden = []
unresolved = []
for root, dirs, files in os.walk("/"):
if root == "/":
dirs[:] = [name for name in dirs if name not in {"dev", "proc", "run", "sys", "tmp"}]
if root.startswith("/usr/share/licenses/"):
dirs[:] = []
continue
if root == "/usr/include" or root.startswith("/usr/include/"):
forbidden.extend(os.path.join(root, name) for name in files)
if root == "/usr/lib/debug" or root.startswith("/usr/lib/debug/"):
forbidden.extend(os.path.join(root, name) for name in files)
for directory in dirs:
if directory.lower() in {"test", "tests", "testing"}:
forbidden.append(os.path.join(root, directory))
for name in files:
path = os.path.join(root, name)
if name.endswith((".a", ".la")):
forbidden.append(path)
if os.path.islink(path):
continue
try:
with open(path, "rb") as stream:
is_elf = stream.read(4) == b"\x7fELF"
except OSError as error:
unresolved.append(f"{path}: audit failed: {error}")
continue
if not is_elf:
continue
result = subprocess.run(
["/usr/bin/ldd", path],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
check=False,
)
output = result.stdout.strip()
if "not found" in output:
unresolved.append(f"{path}: {output}")
elif result.returncode != 0 and "not a dynamic executable" not in output and "statically linked" not in output:
unresolved.append(f"{path}: ldd exited {result.returncode}: {output}")

if forbidden:
print("FAIL: forbidden runtime payload:\n" + "\n".join(forbidden), file=sys.stderr)
if unresolved:
print("FAIL: unresolved ELF dependencies:\n" + "\n".join(unresolved), file=sys.stderr)
if forbidden or unresolved:
raise SystemExit(1)
PY
'

printf 'OK: complete appliance inventory, metadata, size, and runtime closure (%s bytes)\n' "$size_bytes"
5 changes: 5 additions & 0 deletions tests/core-appliance.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail

podman_binary="$(command -v podman)"
if ! "$podman_binary" info >/dev/null 2>&1; then
podman() { sudo "$podman_binary" "$@"; }
fi

image="ghcr.io/projectbluefin/ghostscript-printer-app:build"
name="ghostscript-printer-app-smoke"
failure_name="ghostscript-printer-app-child-failure"
Expand Down
5 changes: 5 additions & 0 deletions tests/core-payload.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail

podman_binary="$(command -v podman)"
if ! "$podman_binary" info >/dev/null 2>&1; then
podman() { sudo "$podman_binary" "$@"; }
fi

image="ghcr.io/projectbluefin/ghostscript-printer-app:build"
name="ghostscript-printer-app-payload"
port="${PORT:-18010}"
Expand Down
5 changes: 5 additions & 0 deletions tests/packaged-drivers.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail

podman_binary="$(command -v podman)"
if ! "$podman_binary" info >/dev/null 2>&1; then
podman() { sudo "$podman_binary" "$@"; }
fi

image="ghcr.io/projectbluefin/ghostscript-printer-app:build"
name="ghostscript-printer-app-packaged-drivers"
port="${PORT:-18030}"
Expand Down
5 changes: 5 additions & 0 deletions tests/standalone-raster-drivers.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail

podman_binary="$(command -v podman)"
if ! "$podman_binary" info >/dev/null 2>&1; then
podman() { sudo "$podman_binary" "$@"; }
fi

image="ghcr.io/projectbluefin/ghostscript-printer-app:build"
name="ghostscript-printer-app-raster-drivers"
port="${PORT:-18020}"
Expand Down
5 changes: 5 additions & 0 deletions tests/stateful-drivers.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail

podman_binary="$(command -v podman)"
if ! "$podman_binary" info >/dev/null 2>&1; then
podman() { sudo "$podman_binary" "$@"; }
fi

image="ghcr.io/projectbluefin/ghostscript-printer-app:build"
name="ghostscript-printer-app-stateful-drivers"
port="${PORT:-18040}"
Expand Down