From fb90a2f6792246d910282c2bb13c86c9166662f5 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 00:49:37 -0400 Subject: [PATCH 01/22] build: add core FSDK printer application graph Assisted-by: github-copilot/gpt-5.6-sol via pi --- .gitignore | 4 +- Justfile | 47 +++++++++++++++++++++- elements/printer-app/application.bst | 28 +++++++++++++ elements/printer-app/pappl-retrofit.bst | 26 +++++++++++++ elements/printer-app/pappl.bst | 41 +++++++++++++++++++ patches/pappl/printer-application.patch | 52 +++++++++++++++++++++++++ project.conf | 15 +++++++ 7 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 elements/printer-app/application.bst create mode 100644 elements/printer-app/pappl-retrofit.bst create mode 100644 elements/printer-app/pappl.bst create mode 100644 patches/pappl/printer-application.patch diff --git a/.gitignore b/.gitignore index 18e80be..d8fb923 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ .scratch/ .worktrees/ .bst/ -.bst2/ \ No newline at end of file +.bst2/ +.bst-re.conf +.build-out/ \ No newline at end of file diff --git a/Justfile b/Justfile index 354a633..2fb01e1 100644 --- a/Justfile +++ b/Justfile @@ -1,6 +1,7 @@ # BuildStream runs in the pinned freedesktop-sdk builder image. bst2_image := env("BST2_IMAGE", "registry.gitlab.com/freedesktop-sdk/infrastructure/freedesktop-sdk-docker-images/bst2:64eb0b4930d57a92710822898fb73af6cc1ae35d") sudo_cmd := if `podman info >/dev/null 2>&1 && echo 1 || echo 0` == "1" { "" } else { "sudo" } +image_ref := "ghcr.io/projectbluefin/ghostscript-printer-app:build" default: @just --list @@ -9,6 +10,29 @@ bst *ARGS: #!/usr/bin/env bash set -euo pipefail mkdir -p "${HOME}/.cache/buildstream" + RE_FLAG=() + PF_PID="" + cleanup() { [[ -n "$PF_PID" ]] && kill "$PF_PID" 2>/dev/null || true; } + trap cleanup EXIT + if [[ "${BST_REMOTE:-0}" == "1" ]]; then + export KUBECONFIG="${KUBECONFIG:-$HOME/.kube/bluespeed.yaml}" + kubectl port-forward -n buildbarn svc/frontend 18980:8980 >/dev/null 2>&1 & + PF_PID=$! + for _ in $(seq 1 20); do + (echo > /dev/tcp/127.0.0.1/18980) 2>/dev/null && break + sleep 0.5 + done + cat > .bst-re.conf <<'EOF' + remote-execution: + execution-service: + url: grpc://127.0.0.1:18980 + storage-service: + url: grpc://127.0.0.1:18980 + action-cache-service: + url: grpc://127.0.0.1:18980 + EOF + RE_FLAG=(--config /src/.bst-re.conf) + fi {{ sudo_cmd }} podman run --rm \ --privileged \ --device /dev/fuse \ @@ -17,7 +41,28 @@ bst *ARGS: -v "${HOME}/.cache/buildstream:/root/.cache/buildstream:rw" \ -w /src \ "{{ bst2_image }}" \ - bash -c 'bst "$@"' -- --no-interactive {{ ARGS }} + bash -c 'bst "$@"' -- --no-interactive "${RE_FLAG[@]}" {{ ARGS }} + +validate: + just bst show --deps all oci/ghostscript-printer-app.bst + +build: + #!/usr/bin/env bash + set -euo pipefail + just bst build oci/ghostscript-printer-app.bst + just export + +export: + #!/usr/bin/env bash + set -euo pipefail + rm -rf .build-out + just bst artifact checkout oci/ghostscript-printer-app.bst --directory /src/.build-out + IMAGE_ID=$({{ sudo_cmd }} podman pull -q oci:.build-out) + rm -rf .build-out + {{ sudo_cmd }} podman tag "$IMAGE_ID" "{{ image_ref }}" + +verify-core: + tests/core-appliance.sh verify-cups-patch-chain: tests/cups-patch-chain.sh diff --git a/elements/printer-app/application.bst b/elements/printer-app/application.bst new file mode 100644 index 0000000..e4096e3 --- /dev/null +++ b/elements/printer-app/application.bst @@ -0,0 +1,28 @@ +kind: manual +description: Build the Ghostscript Printer Application executable. + +sources: + - kind: local + path: Makefile + - kind: local + path: ghostscript-printer-app.c + - kind: local + path: ghostscript-printer-app.1 + - kind: local + path: ghostscript-printer-app.service + - kind: local + path: testpage.ps + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-autotools.bst + +depends: + - printer-app/pappl-retrofit.bst + - freedesktop-sdk.bst:components/ghostscript.bst + +config: + build-commands: + - make clean + - make -j2 VERSION=10.07.1-1 LDFLAGS="$LDFLAGS -ljpeg" + install-commands: + - make DESTDIR="%{install-root}" VERSION=10.07.1-1 LDFLAGS="$LDFLAGS -ljpeg" unitdir= install diff --git a/elements/printer-app/pappl-retrofit.bst b/elements/printer-app/pappl-retrofit.bst new file mode 100644 index 0000000..4a9ce30 --- /dev/null +++ b/elements/printer-app/pappl-retrofit.bst @@ -0,0 +1,26 @@ +kind: manual +description: Build pappl-retrofit against the FSDK printing stack. + +sources: + - kind: git_repo + url: github:OpenPrinting/pappl-retrofit.git + track: master + ref: 1626b338fc8b92a99c2c1a483ac39c739d71b305 + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-autotools.bst + +depends: + - printer-app/pappl.bst + - freedesktop-sdk.bst:components/cups.bst + - freedesktop-sdk.bst:components/libcupsfilters.bst + - freedesktop-sdk.bst:components/libppd.bst + +config: + configure-commands: + - ./autogen.sh + - ./configure --prefix=/usr + build-commands: + - make -j2 + install-commands: + - make DESTDIR="%{install-root}" install diff --git a/elements/printer-app/pappl.bst b/elements/printer-app/pappl.bst new file mode 100644 index 0000000..3eb9018 --- /dev/null +++ b/elements/printer-app/pappl.bst @@ -0,0 +1,41 @@ +kind: manual +description: Build PAPPL for the Ghostscript Printer Application. + +sources: + - kind: git_repo + url: github:michaelrsweet/pappl.git + track: v1.4.12 + ref: v1.4.12-0-g6db8e137557ad84662e78d24fdb2a591c621f4ac + - kind: patch_queue + path: patches/pappl + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-autotools.bst + +depends: + - freedesktop-sdk.bst:components/avahi-libs.bst + - freedesktop-sdk.bst:components/cups.bst + - freedesktop-sdk.bst:components/gnutls.bst + - freedesktop-sdk.bst:components/jpeg.bst + - freedesktop-sdk.bst:components/libpng.bst + - freedesktop-sdk.bst:components/libusb.bst + +config: + configure-commands: + - | + ./configure \ + --prefix=/usr \ + --disable-libpam \ + --disable-static \ + --enable-shared \ + --enable-libjpeg \ + --enable-libpng \ + --enable-libusb \ + --with-dnssd=avahi \ + --with-tls=gnutls \ + --with-papplstatedir=/var/lib/ghostscript-printer-app \ + --with-papplsockdir=/run/ghostscript-printer-app + build-commands: + - make -j2 + install-commands: + - make DESTDIR="%{install-root}" install diff --git a/patches/pappl/printer-application.patch b/patches/pappl/printer-application.patch new file mode 100644 index 0000000..122dbe3 --- /dev/null +++ b/patches/pappl/printer-application.patch @@ -0,0 +1,52 @@ +diff --git a/Makedefs.in b/Makedefs.in +index 3470975..9f12b20 100644 +--- a/Makedefs.in ++++ b/Makedefs.in +@@ -26,7 +26,7 @@ CODE_SIGN = @CODE_SIGN@ + CODESIGN_IDENTITY = - + CPPFLAGS = -I.. @CPPFLAGS@ + CSFLAGS = -s "$(CODESIGN_IDENTITY)" @CSFLAGS@ --timestamp +-DSOFLAGS = @DSOFLAGS@ $(CFLAGS) ++DSOFLAGS = @DSOFLAGS@ $(LDFLAGS) $(CFLAGS) + INSTALL = @INSTALL@ + LDFLAGS = @LDFLAGS@ $(OPTIM) + LIBS = @LIBS@ +diff --git a/pappl/printer.h b/pappl/printer.h +index c261f5f..860cae0 100644 +--- a/pappl/printer.h ++++ b/pappl/printer.h +@@ -27,7 +27,7 @@ extern "C" { + # define PAPPL_MAX_SOURCE 16 // Maximum number of sources/rolls + # define PAPPL_MAX_SUPPLY 32 // Maximum number of supplies + # define PAPPL_MAX_TYPE 32 // Maximum number of media types +-# define PAPPL_MAX_VENDOR 32 // Maximum number of vendor extension attributes ++# define PAPPL_MAX_VENDOR 256 // Maximum number of vendor extension attributes + + + // +diff --git a/pappl/system-webif.c b/pappl/system-webif.c +index 4fc176f..9a7f49f 100644 +--- a/pappl/system-webif.c ++++ b/pappl/system-webif.c +@@ -451,7 +451,7 @@ _papplSystemWebAddPrinter( + else + { + httpAddrFreeList(list); +- httpAssembleURI(HTTP_URI_CODING_ALL, device_uri, sizeof(device_uri), "socket", NULL, hostname, port, "/"); ++ httpAssembleURI(HTTP_URI_CODING_ALL, device_uri, sizeof(device_uri), "cups:socket", NULL, hostname, port, "/"); + } + } + } +diff --git a/pappl/system.c b/pappl/system.c +index 7bb63b6..af5702a 100644 +--- a/pappl/system.c ++++ b/pappl/system.c +@@ -192,7 +192,7 @@ papplSystemCreate( + system->log_fd = -1; + system->log_file = logfile ? strdup(logfile) : NULL; + system->log_level = loglevel; +- system->log_max_size = 1024 * 1024; ++ system->log_max_size = 0; + system->next_client = 1; + system->next_printer_id = 1; + system->subtypes = subtypes ? strdup(subtypes) : NULL; diff --git a/project.conf b/project.conf index 97d7e90..ff52f30 100644 --- a/project.conf +++ b/project.conf @@ -14,9 +14,24 @@ options: - aarch64 - x86_64 +variables: + (?): + - arch == "x86_64": + go-arch: "amd64" + - arch == "aarch64": + go-arch: "arm64" + sandbox: build-arch: "%{arch}" +artifacts: + - url: https://gbm.gnome.org:11003 + - url: https://cache.projectbluefin.io:11001 + +source-caches: + - url: https://gbm.gnome.org:11003 + - url: https://cache.projectbluefin.io:11001 + plugins: - origin: junction junction: plugins/buildstream-plugins-community.bst From af2cc501b1c31926c9d25272f20d1cfb13fe4f34 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 01:43:58 -0400 Subject: [PATCH 02/22] fix: install local libraries in FSDK multiarch path Assisted-by: github-copilot/gpt-5.6-sol via pi --- elements/printer-app/pappl-retrofit.bst | 2 +- elements/printer-app/pappl.bst | 1 + project.conf | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/elements/printer-app/pappl-retrofit.bst b/elements/printer-app/pappl-retrofit.bst index 4a9ce30..43bdc97 100644 --- a/elements/printer-app/pappl-retrofit.bst +++ b/elements/printer-app/pappl-retrofit.bst @@ -19,7 +19,7 @@ depends: config: configure-commands: - ./autogen.sh - - ./configure --prefix=/usr + - ./configure --prefix=/usr --libdir="%{libdir}" build-commands: - make -j2 install-commands: diff --git a/elements/printer-app/pappl.bst b/elements/printer-app/pappl.bst index 3eb9018..a24d137 100644 --- a/elements/printer-app/pappl.bst +++ b/elements/printer-app/pappl.bst @@ -25,6 +25,7 @@ config: - | ./configure \ --prefix=/usr \ + --libdir="%{libdir}" \ --disable-libpam \ --disable-static \ --enable-shared \ diff --git a/project.conf b/project.conf index ff52f30..de68ad2 100644 --- a/project.conf +++ b/project.conf @@ -15,6 +15,10 @@ options: - x86_64 variables: + abi: gnu + gcc-triplet: "%{arch}-linux-%{abi}" + lib: "lib/%{gcc-triplet}" + libdir: "%{prefix}/%{lib}" (?): - arch == "x86_64": go-arch: "amd64" From 5fded2d032534c58b5fcac5877d87a4283d47a54 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 01:45:22 -0400 Subject: [PATCH 03/22] build: compose core FSDK OCI appliance Assisted-by: github-copilot/gpt-5.6-sol via pi --- elements/oci/ghostscript-printer-app.bst | 45 +++++++++++++++++ elements/printer-app/core-runtime.bst | 15 ++++++ elements/printer-app/core-stack.bst | 16 ++++++ elements/printer-app/runtime-files.bst | 15 ++++++ files/container-entrypoint.sh | 62 ++++++++++++++++++++++++ 5 files changed, 153 insertions(+) create mode 100644 elements/oci/ghostscript-printer-app.bst create mode 100644 elements/printer-app/core-runtime.bst create mode 100644 elements/printer-app/core-stack.bst create mode 100644 elements/printer-app/runtime-files.bst create mode 100755 files/container-entrypoint.sh diff --git a/elements/oci/ghostscript-printer-app.bst b/elements/oci/ghostscript-printer-app.bst new file mode 100644 index 0000000..22dc944 --- /dev/null +++ b/elements/oci/ghostscript-printer-app.bst @@ -0,0 +1,45 @@ +kind: script + +build-depends: + - freedesktop-sdk.bst:bootstrap/bash.bst + - freedesktop-sdk.bst:bootstrap/coreutils.bst + - freedesktop-sdk.bst:components/oci-builder.bst + - freedesktop-sdk.bst:components/sed.bst + - filename: printer-app/core-runtime.bst + config: + location: /layer + +config: + commands: + - | + install -d /layer/etc + printf '%s\n' 'nonroot:x:65532:65532:Printer Application:/var/lib/ghostscript-printer-app:/usr/sbin/nologin' >> /layer/etc/passwd + printf '%s\n' 'nonroot:x:65532:' >> /layer/etc/group + if [ -f /layer/usr/share/dbus-1/system.conf ]; then + sed -i 's/messagebus<\/user>/nonroot<\/user>/' /layer/usr/share/dbus-1/system.conf + fi + if [ -f /layer/usr/share/dbus-1/system.d/avahi-dbus.conf ]; then + sed -i 's///g; s///g' /layer/usr/share/dbus-1/system.d/avahi-dbus.conf + fi + cd "%{install-root}" + build-oci <&2 + exit 64 +fi + +state_dir=/var/lib/ghostscript-printer-app +mkdir -p "$state_dir/ppd" "$state_dir/spool" "$state_dir/usb" "$state_dir/cups/ssl" /run/dbus /run/avahi-daemon /run/ghostscript-printer-app + +export BACKEND_DIR=/usr/lib/ghostscript-printer-app/backend +export CUPS_SERVERBIN=/usr/lib/ghostscript-printer-app +export CUPS_SERVERROOT="$state_dir/cups" +export FILTER_DIR=/usr/lib/ghostscript-printer-app/filter +export PPDC_DATADIR=/usr/share/ppdc +export PPD_PATHS="/usr/share/ppd/:$state_dir/ppd/" +export SPOOL_DIR="$state_dir/spool" +export STATE_DIR="$state_dir" +export STATE_FILE="$state_dir/ghostscript-printer-app.state" +export TESTPAGE_DIR=/usr/share/ghostscript-printer-app +export TMPDIR=/tmp +export USB_QUIRK_DIR="$state_dir" + +children=() +stop_children() { + local pid + for pid in "${children[@]}"; do + kill -TERM "$pid" 2>/dev/null || true + done + wait "${children[@]}" 2>/dev/null || true +} +trap stop_children TERM INT EXIT + +dbus-daemon --system --nofork --nopidfile & +children+=("$!") +for _ in $(seq 1 30); do + [[ -S /run/dbus/system_bus_socket ]] && break + sleep 0.1 +done +[[ -S /run/dbus/system_bus_socket ]] + +avahi-daemon --no-drop-root --no-chroot & +children+=("$!") +for _ in $(seq 1 30); do + [[ -f /run/avahi-daemon/pid ]] && break + sleep 0.1 +done +[[ -f /run/avahi-daemon/pid ]] + +args=(-o "log-file=$state_dir/ghostscript-printer-app.log") +if [[ -n "${PORT:-}" ]]; then + args+=(-o "server-port=$PORT") +fi +ghostscript-printer-app "${args[@]}" server & +children+=("$!") + +wait -n "${children[@]}" +status=$? +stop_children +trap - TERM INT EXIT +exit "$status" From ccc0ffdc56b0a3d3d78b6013efe049fd30eddf90 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 02:02:52 -0400 Subject: [PATCH 04/22] test: verify core appliance lifecycle Assisted-by: github-copilot/gpt-5.6-sol via pi --- elements/oci/ghostscript-printer-app.bst | 24 ++++- elements/printer-app/runtime-files.bst | 1 - tests/core-appliance.sh | 113 +++++++++++++++++++++++ 3 files changed, 132 insertions(+), 6 deletions(-) create mode 100755 tests/core-appliance.sh diff --git a/elements/oci/ghostscript-printer-app.bst b/elements/oci/ghostscript-printer-app.bst index 22dc944..41f51c1 100644 --- a/elements/oci/ghostscript-printer-app.bst +++ b/elements/oci/ghostscript-printer-app.bst @@ -13,13 +13,27 @@ config: commands: - | install -d /layer/etc - printf '%s\n' 'nonroot:x:65532:65532:Printer Application:/var/lib/ghostscript-printer-app:/usr/sbin/nologin' >> /layer/etc/passwd - printf '%s\n' 'nonroot:x:65532:' >> /layer/etc/group + printf '%s\n' \ + 'root:x:0:0:root:/root:/usr/bin/bash' \ + 'avahi:x:65532:65532:Avahi:/run/avahi-daemon:/usr/sbin/nologin' \ + 'nonroot:x:65532:65532:Printer Application:/var/lib/ghostscript-printer-app:/usr/sbin/nologin' \ + >> /layer/etc/passwd + printf '%s\n' \ + 'root:x:0:' \ + 'avahi:x:65532:' \ + 'nonroot:x:65532:' \ + >> /layer/etc/group + rm -rf /layer/run/dbus /layer/run/avahi-daemon /layer/run/ghostscript-printer-app + chmod 0777 /layer/run + install -d -m 0777 \ + /layer/var/lib/ghostscript-printer-app \ + /layer/var/lib/ghostscript-printer-app/ppd \ + /layer/var/spool/ghostscript-printer-app if [ -f /layer/usr/share/dbus-1/system.conf ]; then - sed -i 's/messagebus<\/user>/nonroot<\/user>/' /layer/usr/share/dbus-1/system.conf + sed -i '/messagebus<\/user>/d; s///g' /layer/usr/share/dbus-1/system.conf fi - if [ -f /layer/usr/share/dbus-1/system.d/avahi-dbus.conf ]; then - sed -i 's///g; s///g' /layer/usr/share/dbus-1/system.d/avahi-dbus.conf + if [ -f /layer/etc/dbus-1/system.d/avahi-dbus.conf ]; then + sed -i 's///g; s///g; s///g' /layer/etc/dbus-1/system.d/avahi-dbus.conf fi cd "%{install-root}" build-oci </dev/null 2>&1 || true + podman unshare rm -rf "$state_dir" +} +trap cleanup EXIT + +wait_for_http() { + local target_port="$1" + for _ in $(seq 1 60); do + if curl --fail --silent --show-error "http://127.0.0.1:${target_port}/" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + return 1 +} + +just build +chmod 0777 "$state_dir" + +podman run -d \ + --name "$name" \ + --network host \ + --user 65532:65532 \ + -e PORT="$port" \ + -v "$state_dir:/var/lib/ghostscript-printer-app:Z" \ + "$image" >/dev/null + +wait_for_http "$port" +podman exec "$name" /usr/bin/bash -c ' + test "$(id -u):$(id -g)" = 65532:65532 + test "$(id -un)" = nonroot + passwd_ok=0 + while IFS=: read -r name password uid gid gecos home shell; do + [[ "$name:$uid:$gid" == "nonroot:65532:65532" ]] && passwd_ok=1 + done < /etc/passwd + group_ok=0 + while IFS=: read -r name password gid members; do + [[ "$name:$gid" == "nonroot:65532" ]] && group_ok=1 + done < /etc/group + (( passwd_ok && group_ok )) +' +test -d "$state_dir/ppd" +test -d "$state_dir/spool" +test -d "$state_dir/cups/ssl" +podman stop --time 15 "$name" >/dev/null +read -r running exit_status <<< "$(podman inspect "$name" --format '{{.State.Running}} {{.State.ExitCode}}')" +if [[ "$running" != false || "$exit_status" -ne 143 ]]; then + podman logs "$name" >&2 + printf 'FAIL: TERM shutdown ended in state %s with status %s, expected false 143\n' "$running" "$exit_status" >&2 + exit 1 +fi + +podman run -d \ + --name "$failure_name" \ + --network host \ + --user 65532:65532 \ + -e PORT="$failure_port" \ + -v "$state_dir:/var/lib/ghostscript-printer-app:Z" \ + "$image" >/dev/null + +wait_for_http "$failure_port" +podman exec "$failure_name" /usr/bin/bash -c ' + for proc in /proc/[0-9]*; do + read -r comm < "$proc/comm" || continue + if [[ "$comm" == dbus-daemon ]]; then + kill -KILL "${proc##*/}" + exit 0 + fi + done + exit 1 +' +for _ in $(seq 1 150); do + running="$(podman inspect "$failure_name" --format '{{.State.Running}}')" + [[ "$running" == false ]] && break + sleep 0.1 +done +read -r running failure_status <<< "$(podman inspect "$failure_name" --format '{{.State.Running}} {{.State.ExitCode}}')" +if [[ "$running" != false ]]; then + podman logs "$failure_name" >&2 + printf 'FAIL: container stayed running after a required child died\n' >&2 + exit 1 +fi +if [[ "$failure_status" -eq 0 ]]; then + printf 'FAIL: required child failure returned success\n' >&2 + exit 1 +fi + +set +e +podman run --name "$invalid_name" -e PORT=invalid "$image" >/dev/null 2>&1 +invalid_status=$? +set -e +if [[ "$invalid_status" -ne 64 ]]; then + printf 'FAIL: invalid PORT exited %s instead of 64\n' "$invalid_status" >&2 + exit 1 +fi +if ! podman logs "$invalid_name" 2>&1 | grep -q 'PORT must be numeric'; then + printf 'FAIL: invalid PORT diagnostic missing\n' >&2 + exit 1 +fi + +printf 'OK: core FSDK Printer Application passed lifecycle verification\n' From c4beedf5bf37bfdbf160a1cb794f3d458724f398 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 02:09:27 -0400 Subject: [PATCH 05/22] docs: record FSDK core appliance plan Assisted-by: github-copilot/gpt-5.6-sol via pi --- .../plans/2026-09-16-fsdk-core-appliance.md | 377 ++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md diff --git a/docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md b/docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md new file mode 100644 index 0000000..d5f0aa8 --- /dev/null +++ b/docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md @@ -0,0 +1,377 @@ +# FSDK Core Printer Application Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build and boot a local amd64 OCI appliance from freedesktop-sdk, PAPPL, pappl-retrofit, and the repository application while proving non-root lifecycle behavior. + +**Architecture:** BuildStream owns the complete build graph and consumes the already-proven patched freedesktop-sdk junction as the single CUPS owner. Focused elements build PAPPL, pappl-retrofit, and the C application; a compose element produces the runtime closure; an OCI element adds numeric identity metadata and runs catatonit plus one Bash lifecycle launcher. The real exported image is the verification boundary. + +**Tech Stack:** BuildStream 2, freedesktop-sdk 26.08rc.1, PAPPL 1.4.12, pappl-retrofit, OCI Builder, Podman, Bash, catatonit. + +## Global Constraints + +- Keep freedesktop-sdk's patched private CUPS base as the only `libcups.so*` owner. +- Do not change `ghostscript-printer-app.c` behavior or driver selection. +- Preserve the existing `Makefile` compile and install contract. +- Run the appliance as numeric identity `65532:65532` with matching passwd and group records. +- Keep `/var/lib/ghostscript-printer-app` as the single persistent state location. +- Use catatonit as PID 1 and one Bash launcher for D-Bus, Avahi, and the Printer Application. +- A required child exit must terminate the container; TERM and INT must stop all children. +- Keep Snap and Rockcraft behavior unchanged in this slice. +- Do not publish the incomplete core-only image. + +--- + +### Task 1: Stabilize BuildStream tooling and graph + +**Files:** +- Modify: `.gitignore` +- Modify: `Justfile` +- Modify: `project.conf` +- Create: `elements/printer-app/pappl.bst` +- Create: `elements/printer-app/pappl-retrofit.bst` +- Create: `elements/printer-app/application.bst` +- Create: `patches/pappl/printer-application.patch` + +**Interfaces:** +- Consumes: `elements/freedesktop-sdk.bst`, the repository `Makefile`, and the four PAPPL compatibility modifications defined by the accepted specification. +- Produces: `printer-app/application.bst`, an application artifact built against PAPPL, pappl-retrofit, Ghostscript, CUPS, libppd, and libcupsfilters; `just validate`, `just build`, and `just export` commands. + +- [x] **Step 1: Keep generated BuildStream configuration out of Git** + +Add these generated paths to `.gitignore`: + +```gitignore +.bst-re.conf +.build-out/ +``` + +- [x] **Step 2: Define architecture and cache configuration** + +Keep the existing `arch` option and add the exact OCI mapping and public cache configuration in `project.conf`: + +```yaml +variables: + abi: gnu + gcc-triplet: "%{arch}-linux-%{abi}" + lib: "lib/%{gcc-triplet}" + libdir: "%{prefix}/%{lib}" + (?): + - arch == "x86_64": + go-arch: "amd64" + - arch == "aarch64": + go-arch: "arm64" + +artifacts: + - url: https://gbm.gnome.org:11003 + - url: https://cache.projectbluefin.io:11001 + +source-caches: + - url: https://gbm.gnome.org:11003 + - url: https://cache.projectbluefin.io:11001 +``` + +- [x] **Step 3: Preserve the four PAPPL compatibility changes as one explicit patch** + +`patches/pappl/printer-application.patch` must change exactly these upstream behaviors: + +```diff +-DSOFLAGS = @DSOFLAGS@ $(CFLAGS) ++DSOFLAGS = @DSOFLAGS@ $(LDFLAGS) $(CFLAGS) +-PAPPL_MAX_VENDOR 32 ++PAPPL_MAX_VENDOR 256 +-"socket" ++"cups:socket" +-system->log_max_size = 1024 * 1024; ++system->log_max_size = 0; +``` + +The patch is a compatibility inventory, not a new fork: linked shared-library flags, the existing vendor-option ceiling, the retrofit CUPS URI scheme, and disabled log rotation. + +- [x] **Step 4: Build PAPPL and pappl-retrofit against public FSDK elements** + +`elements/printer-app/pappl.bst` must pin PAPPL `v1.4.12` at `6db8e137557ad84662e78d24fdb2a591c621f4ac`, apply `patches/pappl`, use GnuTLS and Avahi, and set: + +```sh +--libdir=%{libdir} +--with-papplstatedir=/var/lib/ghostscript-printer-app +--with-papplsockdir=/run/ghostscript-printer-app +``` + +`elements/printer-app/pappl-retrofit.bst` must pin commit `1626b338fc8b92a99c2c1a483ac39c739d71b305` and depend on only public `cups.bst`, `libcupsfilters.bst`, and `libppd.bst` elements plus local PAPPL. + +- [x] **Step 5: Build the repository application through its Makefile** + +`elements/printer-app/application.bst` must stage `Makefile`, `ghostscript-printer-app.c`, the man page, service file, and test page, then run: + +```sh +make clean +make -j2 VERSION=10.07.1-1 LDFLAGS="$LDFLAGS -ljpeg" +make DESTDIR="%{install-root}" VERSION=10.07.1-1 LDFLAGS="$LDFLAGS -ljpeg" unitdir= install +``` + +- [x] **Step 6: Verify graph and source patches** + +Run: + +```bash +just validate +just bst source checkout --force --directory /src/.bst/pappl-source printer-app/pappl.bst +``` + +Expected: `just validate` resolves `oci/ghostscript-printer-app.bst`; the checked-out PAPPL source contains vendor limit `256`, `cups:socket`, zero log rotation size, and `$(LDFLAGS)` in `DSOFLAGS`. + +- [x] **Step 7: Commit the graph slice** + +```bash +git add .gitignore Justfile project.conf elements/printer-app/application.bst elements/printer-app/pappl.bst elements/printer-app/pappl-retrofit.bst patches/pappl/printer-application.patch +git commit -m "build: add core FSDK printer application graph" +``` + +### Task 2: Compose and export the non-root OCI appliance + +**Files:** +- Create: `elements/printer-app/runtime-files.bst` +- Create: `elements/printer-app/core-stack.bst` +- Create: `elements/printer-app/core-runtime.bst` +- Create: `elements/oci/ghostscript-printer-app.bst` +- Create: `files/container-entrypoint.sh` +- Modify: `Justfile` + +**Interfaces:** +- Consumes: `printer-app/application.bst` and public FSDK runtime components. +- Produces: local image `ghcr.io/projectbluefin/ghostscript-printer-app:build` with user `65532:65532`, catatonit entrypoint, and writable runtime/state paths. + +- [x] **Step 1: Install only appliance-owned runtime files** + +`runtime-files.bst` installs the launcher at `/usr/libexec/ghostscript-printer-app/container-entrypoint` and creates the persistent state directories. The final OCI layer removes inherited root-owned daemon subdirectories, makes `/run` writable, and lets the launcher recreate these paths as UID `65532`: + +```text +/run/dbus +/run/avahi-daemon +/run/ghostscript-printer-app +/var/lib/ghostscript-printer-app +/var/spool/ghostscript-printer-app +``` + +- [x] **Step 2: Define the core runtime closure** + +`core-stack.bst` must contain the local application and runtime files plus these public FSDK runtime elements: + +```yaml +- public-stacks/runtime-gnu.bst +- components/avahi.bst +- components/ca-certificates.bst +- components/catatonit.bst +- components/cups-daemon-only.bst +- components/dbus.bst +- components/ghostscript.bst +- components/libcupsfilters.bst +- components/libppd.bst +- components/tzdata.bst +``` + +`core-runtime.bst` composes that stack while excluding `debug`, `devel`, `doc`, `locale`, `static-blocklist`, `tests`, and `vm-only` domains. + +- [x] **Step 3: Define OCI identity and process metadata** + +`elements/oci/ghostscript-printer-app.bst` must add matching passwd/group records for UID/GID `65532`, adjust FSDK D-Bus/Avahi policies to that identity, and emit this runtime configuration: + +```yaml +User: '65532:65532' +Entrypoint: ['/usr/bin/catatonit', '--', '/usr/bin/bash', '/usr/libexec/ghostscript-printer-app/container-entrypoint'] +Env: + - 'HOME=/var/lib/ghostscript-printer-app' + - 'PATH=/usr/bin:/usr/sbin' +``` + +Use OCI architecture `%{go-arch}` and keep the core-slice version labels `10.07.1-1` and `26.08rc.1`; canonical version generation belongs to issue 09. + +- [x] **Step 4: Export through one local command** + +`just build` must build the OCI element and call `just export`. `just export` must check out the OCI artifact, import it with Podman, remove the temporary checkout, and tag it as `ghcr.io/projectbluefin/ghostscript-printer-app:build`. + +- [x] **Step 5: Build and inspect the real image** + +Run: + +```bash +just build +podman image inspect ghcr.io/projectbluefin/ghostscript-printer-app:build --format '{{json .Config}}' +podman run --rm --entrypoint /usr/bin/bash ghcr.io/projectbluefin/ghostscript-printer-app:build -c ' + test "$(id -u):$(id -g)" = 65532:65532 + passwd_ok=0 + while IFS=: read -r name password uid gid gecos home shell; do + [[ "$name:$uid:$gid" == "nonroot:65532:65532" ]] && passwd_ok=1 + done < /etc/passwd + group_ok=0 + while IFS=: read -r name password gid members; do + [[ "$name:$gid" == "nonroot:65532" ]] && group_ok=1 + done < /etc/group + (( passwd_ok && group_ok )) +' +``` + +Expected: build and export succeed; image config reports the numeric user and catatonit entrypoint; passwd/group checks pass. + +- [x] **Step 6: Commit the OCI slice** + +```bash +git add Justfile elements/printer-app/runtime-files.bst elements/printer-app/core-stack.bst elements/printer-app/core-runtime.bst elements/oci/ghostscript-printer-app.bst files/container-entrypoint.sh +git commit -m "build: compose core FSDK OCI appliance" +``` + +### Task 3: Prove lifecycle behavior against the built image + +**Files:** +- Modify: `files/container-entrypoint.sh` +- Create: `tests/core-appliance.sh` +- Modify: `Justfile` + +**Interfaces:** +- Consumes: exported image `ghcr.io/projectbluefin/ghostscript-printer-app:build`. +- Produces: `just verify-core`, proving HTTP readiness, numeric non-root execution, persistent state initialization, clean TERM handling, and fail-fast behavior for required children. + +- [x] **Step 1: Implement deterministic launcher startup and shutdown** + +The launcher must: + +```bash +set -euo pipefail +``` + +Validate `PORT` with Bash's numeric regular expression, initialize state directories without overwriting existing files, export the repository runtime-path contract, start D-Bus and wait for `/run/dbus/system_bus_socket`, start Avahi and wait for `/run/avahi-daemon/pid`, then start `ghostscript-printer-app`. Track every child PID. On TERM, INT, EXIT, or the first required child exit, send TERM to all remaining children and wait for them. + +- [x] **Step 2: Extend the smoke check to assert the observable contract** + +`tests/core-appliance.sh` must build the image, start it with host networking and a temporary state volume, then execute these checks: + +```bash +curl --fail --silent --show-error "http://127.0.0.1:${port}/" >/dev/null +podman exec "$name" /usr/bin/bash -c 'test "$(id -u):$(id -g)" = 65532:65532' +test -d "$state_dir/ppd" +test -d "$state_dir/spool" +test -d "$state_dir/cups/ssl" +podman stop --time 15 "$name" >/dev/null +read -r running exit_status <<< "$(podman inspect "$name" --format '{{.State.Running}} {{.State.ExitCode}}')" +test "$running" = false +test "$exit_status" -eq 143 +``` + +Exit status `143` proves catatonit and the launcher completed the TERM path; Podman's timeout fallback would report SIGKILL status `137`. + +For child-failure propagation, start a second container, wait for HTTP readiness, and kill its D-Bus child without adding runtime packages: + +```bash +podman exec "$failure_name" /usr/bin/bash -c ' + for proc in /proc/[0-9]*; do + read -r comm < "$proc/comm" || continue + if [[ "$comm" == dbus-daemon ]]; then + kill -KILL "${proc##*/}" + exit 0 + fi + done + exit 1 +' +for _ in $(seq 1 150); do + running="$(podman inspect "$failure_name" --format '{{.State.Running}}')" + [[ "$running" == false ]] && break + sleep 0.1 +done +read -r running status <<< "$(podman inspect "$failure_name" --format '{{.State.Running}} {{.State.ExitCode}}')" +test "$running" = false +test "$status" -ne 0 +``` + +Also run the image once with `PORT=invalid` and assert exit status `64` plus `PORT must be numeric` on stderr. + +- [x] **Step 3: Run the complete core acceptance command** + +Run: + +```bash +just verify-core +``` + +Expected: the actual image reaches HTTP readiness as UID/GID `65532`, initializes the mounted state tree, exits cleanly on TERM, exits nonzero when D-Bus dies, and rejects an invalid port with status `64`. + +- [x] **Step 4: Re-run the CUPS ownership gate** + +Run: + +```bash +just verify-cups-patch-chain +``` + +Expected: one FSDK private CUPS base remains, canonical DNS-SD and USB-quirk patches are staged, and public CUPS split rules remain intact. + +- [x] **Step 5: Commit the lifecycle slice** + +```bash +git add Justfile files/container-entrypoint.sh tests/core-appliance.sh +git commit -m "test: verify core appliance lifecycle" +``` + +### Task 4: Close and publish the implementation slice + +**Files:** +- Modify: `.scratch/fsdk-container-modernization/issues/02-boot-minimal-fsdk-printer-application.md` (local tracker; ignored by Git) +- Modify: `docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md` + +**Interfaces:** +- Consumes: passing `just validate`, `just verify-core`, and `just verify-cups-patch-chain` results. +- Produces: resolved local ticket, committed plan, and a pushed `feat/fsdk-core-app` branch in `projectbluefin/ghostscript-printer-app`. + +- [x] **Step 1: Mark every plan checkbox complete only after its command passes** + +Update this file's completed steps from `- [ ]` to `- [x]`; do not mark commands that were not executed successfully. + +- [x] **Step 2: Resolve the local ticket** + +Set issue 02 to: + +```markdown +**Status:** resolved +``` + +Append an `## Answer` recording the implementing commit IDs and exact successful verification commands. + +- [x] **Step 3: Commit the plan and final metadata** + +```bash +git add docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md +git commit -m "docs: record FSDK core appliance plan" -m "Assisted-by: github-copilot/gpt-5.6-sol via pi" +``` + +The `.scratch` ticket remains local because `.scratch/` is intentionally ignored. + +- [x] **Step 4: Integrate the current Project Bluefin main branch** + +Run: + +```bash +git rebase main +``` + +Expected: the feature branch contains the three existing post-branch Rock payload fixes without changing this slice's FSDK contract. + +- [x] **Step 5: Verify the branch tip from a clean worktree** + +Run: + +```bash +just validate +just verify-core +just verify-cups-patch-chain +git status --short +``` + +Expected: all three commands succeed and `git status --short` prints nothing. + +- [ ] **Step 6: Push the feature branch to Project Bluefin** + +```bash +git push --set-upstream origin feat/fsdk-core-app +``` + +Expected: `origin/feat/fsdk-core-app` is created or updated at the verified branch tip. Do not tag or publish the core-only image. From 0209be551051f0e90521acc275c318c2da9922e2 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 02:11:53 -0400 Subject: [PATCH 06/22] docs(skills): capture FSDK runtime constraints Assisted-by: github-copilot/gpt-5.6-sol via pi --- docs/skills/fsdk-cups-patching.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/skills/fsdk-cups-patching.md b/docs/skills/fsdk-cups-patching.md index ef01c73..fbc9946 100644 --- a/docs/skills/fsdk-cups-patching.md +++ b/docs/skills/fsdk-cups-patching.md @@ -30,6 +30,9 @@ metadata: 6. Do not stage a second CUPS implementation. Duplicate `libcups.so*` ownership creates an artifact overlap and can compile reverse dependencies against a different library than the application receives. 7. When moving a shared patch, update both Snap and Rock references while Rock remains. Apply patches from the source root when their paths start with `a/backend/` and use `-p1`. 8. Cross-junction source checkouts nest under `//`; the CUPS probe therefore checks `freedesktop-sdk/components-_private-cups-base/`, not the checkout root. +9. Match FSDK's multiarch install layout for every repository-built library. Define `gcc-triplet`, `lib`, and `libdir` in the root project and pass `--libdir=%{libdir}` to Autotools; FSDK's `pkg-config` searches `/usr/lib//pkgconfig`, not `/usr/lib/pkgconfig`. +10. Do not `chown` high numeric runtime IDs inside the BuildStream sandbox; user-namespace mappings can reject them with `EINVAL`. After composition, reapply writable directory modes in the final OCI layer. Remove inherited `/run` service directories and let the numeric runtime user recreate them so ownership checks observe the actual user. +11. Avahi's `--no-drop-root` still resolves its compiled `AVAHI_USER`/`AVAHI_GROUP` and requires its runtime directory to have those numeric IDs. When one numeric OCI user runs the service stack, provide the Avahi names as aliases for that UID/GID. Remove D-Bus's `` directive so it does not attempt a second privilege drop, and patch Avahi policy at `/etc/dbus-1/system.d/avahi-dbus.conf`. ## Common Rationalizations @@ -49,6 +52,10 @@ metadata: - `cups-libs` or `cups-license` disappears from the FSDK CUPS split rules. - A manifest invokes a patch after changing into a subdirectory incompatible with its `a/...` paths. - An FSDK junction update lands without rerunning the patch-chain verification. +- Repository-built `.pc` files under `/usr/lib/pkgconfig` while the FSDK build sandbox searches only `/usr/lib//pkgconfig` and `/usr/share/pkgconfig`. +- `chown 65532:65532` in a BuildStream build command; unprivileged sandbox UID maps do not guarantee that numeric owner exists. +- Pre-creating Avahi's runtime directory as root; Avahi verifies it belongs to its compiled service UID even with `--no-drop-root`. +- Editing `/usr/share/dbus-1/system.d/avahi-dbus.conf`; the FSDK runtime installs that policy under `/etc/dbus-1/system.d/`. ## Verification @@ -58,3 +65,6 @@ metadata: - [ ] The staged CUPS source contains the DNS-SD and `USB_QUIRK_DIR` changes. - [ ] The CUPS base still exposes `cups-libs` and `cups-license`. - [ ] Both current Snap and Rock CUPS source versions accept the canonical patches while both packaging paths exist. +- [ ] Repository-built libraries install their `.pc` files in FSDK's multiarch pkg-config directory and are discoverable from a dependent element's build sandbox. +- [ ] The exported image runs with the numeric UID/GID, creates runtime directories, and reaches application readiness. +- [ ] TERM yields signal exit status `143`, not Podman's SIGKILL timeout status `137`; killing a required child makes the container exit nonzero. From 3897e9a867fb08ea748715973e388d41f1346118 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 02:32:00 -0400 Subject: [PATCH 07/22] fix: harden core appliance lifecycle Assisted-by: github-copilot/gpt-5.6-sol via pi --- docs/skills/fsdk-cups-patching.md | 3 ++- .../plans/2026-09-16-fsdk-core-appliance.md | 24 ++++++++++++------- elements/oci/ghostscript-printer-app.bst | 2 -- files/container-entrypoint.sh | 23 ++++++++++++++---- ...stomize-cups-for-printer-application.patch | 12 ++++++++++ tests/core-appliance.sh | 13 ++++++---- 6 files changed, 55 insertions(+), 22 deletions(-) diff --git a/docs/skills/fsdk-cups-patching.md b/docs/skills/fsdk-cups-patching.md index fbc9946..2a93473 100644 --- a/docs/skills/fsdk-cups-patching.md +++ b/docs/skills/fsdk-cups-patching.md @@ -32,7 +32,7 @@ metadata: 8. Cross-junction source checkouts nest under `//`; the CUPS probe therefore checks `freedesktop-sdk/components-_private-cups-base/`, not the checkout root. 9. Match FSDK's multiarch install layout for every repository-built library. Define `gcc-triplet`, `lib`, and `libdir` in the root project and pass `--libdir=%{libdir}` to Autotools; FSDK's `pkg-config` searches `/usr/lib//pkgconfig`, not `/usr/lib/pkgconfig`. 10. Do not `chown` high numeric runtime IDs inside the BuildStream sandbox; user-namespace mappings can reject them with `EINVAL`. After composition, reapply writable directory modes in the final OCI layer. Remove inherited `/run` service directories and let the numeric runtime user recreate them so ownership checks observe the actual user. -11. Avahi's `--no-drop-root` still resolves its compiled `AVAHI_USER`/`AVAHI_GROUP` and requires its runtime directory to have those numeric IDs. When one numeric OCI user runs the service stack, provide the Avahi names as aliases for that UID/GID. Remove D-Bus's `` directive so it does not attempt a second privilege drop, and patch Avahi policy at `/etc/dbus-1/system.d/avahi-dbus.conf`. +11. Avahi's `--no-drop-root` still resolves its compiled `AVAHI_USER`/`AVAHI_GROUP` and requires its runtime directory to have those numeric IDs. Configure FSDK's Avahi build with `--with-avahi-user=nonroot --with-avahi-group=nonroot`; never create a second passwd/group name with UID/GID `65532`. Remove D-Bus's `` directive so it does not attempt a second privilege drop, and patch Avahi policy at `/etc/dbus-1/system.d/avahi-dbus.conf`. ## Common Rationalizations @@ -55,6 +55,7 @@ metadata: - Repository-built `.pc` files under `/usr/lib/pkgconfig` while the FSDK build sandbox searches only `/usr/lib//pkgconfig` and `/usr/share/pkgconfig`. - `chown 65532:65532` in a BuildStream build command; unprivileged sandbox UID maps do not guarantee that numeric owner exists. - Pre-creating Avahi's runtime directory as root; Avahi verifies it belongs to its compiled service UID even with `--no-drop-root`. +- Giving `avahi` and `nonroot` the same UID/GID; numeric-to-name lookup becomes ambiguous and can hide a broken OCI identity. - Editing `/usr/share/dbus-1/system.d/avahi-dbus.conf`; the FSDK runtime installs that policy under `/etc/dbus-1/system.d/`. ## Verification diff --git a/docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md b/docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md index d5f0aa8..3d4d3a7 100644 --- a/docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md +++ b/docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md @@ -175,7 +175,7 @@ git commit -m "build: add core FSDK printer application graph" - [x] **Step 3: Define OCI identity and process metadata** -`elements/oci/ghostscript-printer-app.bst` must add matching passwd/group records for UID/GID `65532`, adjust FSDK D-Bus/Avahi policies to that identity, and emit this runtime configuration: +`elements/oci/ghostscript-printer-app.bst` must add one matching passwd/group record for UID/GID `65532`, adjust FSDK D-Bus/Avahi policies to that identity, and emit this runtime configuration. The junction patch configures Avahi's compiled service user and group as `nonroot`; do not add a duplicate UID alias. ```yaml User: '65532:65532' @@ -199,7 +199,9 @@ Run: just build podman image inspect ghcr.io/projectbluefin/ghostscript-printer-app:build --format '{{json .Config}}' podman run --rm --entrypoint /usr/bin/bash ghcr.io/projectbluefin/ghostscript-printer-app:build -c ' + set -e test "$(id -u):$(id -g)" = 65532:65532 + test "$(id -un)" = nonroot passwd_ok=0 while IFS=: read -r name password uid gid gecos home shell; do [[ "$name:$uid:$gid" == "nonroot:65532:65532" ]] && passwd_ok=1 @@ -225,6 +227,8 @@ git commit -m "build: compose core FSDK OCI appliance" **Files:** - Modify: `files/container-entrypoint.sh` +- Modify: `elements/oci/ghostscript-printer-app.bst` +- Modify: `patches/freedesktop-sdk/0001-customize-cups-for-printer-application.patch` - Create: `tests/core-appliance.sh` - Modify: `Justfile` @@ -240,18 +244,20 @@ The launcher must: set -euo pipefail ``` -Validate `PORT` with Bash's numeric regular expression, initialize state directories without overwriting existing files, export the repository runtime-path contract, start D-Bus and wait for `/run/dbus/system_bus_socket`, start Avahi and wait for `/run/avahi-daemon/pid`, then start `ghostscript-printer-app`. Track every child PID. On TERM, INT, EXIT, or the first required child exit, send TERM to all remaining children and wait for them. +Validate `PORT` with Bash's numeric regular expression, initialize state directories and seed `cups/snmp.conf` only when absent, export the repository runtime-path contract, start D-Bus and wait for `/run/dbus/system_bus_socket`, start Avahi and wait for `/run/avahi-daemon/pid`, then start `ghostscript-printer-app`. Track every child PID. On TERM or INT, stop the application, Avahi, then D-Bus and retain TERM status `143`. Any unexpected required-child exit, including status `0`, must stop the remaining children and return nonzero. - [x] **Step 2: Extend the smoke check to assert the observable contract** -`tests/core-appliance.sh` must build the image, start it with host networking and a temporary state volume, then execute these checks: +`tests/core-appliance.sh` must build the image, start it without a `--user` override using host networking and a temporary state volume, require an application-specific HTTP title, then execute these checks: ```bash -curl --fail --silent --show-error "http://127.0.0.1:${port}/" >/dev/null -podman exec "$name" /usr/bin/bash -c 'test "$(id -u):$(id -g)" = 65532:65532' +response="$(curl --fail --silent --show-error "http://127.0.0.1:${port}/")" +[[ "$response" == *'Ghostscript Printer Application'* ]] +podman exec "$name" /usr/bin/bash -c 'test "$(id -u):$(id -g):$(id -un)" = 65532:65532:nonroot' test -d "$state_dir/ppd" test -d "$state_dir/spool" test -d "$state_dir/cups/ssl" +test -s "$state_dir/cups/snmp.conf" podman stop --time 15 "$name" >/dev/null read -r running exit_status <<< "$(podman inspect "$name" --format '{{.State.Running}} {{.State.ExitCode}}')" test "$running" = false @@ -260,14 +266,14 @@ test "$exit_status" -eq 143 Exit status `143` proves catatonit and the launcher completed the TERM path; Podman's timeout fallback would report SIGKILL status `137`. -For child-failure propagation, start a second container, wait for HTTP readiness, and kill its D-Bus child without adding runtime packages: +For child-failure propagation, start a second container, prove the edited persistent SNMP configuration survived, then terminate Avahi cleanly. The supervisor must still return nonzero because any required-child exit is a failure: ```bash podman exec "$failure_name" /usr/bin/bash -c ' for proc in /proc/[0-9]*; do read -r comm < "$proc/comm" || continue - if [[ "$comm" == dbus-daemon ]]; then - kill -KILL "${proc##*/}" + if [[ "$comm" == avahi-daemon ]]; then + kill -TERM "${proc##*/}" exit 0 fi done @@ -293,7 +299,7 @@ Run: just verify-core ``` -Expected: the actual image reaches HTTP readiness as UID/GID `65532`, initializes the mounted state tree, exits cleanly on TERM, exits nonzero when D-Bus dies, and rejects an invalid port with status `64`. +Expected: the actual image reaches its own HTTP interface as `nonroot` UID/GID `65532`, initializes and preserves the mounted state tree, exits with `143` on TERM rather than Podman's `137` timeout fallback, exits nonzero when Avahi exits cleanly, and rejects an invalid port with status `64`. - [x] **Step 4: Re-run the CUPS ownership gate** diff --git a/elements/oci/ghostscript-printer-app.bst b/elements/oci/ghostscript-printer-app.bst index 41f51c1..f605f93 100644 --- a/elements/oci/ghostscript-printer-app.bst +++ b/elements/oci/ghostscript-printer-app.bst @@ -15,12 +15,10 @@ config: install -d /layer/etc printf '%s\n' \ 'root:x:0:0:root:/root:/usr/bin/bash' \ - 'avahi:x:65532:65532:Avahi:/run/avahi-daemon:/usr/sbin/nologin' \ 'nonroot:x:65532:65532:Printer Application:/var/lib/ghostscript-printer-app:/usr/sbin/nologin' \ >> /layer/etc/passwd printf '%s\n' \ 'root:x:0:' \ - 'avahi:x:65532:' \ 'nonroot:x:65532:' \ >> /layer/etc/group rm -rf /layer/run/dbus /layer/run/avahi-daemon /layer/run/ghostscript-printer-app diff --git a/files/container-entrypoint.sh b/files/container-entrypoint.sh index 8e1270b..4c8eaf1 100755 --- a/files/container-entrypoint.sh +++ b/files/container-entrypoint.sh @@ -8,6 +8,9 @@ fi state_dir=/var/lib/ghostscript-printer-app mkdir -p "$state_dir/ppd" "$state_dir/spool" "$state_dir/usb" "$state_dir/cups/ssl" /run/dbus /run/avahi-daemon /run/ghostscript-printer-app +if [[ ! -e "$state_dir/cups/snmp.conf" ]]; then + cp /etc/cups/snmp.conf "$state_dir/cups/snmp.conf" +fi export BACKEND_DIR=/usr/lib/ghostscript-printer-app/backend export CUPS_SERVERBIN=/usr/lib/ghostscript-printer-app @@ -24,13 +27,20 @@ export USB_QUIRK_DIR="$state_dir" children=() stop_children() { - local pid - for pid in "${children[@]}"; do + local index pid + for ((index = ${#children[@]} - 1; index >= 0; index--)); do + pid="${children[index]}" kill -TERM "$pid" 2>/dev/null || true done wait "${children[@]}" 2>/dev/null || true } -trap stop_children TERM INT EXIT +handle_signal() { + trap - TERM INT EXIT + stop_children + exit 143 +} +trap handle_signal TERM INT +trap stop_children EXIT dbus-daemon --system --nofork --nopidfile & children+=("$!") @@ -55,8 +65,11 @@ fi ghostscript-printer-app "${args[@]}" server & children+=("$!") -wait -n "${children[@]}" -status=$? +if wait -n "${children[@]}"; then + status=1 +else + status=$? +fi stop_children trap - TERM INT EXIT exit "$status" diff --git a/patches/freedesktop-sdk/0001-customize-cups-for-printer-application.patch b/patches/freedesktop-sdk/0001-customize-cups-for-printer-application.patch index 39e7ae6..9eaa7a7 100644 --- a/patches/freedesktop-sdk/0001-customize-cups-for-printer-application.patch +++ b/patches/freedesktop-sdk/0001-customize-cups-for-printer-application.patch @@ -40,3 +40,15 @@ diff --git a/elements/components/cups.bst b/elements/components/cups.bst config: include: +diff --git a/elements/components/_private/avahi-base.bst b/elements/components/_private/avahi-base.bst +--- a/elements/components/_private/avahi-base.bst ++++ b/elements/components/_private/avahi-base.bst +@@ -16,6 +16,8 @@ build-depends: + + variables: + conf-local: >- ++ --with-avahi-user=nonroot ++ --with-avahi-group=nonroot + --with-distro=none + --disable-libevent + --disable-qt3 diff --git a/tests/core-appliance.sh b/tests/core-appliance.sh index 2a58e0c..a28c47e 100755 --- a/tests/core-appliance.sh +++ b/tests/core-appliance.sh @@ -17,8 +17,9 @@ trap cleanup EXIT wait_for_http() { local target_port="$1" + local response for _ in $(seq 1 60); do - if curl --fail --silent --show-error "http://127.0.0.1:${target_port}/" >/dev/null 2>&1; then + if response="$(curl --fail --silent --show-error "http://127.0.0.1:${target_port}/" 2>/dev/null)" && [[ "$response" == *'Ghostscript Printer Application'* ]]; then return 0 fi sleep 1 @@ -32,13 +33,13 @@ chmod 0777 "$state_dir" podman run -d \ --name "$name" \ --network host \ - --user 65532:65532 \ -e PORT="$port" \ -v "$state_dir:/var/lib/ghostscript-printer-app:Z" \ "$image" >/dev/null wait_for_http "$port" podman exec "$name" /usr/bin/bash -c ' + set -e test "$(id -u):$(id -g)" = 65532:65532 test "$(id -un)" = nonroot passwd_ok=0 @@ -54,6 +55,8 @@ podman exec "$name" /usr/bin/bash -c ' test -d "$state_dir/ppd" test -d "$state_dir/spool" test -d "$state_dir/cups/ssl" +test -s "$state_dir/cups/snmp.conf" +podman exec "$name" /usr/bin/bash -c 'printf "%s\n" "# preserved" > /var/lib/ghostscript-printer-app/cups/snmp.conf' podman stop --time 15 "$name" >/dev/null read -r running exit_status <<< "$(podman inspect "$name" --format '{{.State.Running}} {{.State.ExitCode}}')" if [[ "$running" != false || "$exit_status" -ne 143 ]]; then @@ -65,17 +68,17 @@ fi podman run -d \ --name "$failure_name" \ --network host \ - --user 65532:65532 \ -e PORT="$failure_port" \ -v "$state_dir:/var/lib/ghostscript-printer-app:Z" \ "$image" >/dev/null wait_for_http "$failure_port" +podman exec "$failure_name" /usr/bin/bash -c 'test "$(< /var/lib/ghostscript-printer-app/cups/snmp.conf)" = "# preserved"' podman exec "$failure_name" /usr/bin/bash -c ' for proc in /proc/[0-9]*; do read -r comm < "$proc/comm" || continue - if [[ "$comm" == dbus-daemon ]]; then - kill -KILL "${proc##*/}" + if [[ "$comm" == avahi-daemon ]]; then + kill -TERM "${proc##*/}" exit 0 fi done From 198311ecc0530e3ad4a8cff2c7d6c5e8e55cdb33 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 02:33:34 -0400 Subject: [PATCH 08/22] docs: complete FSDK core appliance plan Assisted-by: github-copilot/gpt-5.6-sol via pi --- docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md b/docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md index 3d4d3a7..455025f 100644 --- a/docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md +++ b/docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md @@ -374,7 +374,7 @@ git status --short Expected: all three commands succeed and `git status --short` prints nothing. -- [ ] **Step 6: Push the feature branch to Project Bluefin** +- [x] **Step 6: Push the feature branch to Project Bluefin** ```bash git push --set-upstream origin feat/fsdk-core-app From 1282d855dc009953f027e80a1a029f53d089a789 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 02:53:56 -0400 Subject: [PATCH 09/22] docs: plan FSDK core driver payload Assisted-by: github-copilot/gpt-5.6-sol via pi --- .../plans/2026-09-16-fsdk-core-payload.md | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-16-fsdk-core-payload.md diff --git a/docs/superpowers/plans/2026-09-16-fsdk-core-payload.md b/docs/superpowers/plans/2026-09-16-fsdk-core-payload.md new file mode 100644 index 0000000..44f15e3 --- /dev/null +++ b/docs/superpowers/plans/2026-09-16-fsdk-core-payload.md @@ -0,0 +1,294 @@ +# FSDK Core Driver Payload Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Expose the core CUPS, Ghostscript, cups-filters, libppd, and Foomatic driver payload from the FSDK appliance and prove one real conversion reaches a socket-backed printer. + +**Architecture:** Reuse FSDK's existing printing artifacts without rebuilding or duplicating them. Build only pyppd and the Foomatic XML compiler that FSDK does not ship, generate the same three self-extracting PPD archives as the current OCI contract, and compose those archives with FSDK's filters plus their measured runtime dependencies. Verify the exported image, not BuildStream source text. + +**Tech Stack:** BuildStream 2, freedesktop-sdk 26.08rc.1, cups-filters 2.0.1, Foomatic DB 20240504, foomatic-db-engine 4.1.0, pyppd 1.1.0, Python 3, xz, Podman, socat. + +## Global Constraints + +- FSDK remains the only owner of CUPS, Ghostscript, cups-filters, libppd, and the Foomatic database. +- Repository elements may build only missing tooling and appliance-specific generated payloads. +- Keep `/usr/lib/ghostscript-printer-app` as the existing symlink to `/usr/lib/cups`; do not duplicate filters. +- Keep PPD drivers under `/usr/share/ppd`, matching the existing launcher `PPD_PATHS` contract. +- Generated pyppd archives must run with the same packaged Python major/minor used to create them and with packaged xz. +- Do not publish this incomplete image. + +--- + +### Task 1: Build the missing PPD tooling + +**Files:** +- Create: `elements/printer-app/pyppd.bst` +- Create: `elements/printer-app/foomatic-db-engine.bst` +- Create: `patches/foomatic-db-engine/xml-database-without-dbi.patch` + +**Interfaces:** +- Consumes: FSDK Python, Perl, XML::Parser, curl, file, gzip, cups-filters, and Foomatic database artifacts. +- Produces: build-time `/usr/bin/pyppd` and `/usr/sbin/foomatic-compiledb` commands. + +- [ ] **Step 1: Add the pyppd element** + +Create `elements/printer-app/pyppd.bst`: + +```yaml +kind: pyproject +description: Build the pyppd self-extracting PPD archive generator. + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-python-setuptools.bst + +depends: + - freedesktop-sdk.bst:components/python3.bst + - freedesktop-sdk.bst:components/xz.bst + +sources: + - kind: git_repo + url: github:OpenPrinting/pyppd.git + track: release-1-1-0 + ref: release-1-1-0-0-g29ccf6cf85781315a696774e7458a2f1f61aac57 +``` + +- [ ] **Step 2: Make Foomatic's XML-only path independent of DBI** + +Create `patches/foomatic-db-engine/xml-database-without-dbi.patch` that removes the unconditional `use DBI;` from `lib/Foomatic/DB.pm` and adds `require DBI;` only inside the MySQL and SQLite branches of `connect_to_mysql_db()`. The XML database path used by `foomatic-compiledb` must not require an unavailable SQL driver. + +- [ ] **Step 3: Add the Foomatic engine element** + +Create `elements/printer-app/foomatic-db-engine.bst` with immutable commit `e4e7b9cd28ba160428f82bc5234559d1f50e5c42`, the DBI patch queue, and these build dependencies: + +```yaml +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-autotools.bst + - freedesktop-sdk.bst:components/cups-filters.bst + - freedesktop-sdk.bst:components/curl.bst + - freedesktop-sdk.bst:components/file.bst + - freedesktop-sdk.bst:components/foomatic-db.bst + - freedesktop-sdk.bst:components/ghostscript.bst + - freedesktop-sdk.bst:components/gzip.bst + - freedesktop-sdk.bst:components/perl.bst + - freedesktop-sdk.bst:components/perl-xml-parser.bst +``` + +Use these commands: + +```yaml +config: + build-commands: + - ./make_configure + - PERL_INSTALLDIRS=vendor ./configure --prefix=/usr --libdir="%{libdir}" + - make -j1 + install-commands: + - make DESTDIR="%{install-root}" install +``` + +- [ ] **Step 4: Verify both tools build** + +Run: + +```bash +just bst build printer-app/pyppd.bst printer-app/foomatic-db-engine.bst +just bst artifact list-contents printer-app/pyppd.bst printer-app/foomatic-db-engine.bst +``` + +Expected: the artifacts contain `/usr/bin/pyppd` and `/usr/sbin/foomatic-compiledb`; no second CUPS library is introduced. + +- [ ] **Step 5: Commit the tooling** + +```bash +git add elements/printer-app/pyppd.bst elements/printer-app/foomatic-db-engine.bst patches/foomatic-db-engine/xml-database-without-dbi.patch +git commit -m "build: add core PPD generation tools" -m "Assisted-by: github-copilot/gpt-5.6-sol via pi" +``` + +### Task 2: Generate the core PPD archives + +**Files:** +- Create: `elements/printer-app/core-payload.bst` + +**Interfaces:** +- Consumes: FSDK cups-filters PPDs and Foomatic XML/manufacturer data plus the Task 1 generators. +- Produces: executable `/usr/share/ppd/cups-filters-ppds`, `/usr/share/ppd/foomatic-ppds`, and `/usr/share/ppd/manufacturer-ppds` archives. + +- [ ] **Step 1: Stage source payloads and generators** + +Create a `manual` element with build dependencies on `printer-app/pyppd.bst`, `printer-app/foomatic-db-engine.bst`, `freedesktop-sdk.bst:components/cups-filters.bst`, `freedesktop-sdk.bst:components/foomatic-db.bst`, and `freedesktop-sdk.bst:public-stacks/runtime-gnu.bst`. + +- [ ] **Step 2: Generate the cups-filters archive** + +The build commands must copy `/usr/share/ppd/cupsfilters` to `payload/cupsfilters` and run: + +```bash +pyppd -v -o cups-filters-ppds payload/cupsfilters +``` + +- [ ] **Step 3: Generate the Foomatic archives** + +Copy `/usr/share/foomatic` to `payload/foomatic`, remove PostScript manufacturer PPDs and the unsupported driver XML files carried by the current Snap contract: + +```text +bjc800j.xml c2070.xml drv_x125.xml lm1100.xml lpstyl.xml ml85p.xml +pbm2l2030.xml pbm2l7k.xml pbm2lwxl.xml pentaxpj.xml ppmtomd.xml +``` + +Then run: + +```bash +FOOMATICDB="$PWD/payload/foomatic" foomatic-compiledb -j "%{max-jobs}" -t ppd -d payload/foomatic-ppds +pyppd -v -o foomatic-ppds payload/foomatic-ppds +pyppd -v -o manufacturer-ppds payload/foomatic/db/source/PPD +``` + +- [ ] **Step 4: Install only generated archives** + +Install the three executable archives with mode `0755` beneath `%{install-root}/usr/share/ppd`. Do not carry the Foomatic compiler, raw XML database, or pyppd package into this artifact. + +- [ ] **Step 5: Build and inspect the payload artifact** + +Run: + +```bash +just bst build printer-app/core-payload.bst +just bst artifact list-contents printer-app/core-payload.bst +``` + +Expected: exactly the three executable archive files appear under `/usr/share/ppd`. + +- [ ] **Step 6: Commit the generated-payload element** + +```bash +git add elements/printer-app/core-payload.bst +git commit -m "build: generate core PPD archives" -m "Assisted-by: github-copilot/gpt-5.6-sol via pi" +``` + +### Task 3: Compose the runtime payload and verify its closure + +**Files:** +- Modify: `elements/printer-app/core-stack.bst` +- Create: `tests/core-payload.sh` +- Modify: `Justfile` + +**Interfaces:** +- Consumes: `printer-app/core-payload.bst` and FSDK runtime components. +- Produces: `just verify-payload`, proving driver archives, filters, interpreters, HTTPS, and shared-library closure inside the exported image. + +- [ ] **Step 1: Write the failing image-level payload check** + +Create `tests/core-payload.sh`. It must run `just build`, then assert from the image that: + +```text +/usr/lib/ghostscript-printer-app -> /usr/lib/cups +/usr/lib/cups/filter/foomatic-rip +/usr/lib/cups/filter/gstoraster +/usr/lib/cups/filter/pdftops +/usr/lib/cups/filter/rastertoescpx +/usr/lib/cups/filter/rastertopclx +/usr/share/ghostscript-printer-app/testpage.ps +/usr/share/ppd/cups-filters-ppds +/usr/share/ppd/foomatic-ppds +/usr/share/ppd/manufacturer-ppds +/usr/bin/python3 +/usr/bin/xz +``` + +For each archive, run `list`, capture its first URI, run `cat `, and require the extracted text to contain `*PPD-Adobe:`. Run this check now; it must fail because the payload is not yet composed. + +- [ ] **Step 2: Add runtime components** + +Add these dependencies to `core-stack.bst`: + +```yaml +- printer-app/core-payload.bst +- freedesktop-sdk.bst:components/cups-filters.bst +- freedesktop-sdk.bst:components/mutool.bst +- freedesktop-sdk.bst:components/python3.bst +- freedesktop-sdk.bst:components/xz.bst +``` + +Keep the existing Ghostscript, CUPS, libppd, and libcupsfilters dependencies. + +- [ ] **Step 3: Verify HTTP and HTTPS** + +The payload test starts the real image on host networking, waits for `Ghostscript Printer Application` over HTTP, and requires the same title over HTTPS with `curl --insecure`. + +- [ ] **Step 4: Verify ELF closure** + +Inside the image, run `ldd` for the application and each core filter listed in Step 1. Fail if any output contains `not found`. + +- [ ] **Step 5: Add the verification command** + +Add to `Justfile`: + +```just +verify-payload: + tests/core-payload.sh +``` + +- [ ] **Step 6: Run and commit the runtime checks** + +Run: + +```bash +just verify-payload +``` + +Expected: all payload, archive, HTTPS, and ELF checks pass. + +```bash +git add Justfile elements/printer-app/core-stack.bst tests/core-payload.sh +git commit -m "test: verify core driver payload" -m "Assisted-by: github-copilot/gpt-5.6-sol via pi" +``` + +### Task 4: Prove a deterministic print conversion + +**Files:** +- Modify: `tests/core-payload.sh` + +**Interfaces:** +- Consumes: running Printer Application, `cups-filters-ppds:Generic-PDF_Printer-PDF.ppd`, repository test page, and host `socat`. +- Produces: non-empty printer-language output captured from a real submitted job. + +- [ ] **Step 1: Add the socket-backed printer test** + +Start a one-shot host sink before the container: + +```bash +socat -u "TCP-LISTEN:${sink_port},reuseaddr" "OPEN:${output_file},creat,trunc" & +sink_pid=$! +``` + +After HTTP/HTTPS readiness, run inside the image: + +```bash +ghostscript-printer-app -u "ipp://127.0.0.1:${port}/ipp/system" add core-test \ + -m cups-filters-ppds:Generic-PDF_Printer-PDF.ppd \ + -v "cups:socket://127.0.0.1:${sink_port}" +ghostscript-printer-app -u "ipp://127.0.0.1:${port}/ipp/system" \ + -d core-test submit /usr/share/ghostscript-printer-app/testpage.ps +``` + +- [ ] **Step 2: Assert conversion output** + +Poll until `${output_file}` is non-empty, then require its first five bytes to be `%PDF-`. A successful CLI return without captured output is a failure. + +- [ ] **Step 3: Re-run all slice gates** + +Run: + +```bash +just validate +just verify-core +just verify-payload +just verify-cups-patch-chain +actionlint .github/workflows/*.yml +bash -n files/container-entrypoint.sh tests/core-appliance.sh tests/core-payload.sh tests/cups-patch-chain.sh +git diff --check +``` + +Expected: every command succeeds. + +- [ ] **Step 4: Resolve and publish the slice** + +Set `.scratch/fsdk-container-modernization/issues/03-serve-core-ppd-filter-payload.md` to `resolved`, record exact successful commands and commit IDs, commit this completed plan, push `feat/fsdk-core-payload`, and open a pull request with base `feat/fsdk-core-app`. Do not publish an OCI release. From 54eabd1a95c984ce81ae222cefbeeb593bcee956 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 02:59:46 -0400 Subject: [PATCH 10/22] build: add core PPD generation tools Assisted-by: github-copilot/gpt-5.6-sol via pi --- elements/printer-app/foomatic-db-engine.bst | 29 +++++++++++++++++++ elements/printer-app/pyppd.bst | 21 ++++++++++++++ .../xml-database-without-dbi.patch | 28 ++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 elements/printer-app/foomatic-db-engine.bst create mode 100644 elements/printer-app/pyppd.bst create mode 100644 patches/foomatic-db-engine/xml-database-without-dbi.patch diff --git a/elements/printer-app/foomatic-db-engine.bst b/elements/printer-app/foomatic-db-engine.bst new file mode 100644 index 0000000..e33c20f --- /dev/null +++ b/elements/printer-app/foomatic-db-engine.bst @@ -0,0 +1,29 @@ +kind: manual +description: Build the Foomatic XML database compiler. + +sources: + - kind: git_repo + url: github:OpenPrinting/foomatic-db-engine.git + track: master + ref: e4e7b9cd28ba160428f82bc5234559d1f50e5c42 + - kind: patch_queue + path: patches/foomatic-db-engine + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-autotools.bst + - freedesktop-sdk.bst:components/cups-filters.bst + - freedesktop-sdk.bst:components/curl.bst + - freedesktop-sdk.bst:components/file.bst + - freedesktop-sdk.bst:components/foomatic-db.bst + - freedesktop-sdk.bst:components/ghostscript.bst + - freedesktop-sdk.bst:components/gzip.bst + - freedesktop-sdk.bst:components/perl.bst + - freedesktop-sdk.bst:components/perl-xml-parser.bst + +config: + build-commands: + - ./make_configure + - PERL_INSTALLDIRS=vendor ./configure --prefix=/usr --libdir="%{libdir}" + - make -j1 + install-commands: + - make DESTDIR="%{install-root}" install diff --git a/elements/printer-app/pyppd.bst b/elements/printer-app/pyppd.bst new file mode 100644 index 0000000..aa360f8 --- /dev/null +++ b/elements/printer-app/pyppd.bst @@ -0,0 +1,21 @@ +kind: manual +description: Build the pyppd self-extracting PPD archive generator. + +sources: + - kind: git_repo + url: github:OpenPrinting/pyppd.git + track: release-1-1-0 + ref: release-1-1-0-0-g29ccf6cf85781315a696774e7458a2f1f61aac57 + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-python-setuptools.bst + +depends: + - freedesktop-sdk.bst:components/python3.bst + - freedesktop-sdk.bst:components/xz.bst + +config: + build-commands: + - python3 -m build --no-isolation --wheel --outdir dist . + install-commands: + - python3 -m installer dist/*.whl --destdir "%{install-root}" diff --git a/patches/foomatic-db-engine/xml-database-without-dbi.patch b/patches/foomatic-db-engine/xml-database-without-dbi.patch new file mode 100644 index 0000000..adc91cc --- /dev/null +++ b/patches/foomatic-db-engine/xml-database-without-dbi.patch @@ -0,0 +1,28 @@ +diff --git a/lib/Foomatic/DB.pm b/lib/Foomatic/DB.pm +index 50990cc..cb46f5e 100644 +--- a/lib/Foomatic/DB.pm ++++ b/lib/Foomatic/DB.pm +@@ -14,7 +14,6 @@ use Encode; + use Foomatic::Defaults qw(:DEFAULT $DEBUG); + use Foomatic::filters::xml::xmlParse; + use Foomatic::filters::xml::to; +-use DBI; + use Data::Dumper; + use POSIX; # for rounding integers + use strict; +@@ -93,6 +92,7 @@ sub connect_to_mysql_db { + $mysqlconf{'user'} = 'root' if !$mysqlconf{'user'}; + $mysqlconf{'password'} = '' if !$mysqlconf{'password'}; + $mysqlconf{'database'} = 'openprinting' if !$mysqlconf{'database'}; ++ require DBI; + $this->{'dbh'} = DBI->connect("dbi:mysql:database=" . + $mysqlconf{'database'} . ';host=' . + $mysqlconf{'server'}, +@@ -103,6 +103,7 @@ sub connect_to_mysql_db { + $this->{'dbtype'} = 'mysql'; + } elsif(-r $sqlitedb) { + $sqlitedb = "$libdir/db/openprinting.db"; ++ require DBI; + $this->{'dbh'} = DBI->connect("dbi:SQLite:dbname=$sqlitedb","","")or + warn $this->{'dbh'}->errstr; + $this->{'dbh'}->do('PRAGMA synchronous = OFF;'); From dedc48ee5cda71cf8e9864846a13415cb9677234 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 03:53:05 -0400 Subject: [PATCH 11/22] build: add Foomatic XML dependencies Assisted-by: github-copilot/gpt-5.6-sol via pi --- elements/printer-app/foomatic-db-engine.bst | 6 ++- elements/printer-app/perl-clone.bst | 14 ++++++ elements/printer-app/perl-xml-libxml.bst | 20 +++++++++ .../perl-xml-namespace-support.bst | 14 ++++++ elements/printer-app/perl-xml-sax-base.bst | 14 ++++++ elements/printer-app/perl-xml-sax.bst | 16 +++++++ .../xml-database-without-dbi.patch | 12 +++++ .../perl-xml-libxml/use-system-libxml2.patch | 45 +++++++++++++++++++ project.conf | 3 ++ 9 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 elements/printer-app/perl-clone.bst create mode 100644 elements/printer-app/perl-xml-libxml.bst create mode 100644 elements/printer-app/perl-xml-namespace-support.bst create mode 100644 elements/printer-app/perl-xml-sax-base.bst create mode 100644 elements/printer-app/perl-xml-sax.bst create mode 100644 patches/perl-xml-libxml/use-system-libxml2.patch diff --git a/elements/printer-app/foomatic-db-engine.bst b/elements/printer-app/foomatic-db-engine.bst index e33c20f..4e98b7c 100644 --- a/elements/printer-app/foomatic-db-engine.bst +++ b/elements/printer-app/foomatic-db-engine.bst @@ -10,6 +10,8 @@ sources: path: patches/foomatic-db-engine build-depends: + - printer-app/perl-clone.bst + - printer-app/perl-xml-libxml.bst - freedesktop-sdk.bst:public-stacks/buildsystem-autotools.bst - freedesktop-sdk.bst:components/cups-filters.bst - freedesktop-sdk.bst:components/curl.bst @@ -26,4 +28,6 @@ config: - PERL_INSTALLDIRS=vendor ./configure --prefix=/usr --libdir="%{libdir}" - make -j1 install-commands: - - make DESTDIR="%{install-root}" install + - make DESTDIR="$PWD/full-install" install + - install -D -m 0755 full-install/usr/sbin/foomatic-compiledb "%{install-root}/usr/bin/foomatic-compiledb" + - mkdir -p "%{install-root}/usr/lib" && cp -a full-install/usr/lib/perl5 "%{install-root}/usr/lib/" diff --git a/elements/printer-app/perl-clone.bst b/elements/printer-app/perl-clone.bst new file mode 100644 index 0000000..cce4677 --- /dev/null +++ b/elements/printer-app/perl-clone.bst @@ -0,0 +1,14 @@ +kind: makemaker +description: Build the Perl Clone module used by Foomatic. + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-make.bst + +depends: + - freedesktop-sdk.bst:components/perl.bst + +sources: + - kind: cpan + name: Clone + suffix: authors/id/A/AT/ATOOMIC/Clone-0.50.tar.gz + sha256sum: f9732a4a857974db30905233589113003301b585b0cecda29a21cfba5bb014f9 diff --git a/elements/printer-app/perl-xml-libxml.bst b/elements/printer-app/perl-xml-libxml.bst new file mode 100644 index 0000000..a4c8ab9 --- /dev/null +++ b/elements/printer-app/perl-xml-libxml.bst @@ -0,0 +1,20 @@ +kind: makemaker +description: Build XML::LibXML against FSDK libxml2 for Foomatic. + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-make.bst + - freedesktop-sdk.bst:components/pkg-config.bst + +depends: + - printer-app/perl-xml-namespace-support.bst + - printer-app/perl-xml-sax.bst + - freedesktop-sdk.bst:components/libxml2.bst + - freedesktop-sdk.bst:components/perl.bst + +sources: + - kind: cpan + name: XML::LibXML + suffix: authors/id/T/TO/TODDR/XML-LibXML-2.0213.tar.gz + sha256sum: 2af21c5d61ac34ea26a5fabf15ba5a5841e648f7189db3e33b6f28b5489802ab + - kind: patch_queue + path: patches/perl-xml-libxml diff --git a/elements/printer-app/perl-xml-namespace-support.bst b/elements/printer-app/perl-xml-namespace-support.bst new file mode 100644 index 0000000..2258d28 --- /dev/null +++ b/elements/printer-app/perl-xml-namespace-support.bst @@ -0,0 +1,14 @@ +kind: makemaker +description: Build XML::NamespaceSupport for Foomatic XML parsing. + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-make.bst + +depends: + - freedesktop-sdk.bst:components/perl.bst + +sources: + - kind: cpan + name: XML::NamespaceSupport + suffix: authors/id/P/PE/PERIGRIN/XML-NamespaceSupport-1.12.tar.gz + sha256sum: 47e995859f8dd0413aa3f22d350c4a62da652e854267aa0586ae544ae2bae5ef diff --git a/elements/printer-app/perl-xml-sax-base.bst b/elements/printer-app/perl-xml-sax-base.bst new file mode 100644 index 0000000..283edaf --- /dev/null +++ b/elements/printer-app/perl-xml-sax-base.bst @@ -0,0 +1,14 @@ +kind: makemaker +description: Build XML::SAX::Base for Foomatic XML parsing. + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-make.bst + +depends: + - freedesktop-sdk.bst:components/perl.bst + +sources: + - kind: cpan + name: XML::SAX::Base + suffix: authors/id/G/GR/GRANTM/XML-SAX-Base-1.09.tar.gz + sha256sum: 66cb355ba4ef47c10ca738bd35999723644386ac853abbeb5132841f5e8a2ad0 diff --git a/elements/printer-app/perl-xml-sax.bst b/elements/printer-app/perl-xml-sax.bst new file mode 100644 index 0000000..fea36ad --- /dev/null +++ b/elements/printer-app/perl-xml-sax.bst @@ -0,0 +1,16 @@ +kind: makemaker +description: Build XML::SAX for Foomatic XML parsing. + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-make.bst + +depends: + - printer-app/perl-xml-namespace-support.bst + - printer-app/perl-xml-sax-base.bst + - freedesktop-sdk.bst:components/perl.bst + +sources: + - kind: cpan + name: XML::SAX + suffix: authors/id/G/GR/GRANTM/XML-SAX-1.02.tar.gz + sha256sum: 4506c387043aa6a77b455f00f57409f3720aa7e553495ab2535263b4ed1ea12a diff --git a/patches/foomatic-db-engine/xml-database-without-dbi.patch b/patches/foomatic-db-engine/xml-database-without-dbi.patch index adc91cc..8516248 100644 --- a/patches/foomatic-db-engine/xml-database-without-dbi.patch +++ b/patches/foomatic-db-engine/xml-database-without-dbi.patch @@ -26,3 +26,15 @@ index 50990cc..cb46f5e 100644 $this->{'dbh'} = DBI->connect("dbi:SQLite:dbname=$sqlitedb","","")or warn $this->{'dbh'}->errstr; $this->{'dbh'}->do('PRAGMA synchronous = OFF;'); +diff --git a/lib/Foomatic/filters/xml/to.pm b/lib/Foomatic/filters/xml/to.pm +index 290c8c8..328a290 100644 +--- a/lib/Foomatic/filters/xml/to.pm ++++ b/lib/Foomatic/filters/xml/to.pm +@@ -4,7 +4,6 @@ use strict; + use warnings; + use Data::Dumper; + +-use DBI; + use Foomatic::filters::phonebook; + use Foomatic::util; + use Foomatic::DB; diff --git a/patches/perl-xml-libxml/use-system-libxml2.patch b/patches/perl-xml-libxml/use-system-libxml2.patch new file mode 100644 index 0000000..9aac89b --- /dev/null +++ b/patches/perl-xml-libxml/use-system-libxml2.patch @@ -0,0 +1,45 @@ +diff --git a/Makefile.PL b/Makefile.PL +--- a/Makefile.PL ++++ b/Makefile.PL +@@ -17,15 +17,12 @@ + + require 5.008001; + +-use Alien::Base::Wrapper qw( Alien::Libxml2 ); + use ExtUtils::MakeMaker; + use Config; + + my $SKIP_SAX_INSTALL = $ENV{SKIP_SAX_INSTALL}; + + my %ConfigReqs = ( +- "Alien::Libxml2" => '0.14', +- "Alien::Base::Wrapper" => 0, + "Config" => 0, + "ExtUtils::MakeMaker" => 0, + ); +@@ -67,15 +64,17 @@ + "warnings" => 0, + ); + +-my %xsbuild_concat = ( +- DEFINE => '-DHAVE_UTF8', +- OBJECT => '$(O_FILES)', ++my $cflags = `pkg-config --cflags libxml-2.0`; ++chomp $cflags; ++my $libs = `pkg-config --libs libxml-2.0`; ++chomp $libs; ++ ++my %xsbuild = ( ++ DEFINE => '-DHAVE_UTF8', ++ INC => $cflags, ++ LIBS => [$libs], ++ OBJECT => '$(O_FILES)', + ); +-my %xsbuild = Alien::Base::Wrapper->mm_args; # Might contain a definition of DEFINE, must thus concatenate. +-while (my ($k, $v) = each %xsbuild_concat) { +- my $base_val = $xsbuild{$k}; +- $xsbuild{$k} = (defined($base_val) ? ($base_val . ' ' . $v) : $v); +-} + + # Strip bogus -L/lib entries that Alien::Base::Wrapper sometimes injects + # (the directory does not exist on macOS and the linker warns about it). diff --git a/project.conf b/project.conf index de68ad2..16a10a5 100644 --- a/project.conf +++ b/project.conf @@ -39,9 +39,12 @@ source-caches: plugins: - origin: junction junction: plugins/buildstream-plugins-community.bst + elements: + - makemaker sources: - git_repo - patch_queue + - cpan sources: git_repo: From 88fe51e3f883c9a2ea9b46c476d7a5dad8ae89ab Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 03:53:19 -0400 Subject: [PATCH 12/22] build: generate core PPD archives Assisted-by: github-copilot/gpt-5.6-sol via pi --- elements/printer-app/core-payload.bst | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 elements/printer-app/core-payload.bst diff --git a/elements/printer-app/core-payload.bst b/elements/printer-app/core-payload.bst new file mode 100644 index 0000000..746dcad --- /dev/null +++ b/elements/printer-app/core-payload.bst @@ -0,0 +1,49 @@ +kind: manual +description: Generate the core cups-filters and Foomatic PPD archives. + +build-depends: + - printer-app/foomatic-db-engine.bst + - printer-app/pyppd.bst + - printer-app/perl-clone.bst + - printer-app/perl-xml-libxml.bst + - freedesktop-sdk.bst:public-stacks/runtime-gnu.bst + - freedesktop-sdk.bst:components/cups-filters.bst + - freedesktop-sdk.bst:components/foomatic-db.bst + - freedesktop-sdk.bst:components/gzip.bst + - freedesktop-sdk.bst:components/perl.bst + - freedesktop-sdk.bst:components/perl-xml-parser.bst + +config: + build-commands: + - | + mkdir -p payload/cupsfilters + cp -a /usr/share/ppd/cupsfilters/. payload/cupsfilters/ + pyppd -v -o cups-filters-ppds payload/cupsfilters + - | + mkdir -p payload/foomatic + cp -a /usr/share/foomatic/. payload/foomatic/ + rm -rf \ + payload/foomatic/db/source/PPD/Kyocera \ + payload/foomatic/db/source/PPD/Utax \ + payload/foomatic/db/source/PPD/Oce \ + payload/foomatic/db/source/PPD/*/PS + rm -f \ + payload/foomatic/db/source/driver/bjc800j.xml \ + payload/foomatic/db/source/driver/c2070.xml \ + payload/foomatic/db/source/driver/drv_x125.xml \ + payload/foomatic/db/source/driver/lm1100.xml \ + payload/foomatic/db/source/driver/lpstyl.xml \ + payload/foomatic/db/source/driver/ml85p.xml \ + payload/foomatic/db/source/driver/pbm2l2030.xml \ + payload/foomatic/db/source/driver/pbm2l7k.xml \ + payload/foomatic/db/source/driver/pbm2lwxl.xml \ + payload/foomatic/db/source/driver/pentaxpj.xml \ + payload/foomatic/db/source/driver/ppmtomd.xml + FOOMATICDB="$PWD/payload/foomatic" \ + foomatic-compiledb -j "%{max-jobs}" -t ppd -d payload/foomatic-ppds + pyppd -v -o foomatic-ppds payload/foomatic-ppds + pyppd -v -o manufacturer-ppds payload/foomatic/db/source/PPD + install-commands: + - install -D -m 0755 cups-filters-ppds "%{install-root}/usr/share/ppd/cups-filters-ppds" + - install -D -m 0755 foomatic-ppds "%{install-root}/usr/share/ppd/foomatic-ppds" + - install -D -m 0755 manufacturer-ppds "%{install-root}/usr/share/ppd/manufacturer-ppds" From e0ec8311e94b206848a48fc29f359ee02a633e8d Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 03:53:48 -0400 Subject: [PATCH 13/22] test: verify core driver payload Assisted-by: github-copilot/gpt-5.6-sol via pi --- Justfile | 3 ++ elements/printer-app/core-stack.bst | 5 +++ tests/core-payload.sh | 70 +++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100755 tests/core-payload.sh diff --git a/Justfile b/Justfile index 2fb01e1..57c3d96 100644 --- a/Justfile +++ b/Justfile @@ -64,5 +64,8 @@ export: verify-core: tests/core-appliance.sh +verify-payload: + tests/core-payload.sh + verify-cups-patch-chain: tests/cups-patch-chain.sh diff --git a/elements/printer-app/core-stack.bst b/elements/printer-app/core-stack.bst index 808f14d..70cce63 100644 --- a/elements/printer-app/core-stack.bst +++ b/elements/printer-app/core-stack.bst @@ -3,14 +3,19 @@ description: Core Ghostscript Printer Application appliance stack. depends: - printer-app/application.bst + - printer-app/core-payload.bst - printer-app/runtime-files.bst - freedesktop-sdk.bst:public-stacks/runtime-gnu.bst - freedesktop-sdk.bst:components/avahi.bst - freedesktop-sdk.bst:components/ca-certificates.bst - freedesktop-sdk.bst:components/catatonit.bst - freedesktop-sdk.bst:components/cups-daemon-only.bst + - freedesktop-sdk.bst:components/cups-filters.bst - freedesktop-sdk.bst:components/dbus.bst - freedesktop-sdk.bst:components/ghostscript.bst - freedesktop-sdk.bst:components/libcupsfilters.bst - freedesktop-sdk.bst:components/libppd.bst + - freedesktop-sdk.bst:components/mutool.bst + - freedesktop-sdk.bst:components/python3.bst + - freedesktop-sdk.bst:components/xz.bst - freedesktop-sdk.bst:components/tzdata.bst diff --git a/tests/core-payload.sh b/tests/core-payload.sh new file mode 100755 index 0000000..3fb2b77 --- /dev/null +++ b/tests/core-payload.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +image="ghcr.io/projectbluefin/ghostscript-printer-app:build" +name="ghostscript-printer-app-payload" +port="${PORT:-18010}" +state_dir="$(mktemp -d)" + +cleanup() { + podman rm -f "$name" >/dev/null 2>&1 || true + podman unshare rm -rf "$state_dir" +} +trap cleanup EXIT + +just build + +podman run --rm --entrypoint /usr/bin/bash "$image" -c ' + set -euo pipefail + test -L /usr/lib/ghostscript-printer-app + test "$(readlink /usr/lib/ghostscript-printer-app)" = /usr/lib/cups + test -f /usr/share/ghostscript-printer-app/testpage.ps + test -x /usr/bin/python3 + test -x /usr/bin/xz + + filters=(foomatic-rip gstoraster pdftops rastertoescpx rastertopclx) + for filter in "${filters[@]}"; do + path="/usr/lib/cups/filter/$filter" + test -x "$path" + dependencies="$(ldd "$path")" + [[ "$dependencies" != *"not found"* ]] + done + + application_dependencies="$(ldd /usr/bin/ghostscript-printer-app)" + [[ "$application_dependencies" != *"not found"* ]] + + archives=(cups-filters-ppds foomatic-ppds manufacturer-ppds) + for archive_name in "${archives[@]}"; do + archive="/usr/share/ppd/$archive_name" + test -x "$archive" + mapfile -t entries < <("$archive" list) + ((${#entries[@]} > 0)) + uri="${entries[0]%% *}" + uri="${uri#\"}" + uri="${uri%\"}" + ppd="$("$archive" cat "$uri")" + [[ "$ppd" == *"*PPD-Adobe:"* ]] + done +' + +chmod 0777 "$state_dir" +podman run -d \ + --name "$name" \ + --network host \ + -e PORT="$port" \ + -v "$state_dir:/var/lib/ghostscript-printer-app:Z" \ + "$image" >/dev/null + +for _ in $(seq 1 60); do + http="$(curl --fail --silent --show-error "http://127.0.0.1:${port}/" 2>/dev/null || true)" + https="$(curl --insecure --fail --silent --show-error "https://127.0.0.1:${port}/" 2>/dev/null || true)" + if [[ "$http" == *'Ghostscript Printer Application'* && "$https" == *'Ghostscript Printer Application'* ]]; then + printf 'OK: core driver payload and HTTPS are available\n' + exit 0 + fi + sleep 1 +done + +podman logs "$name" >&2 +printf 'FAIL: HTTP/HTTPS readiness was not reached\n' >&2 +exit 1 From d87ef56e6c45e8f91f4f6fe66f7aa9d0e1b133c9 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 05:26:48 -0400 Subject: [PATCH 14/22] fix: complete core FSDK print path Assisted-by: github-copilot/gpt-5.6-sol via pi --- elements/freedesktop-sdk.bst | 3 + elements/oci/ghostscript-printer-app.bst | 1 + ...stomize-cups-for-printer-application.patch | 9 ++ .../avoid-global-option-lock-after-fork.patch | 61 +++++++++++ tests/core-payload.sh | 103 +++++++++++++++--- tests/socket-sink.py | 16 +++ 6 files changed, 180 insertions(+), 13 deletions(-) create mode 100644 patches/libcupsfilters/avoid-global-option-lock-after-fork.patch create mode 100755 tests/socket-sink.py diff --git a/elements/freedesktop-sdk.bst b/elements/freedesktop-sdk.bst index 2d93ec9..930295e 100644 --- a/elements/freedesktop-sdk.bst +++ b/elements/freedesktop-sdk.bst @@ -8,6 +8,9 @@ sources: - kind: local path: patches/cups directory: patches/ghostscript-printer-app/cups + - kind: local + path: patches/libcupsfilters + directory: patches/ghostscript-printer-app/libcupsfilters - kind: patch_queue path: patches/freedesktop-sdk diff --git a/elements/oci/ghostscript-printer-app.bst b/elements/oci/ghostscript-printer-app.bst index f605f93..9cf7a30 100644 --- a/elements/oci/ghostscript-printer-app.bst +++ b/elements/oci/ghostscript-printer-app.bst @@ -23,6 +23,7 @@ config: >> /layer/etc/group rm -rf /layer/run/dbus /layer/run/avahi-daemon /layer/run/ghostscript-printer-app chmod 0777 /layer/run + install -d -m 1777 /layer/tmp install -d -m 0777 \ /layer/var/lib/ghostscript-printer-app \ /layer/var/lib/ghostscript-printer-app/ppd \ diff --git a/patches/freedesktop-sdk/0001-customize-cups-for-printer-application.patch b/patches/freedesktop-sdk/0001-customize-cups-for-printer-application.patch index 9eaa7a7..38b4857 100644 --- a/patches/freedesktop-sdk/0001-customize-cups-for-printer-application.patch +++ b/patches/freedesktop-sdk/0001-customize-cups-for-printer-application.patch @@ -52,3 +52,12 @@ diff --git a/elements/components/_private/avahi-base.bst b/elements/components/_ --with-distro=none --disable-libevent --disable-qt3 +diff --git a/elements/components/libcupsfilters.bst b/elements/components/libcupsfilters.bst +--- a/elements/components/libcupsfilters.bst ++++ b/elements/components/libcupsfilters.bst +@@ -34,3 +34,5 @@ sources: + exclude: + - '*rc*' + ref: 2.2.1-0-g2a00cf6aa4234e2e0ac91b9844ab8990d04c7089 ++- kind: patch_queue ++ path: patches/ghostscript-printer-app/libcupsfilters diff --git a/patches/libcupsfilters/avoid-global-option-lock-after-fork.patch b/patches/libcupsfilters/avoid-global-option-lock-after-fork.patch new file mode 100644 index 0000000..07fb5b1 --- /dev/null +++ b/patches/libcupsfilters/avoid-global-option-lock-after-fork.patch @@ -0,0 +1,61 @@ +diff --git a/cupsfilters/filter.c b/cupsfilters/filter.c +--- a/cupsfilters/filter.c ++++ b/cupsfilters/filter.c +@@ -972,7 +972,7 @@ + void *parameters) // I - Filter-specific parameters + { + cf_filter_external_t *params = (cf_filter_external_t *)parameters; +- int i; ++ int i, j; + int is_backend = 0; // Do we call a CUPS backend? + int pid, // Process ID of filter + stderrpid, // Process ID for stderr logging process +@@ -1107,12 +1107,35 @@ + // value in the filter data has priority + // + ++ // Do not use cupsAddOption() here. cfFilterExternal() can run in a ++ // cfFilterPOpen() child of a multi-threaded process, where libcups' ++ // inherited global string-pool lock is not safe to acquire. The option ++ // strings outlive this function, so a shallow array is sufficient. ++ all_options = (cups_option_t *)calloc(params->num_options + ++ data->num_options, ++ sizeof(cups_option_t)); ++ if ((params->num_options + data->num_options) > 0 && !all_options) ++ { ++ if (log) ++ log(ld, CF_LOGLEVEL_ERROR, ++ "cfFilterExternal: Memory allocation failed for options"); ++ status = 1; ++ goto out; ++ } ++ + for (i = 0, opt = params->options; i < params->num_options; i ++, opt ++) +- num_all_options = cupsAddOption(opt->name, opt->value, num_all_options, +- &all_options); ++ all_options[num_all_options ++] = *opt; + for (i = 0, opt = data->options; i < data->num_options; i ++, opt ++) +- num_all_options = cupsAddOption(opt->name, opt->value, num_all_options, +- &all_options); ++ { ++ for (j = 0; j < num_all_options; j ++) ++ if (strcasecmp(all_options[j].name, opt->name) == 0) ++ { ++ all_options[j] = *opt; ++ break; ++ } ++ if (j == num_all_options) ++ all_options[num_all_options ++] = *opt; ++ } + + // + // Create command line arguments for the CUPS filter +@@ -1589,7 +1612,7 @@ + out: + if (params->exec_mode < 0) + unlink(tmp_name); +- cupsFreeOptions(num_all_options, all_options); ++ free(all_options); + if (options_str) + free(options_str); + free(argv[0]); diff --git a/tests/core-payload.sh b/tests/core-payload.sh index 3fb2b77..1befa6f 100755 --- a/tests/core-payload.sh +++ b/tests/core-payload.sh @@ -5,10 +5,18 @@ image="ghcr.io/projectbluefin/ghostscript-printer-app:build" name="ghostscript-printer-app-payload" port="${PORT:-18010}" state_dir="$(mktemp -d)" +sink_port="$((port + 1000))" +output_file="$(mktemp)" +sink_pid="" cleanup() { podman rm -f "$name" >/dev/null 2>&1 || true + if [[ -n "$sink_pid" ]]; then + kill "$sink_pid" >/dev/null 2>&1 || true + wait "$sink_pid" 2>/dev/null || true + fi podman unshare rm -rf "$state_dir" + rm -f "$output_file" } trap cleanup EXIT @@ -22,16 +30,30 @@ podman run --rm --entrypoint /usr/bin/bash "$image" -c ' test -x /usr/bin/python3 test -x /usr/bin/xz - filters=(foomatic-rip gstoraster pdftops rastertoescpx rastertopclx) - for filter in "${filters[@]}"; do - path="/usr/lib/cups/filter/$filter" - test -x "$path" - dependencies="$(ldd "$path")" + executables=( + /usr/bin/ghostscript-printer-app + /usr/bin/gs + /usr/bin/python3 + /usr/bin/xz + /usr/lib/cups/backend/dnssd + /usr/lib/cups/backend/snmp + /usr/lib/cups/backend/socket + /usr/lib/cups/backend/usb + /usr/lib/cups/filter/foomatic-rip + /usr/lib/cups/filter/gstoraster + /usr/lib/cups/filter/pdftops + /usr/lib/cups/filter/rastertoescpx + /usr/lib/cups/filter/rastertopclx + ) + for executable in "${executables[@]}"; do + test -x "$executable" + dependencies="$(ldd "$executable")" [[ "$dependencies" != *"not found"* ]] done - application_dependencies="$(ldd /usr/bin/ghostscript-printer-app)" - [[ "$application_dependencies" != *"not found"* ]] + devices="$(gs -h 2>&1)" + [[ "$devices" == *"cups"* ]] + [[ "$devices" == *"pxlcolor"* ]] archives=(cups-filters-ppds foomatic-ppds manufacturer-ppds) for archive_name in "${archives[@]}"; do @@ -45,9 +67,24 @@ podman run --rm --entrypoint /usr/bin/bash "$image" -c ' ppd="$("$archive" cat "$uri")" [[ "$ppd" == *"*PPD-Adobe:"* ]] done -' + /usr/share/ppd/foomatic-ppds cat \ + foomatic-ppds:0/Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd \ + > /tmp/foomatic.ppd + if ! PPD=/tmp/foomatic.ppd /usr/lib/cups/filter/foomatic-rip \ + 1 nonroot core-conversion 1 "" \ + /usr/share/ghostscript-printer-app/testpage.ps \ + > /tmp/foomatic-output.pcl 2>/tmp/foomatic.log; then + cat /tmp/foomatic.log >&2 + exit 1 + fi + test -s /tmp/foomatic-output.pcl +' chmod 0777 "$state_dir" + +python3 tests/socket-sink.py "$sink_port" "$output_file" & +sink_pid=$! + podman run -d \ --name "$name" \ --network host \ @@ -55,16 +92,56 @@ podman run -d \ -v "$state_dir:/var/lib/ghostscript-printer-app:Z" \ "$image" >/dev/null +ready=0 for _ in $(seq 1 60); do http="$(curl --fail --silent --show-error "http://127.0.0.1:${port}/" 2>/dev/null || true)" https="$(curl --insecure --fail --silent --show-error "https://127.0.0.1:${port}/" 2>/dev/null || true)" if [[ "$http" == *'Ghostscript Printer Application'* && "$https" == *'Ghostscript Printer Application'* ]]; then - printf 'OK: core driver payload and HTTPS are available\n' - exit 0 + ready=1 + break fi sleep 1 done -podman logs "$name" >&2 -printf 'FAIL: HTTP/HTTPS readiness was not reached\n' >&2 -exit 1 +if [[ "$ready" -ne 1 ]]; then + podman logs "$name" >&2 + printf 'FAIL: HTTP/HTTPS readiness was not reached\n' >&2 + exit 1 +fi + +system_uri="ipp://127.0.0.1:${port}/ipp/system" +printer_uri="ipp://127.0.0.1:${port}/ipp/print/core-test" +podman exec "$name" ghostscript-printer-app \ + -u "$system_uri" \ + -d core-test \ + -m generic--pcl-6-pcl-xl-printer--pxlcolor-recommended-en \ + -v "cups:socket://127.0.0.1:${sink_port}" \ + add +podman exec "$name" ghostscript-printer-app \ + -u "$printer_uri" \ + -d core-test \ + submit /usr/share/ghostscript-printer-app/testpage.ps >/dev/null + +for _ in $(seq 1 120); do + [[ -s "$output_file" ]] && break + sleep 0.5 +done +if [[ ! -s "$output_file" ]]; then + podman exec "$name" ghostscript-printer-app -u "$printer_uri" jobs >&2 || true + podman exec "$name" cat /var/lib/ghostscript-printer-app/ghostscript-printer-app.log >&2 || true + printf 'FAIL: print job produced no socket output\n' >&2 + exit 1 +fi + +wait "$sink_pid" +sink_pid="" +python3 -c 'import pathlib, sys; assert pathlib.Path(sys.argv[1]).read_bytes().startswith(b"\x1b%-12345X")' "$output_file" + +jobs="" +for _ in $(seq 1 120); do + jobs="$(podman exec "$name" ghostscript-printer-app -u "$printer_uri" jobs)" + [[ "$jobs" == *"completed"* ]] && break + sleep 0.5 +done +[[ "$jobs" == *"completed"* ]] +printf 'OK: core driver payload, HTTPS, and print conversion are available\n' diff --git a/tests/socket-sink.py b/tests/socket-sink.py new file mode 100755 index 0000000..2be83b9 --- /dev/null +++ b/tests/socket-sink.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import socket +import sys + +port = int(sys.argv[1]) +output_path = sys.argv[2] + +with socket.socket() as listener: + listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + listener.bind(("127.0.0.1", port)) + listener.listen(1) + connection, _ = listener.accept() + connection.settimeout(30) + with connection, open(output_path, "wb") as output: + while data := connection.recv(65536): + output.write(data) From dac1122feab266ccdea3facad8736e470686cb72 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 05:59:07 -0400 Subject: [PATCH 15/22] fix: exercise the installed test page Assisted-by: github-copilot/gpt-5.6-sol via pi --- ghostscript-printer-app.c | 2 +- .../avoid-global-option-lock-after-fork.patch | 69 ++++++++++++++----- tests/core-payload.sh | 24 +++++-- 3 files changed, 70 insertions(+), 25 deletions(-) diff --git a/ghostscript-printer-app.c b/ghostscript-printer-app.c index 4aca074..08fd41e 100644 --- a/ghostscript-printer-app.c +++ b/ghostscript-printer-app.c @@ -43,7 +43,7 @@ // Test page -#define TESTPAGE "testpage.pdf" +#define TESTPAGE "testpage.ps" // diff --git a/patches/libcupsfilters/avoid-global-option-lock-after-fork.patch b/patches/libcupsfilters/avoid-global-option-lock-after-fork.patch index 07fb5b1..9b1caf0 100644 --- a/patches/libcupsfilters/avoid-global-option-lock-after-fork.patch +++ b/patches/libcupsfilters/avoid-global-option-lock-after-fork.patch @@ -6,14 +6,30 @@ diff --git a/cupsfilters/filter.c b/cupsfilters/filter.c { cf_filter_external_t *params = (cf_filter_external_t *)parameters; - int i; -+ int i, j; ++ int i, j, pass; int is_backend = 0; // Do we call a CUPS backend? int pid, // Process ID of filter stderrpid, // Process ID for stderr logging process -@@ -1107,12 +1107,35 @@ +@@ -996,7 +996,8 @@ + char job_id_str[16], + copies_str[16], + *options_str = NULL; +- cups_option_t *opt; ++ cups_option_t *opt, *source_options; ++ int num_source_options; + int status = 65536; + int wstatus; + cf_logfunc_t log = data->logfunc; +@@ -1107,12 +1108,45 @@ // value in the filter data has priority // +- for (i = 0, opt = params->options; i < params->num_options; i ++, opt ++) +- num_all_options = cupsAddOption(opt->name, opt->value, num_all_options, +- &all_options); +- for (i = 0, opt = data->options; i < data->num_options; i ++, opt ++) +- num_all_options = cupsAddOption(opt->name, opt->value, num_all_options, +- &all_options); + // Do not use cupsAddOption() here. cfFilterExternal() can run in a + // cfFilterPOpen() child of a multi-threaded process, where libcups' + // inherited global string-pool lock is not safe to acquire. The option @@ -30,27 +46,33 @@ diff --git a/cupsfilters/filter.c b/cupsfilters/filter.c + goto out; + } + - for (i = 0, opt = params->options; i < params->num_options; i ++, opt ++) -- num_all_options = cupsAddOption(opt->name, opt->value, num_all_options, -- &all_options); -+ all_options[num_all_options ++] = *opt; - for (i = 0, opt = data->options; i < data->num_options; i ++, opt ++) -- num_all_options = cupsAddOption(opt->name, opt->value, num_all_options, -- &all_options); ++ for (pass = 0; pass < 2; pass ++) + { -+ for (j = 0; j < num_all_options; j ++) -+ if (strcasecmp(all_options[j].name, opt->name) == 0) -+ { -+ all_options[j] = *opt; -+ break; -+ } -+ if (j == num_all_options) -+ all_options[num_all_options ++] = *opt; ++ source_options = pass ? data->options : params->options; ++ num_source_options = pass ? data->num_options : params->num_options; ++ ++ for (i = 0, opt = source_options; i < num_source_options; i ++, opt ++) ++ { ++ if (!opt->name || !opt->name[0] || !opt->value) ++ continue; ++ for (j = 0; j < num_all_options; j ++) ++ if (strcasecmp(all_options[j].name, opt->name) == 0 || ++ ((strcasecmp(opt->name, "cupsPrintQuality") == 0 || ++ strcasecmp(opt->name, "print-quality") == 0) && ++ (strcasecmp(all_options[j].name, "cupsPrintQuality") == 0 || ++ strcasecmp(all_options[j].name, "print-quality") == 0))) ++ { ++ all_options[j] = *opt; ++ break; ++ } ++ if (j == num_all_options) ++ all_options[num_all_options ++] = *opt; ++ } + } // // Create command line arguments for the CUPS filter -@@ -1589,7 +1612,7 @@ +@@ -1589,11 +1623,14 @@ out: if (params->exec_mode < 0) unlink(tmp_name); @@ -58,4 +80,13 @@ diff --git a/cupsfilters/filter.c b/cupsfilters/filter.c + free(all_options); if (options_str) free(options_str); - free(argv[0]); +- free(argv[0]); +- free(argv); ++ if (argv) ++ { ++ free(argv[0]); ++ free(argv); ++ } + for (i = 0; envp[i]; i ++) + free(envp[i]); + free(envp); diff --git a/tests/core-payload.sh b/tests/core-payload.sh index 1befa6f..30cf11a 100755 --- a/tests/core-payload.sh +++ b/tests/core-payload.sh @@ -7,6 +7,7 @@ port="${PORT:-18010}" state_dir="$(mktemp -d)" sink_port="$((port + 1000))" output_file="$(mktemp)" +cookie_file="$(mktemp)" sink_pid="" cleanup() { @@ -16,7 +17,7 @@ cleanup() { wait "$sink_pid" 2>/dev/null || true fi podman unshare rm -rf "$state_dir" - rm -f "$output_file" + rm -f "$output_file" "$cookie_file" } trap cleanup EXIT @@ -36,12 +37,18 @@ podman run --rm --entrypoint /usr/bin/bash "$image" -c ' /usr/bin/python3 /usr/bin/xz /usr/lib/cups/backend/dnssd + /usr/lib/cups/backend/ipp + /usr/lib/cups/backend/ipps + /usr/lib/cups/backend/lpd /usr/lib/cups/backend/snmp /usr/lib/cups/backend/socket /usr/lib/cups/backend/usb /usr/lib/cups/filter/foomatic-rip /usr/lib/cups/filter/gstoraster /usr/lib/cups/filter/pdftops + /usr/lib/cups/filter/rastertoepson + /usr/lib/cups/filter/rastertohp + /usr/lib/cups/filter/rastertolabel /usr/lib/cups/filter/rastertoescpx /usr/lib/cups/filter/rastertopclx ) @@ -117,10 +124,17 @@ podman exec "$name" ghostscript-printer-app \ -m generic--pcl-6-pcl-xl-printer--pxlcolor-recommended-en \ -v "cups:socket://127.0.0.1:${sink_port}" \ add -podman exec "$name" ghostscript-printer-app \ - -u "$printer_uri" \ - -d core-test \ - submit /usr/share/ghostscript-printer-app/testpage.ps >/dev/null +printer_page="$(curl --fail --silent --show-error \ + --cookie-jar "$cookie_file" \ + "http://127.0.0.1:${port}/core-test/")" +session="${printer_page#*name=\"session\" value=\"}" +session="${session%%\"*}" +[[ -n "$session" && "$session" != "$printer_page" ]] +curl --fail --silent --show-error \ + --cookie "$cookie_file" \ + --data-urlencode "session=$session" \ + --data 'action=print-test-page' \ + "http://127.0.0.1:${port}/core-test/" >/dev/null for _ in $(seq 1 120); do [[ -s "$output_file" ]] && break From 567c72240943f5368917435c402619af2ebb9623 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 05:59:22 -0400 Subject: [PATCH 16/22] docs: record core payload implementation Assisted-by: github-copilot/gpt-5.6-sol via pi --- .../plans/2026-09-16-fsdk-core-payload.md | 88 ++++++++++++------- 1 file changed, 55 insertions(+), 33 deletions(-) diff --git a/docs/superpowers/plans/2026-09-16-fsdk-core-payload.md b/docs/superpowers/plans/2026-09-16-fsdk-core-payload.md index 44f15e3..da8ea04 100644 --- a/docs/superpowers/plans/2026-09-16-fsdk-core-payload.md +++ b/docs/superpowers/plans/2026-09-16-fsdk-core-payload.md @@ -6,7 +6,7 @@ **Architecture:** Reuse FSDK's existing printing artifacts without rebuilding or duplicating them. Build only pyppd and the Foomatic XML compiler that FSDK does not ship, generate the same three self-extracting PPD archives as the current OCI contract, and compose those archives with FSDK's filters plus their measured runtime dependencies. Verify the exported image, not BuildStream source text. -**Tech Stack:** BuildStream 2, freedesktop-sdk 26.08rc.1, cups-filters 2.0.1, Foomatic DB 20240504, foomatic-db-engine 4.1.0, pyppd 1.1.0, Python 3, xz, Podman, socat. +**Tech Stack:** BuildStream 2, freedesktop-sdk 26.08rc.1, cups-filters 2.0.1, Foomatic DB 20240504, foomatic-db-engine 4.1.0, pyppd 1.1.0, Python 3, xz, Podman. ## Global Constraints @@ -28,9 +28,9 @@ **Interfaces:** - Consumes: FSDK Python, Perl, XML::Parser, curl, file, gzip, cups-filters, and Foomatic database artifacts. -- Produces: build-time `/usr/bin/pyppd` and `/usr/sbin/foomatic-compiledb` commands. +- Produces: build-time `/usr/bin/pyppd` and `/usr/bin/foomatic-compiledb` commands. -- [ ] **Step 1: Add the pyppd element** +- [x] **Step 1: Add the pyppd element** Create `elements/printer-app/pyppd.bst`: @@ -52,11 +52,11 @@ sources: ref: release-1-1-0-0-g29ccf6cf85781315a696774e7458a2f1f61aac57 ``` -- [ ] **Step 2: Make Foomatic's XML-only path independent of DBI** +- [x] **Step 2: Make Foomatic's XML-only path independent of DBI** Create `patches/foomatic-db-engine/xml-database-without-dbi.patch` that removes the unconditional `use DBI;` from `lib/Foomatic/DB.pm` and adds `require DBI;` only inside the MySQL and SQLite branches of `connect_to_mysql_db()`. The XML database path used by `foomatic-compiledb` must not require an unavailable SQL driver. -- [ ] **Step 3: Add the Foomatic engine element** +- [x] **Step 3: Add the Foomatic engine element** Create `elements/printer-app/foomatic-db-engine.bst` with immutable commit `e4e7b9cd28ba160428f82bc5234559d1f50e5c42`, the DBI patch queue, and these build dependencies: @@ -82,10 +82,12 @@ config: - PERL_INSTALLDIRS=vendor ./configure --prefix=/usr --libdir="%{libdir}" - make -j1 install-commands: - - make DESTDIR="%{install-root}" install + - make DESTDIR="$PWD/full-install" install + - install -D -m 0755 full-install/usr/sbin/foomatic-compiledb "%{install-root}/usr/bin/foomatic-compiledb" + - mkdir -p "%{install-root}/usr/lib" && cp -a full-install/usr/lib/perl5 "%{install-root}/usr/lib/" ``` -- [ ] **Step 4: Verify both tools build** +- [x] **Step 4: Verify both tools build** Run: @@ -94,9 +96,9 @@ just bst build printer-app/pyppd.bst printer-app/foomatic-db-engine.bst just bst artifact list-contents printer-app/pyppd.bst printer-app/foomatic-db-engine.bst ``` -Expected: the artifacts contain `/usr/bin/pyppd` and `/usr/sbin/foomatic-compiledb`; no second CUPS library is introduced. +Expected: the artifacts contain `/usr/bin/pyppd` and `/usr/bin/foomatic-compiledb`; no second CUPS library is introduced. -- [ ] **Step 5: Commit the tooling** +- [x] **Step 5: Commit the tooling** ```bash git add elements/printer-app/pyppd.bst elements/printer-app/foomatic-db-engine.bst patches/foomatic-db-engine/xml-database-without-dbi.patch @@ -112,11 +114,11 @@ git commit -m "build: add core PPD generation tools" -m "Assisted-by: github-cop - Consumes: FSDK cups-filters PPDs and Foomatic XML/manufacturer data plus the Task 1 generators. - Produces: executable `/usr/share/ppd/cups-filters-ppds`, `/usr/share/ppd/foomatic-ppds`, and `/usr/share/ppd/manufacturer-ppds` archives. -- [ ] **Step 1: Stage source payloads and generators** +- [x] **Step 1: Stage source payloads and generators** Create a `manual` element with build dependencies on `printer-app/pyppd.bst`, `printer-app/foomatic-db-engine.bst`, `freedesktop-sdk.bst:components/cups-filters.bst`, `freedesktop-sdk.bst:components/foomatic-db.bst`, and `freedesktop-sdk.bst:public-stacks/runtime-gnu.bst`. -- [ ] **Step 2: Generate the cups-filters archive** +- [x] **Step 2: Generate the cups-filters archive** The build commands must copy `/usr/share/ppd/cupsfilters` to `payload/cupsfilters` and run: @@ -124,7 +126,7 @@ The build commands must copy `/usr/share/ppd/cupsfilters` to `payload/cupsfilter pyppd -v -o cups-filters-ppds payload/cupsfilters ``` -- [ ] **Step 3: Generate the Foomatic archives** +- [x] **Step 3: Generate the Foomatic archives** Copy `/usr/share/foomatic` to `payload/foomatic`, remove PostScript manufacturer PPDs and the unsupported driver XML files carried by the current Snap contract: @@ -141,11 +143,11 @@ pyppd -v -o foomatic-ppds payload/foomatic-ppds pyppd -v -o manufacturer-ppds payload/foomatic/db/source/PPD ``` -- [ ] **Step 4: Install only generated archives** +- [x] **Step 4: Install only generated archives** Install the three executable archives with mode `0755` beneath `%{install-root}/usr/share/ppd`. Do not carry the Foomatic compiler, raw XML database, or pyppd package into this artifact. -- [ ] **Step 5: Build and inspect the payload artifact** +- [x] **Step 5: Build and inspect the payload artifact** Run: @@ -156,7 +158,7 @@ just bst artifact list-contents printer-app/core-payload.bst Expected: exactly the three executable archive files appear under `/usr/share/ppd`. -- [ ] **Step 6: Commit the generated-payload element** +- [x] **Step 6: Commit the generated-payload element** ```bash git add elements/printer-app/core-payload.bst @@ -174,17 +176,29 @@ git commit -m "build: generate core PPD archives" -m "Assisted-by: github-copilo - Consumes: `printer-app/core-payload.bst` and FSDK runtime components. - Produces: `just verify-payload`, proving driver archives, filters, interpreters, HTTPS, and shared-library closure inside the exported image. -- [ ] **Step 1: Write the failing image-level payload check** +- [x] **Step 1: Write the failing image-level payload check** Create `tests/core-payload.sh`. It must run `just build`, then assert from the image that: ```text /usr/lib/ghostscript-printer-app -> /usr/lib/cups +/usr/lib/cups/backend/dnssd +/usr/lib/cups/backend/ipp +/usr/lib/cups/backend/ipps +/usr/lib/cups/backend/lpd +/usr/lib/cups/backend/snmp +/usr/lib/cups/backend/socket +/usr/lib/cups/backend/usb /usr/lib/cups/filter/foomatic-rip /usr/lib/cups/filter/gstoraster /usr/lib/cups/filter/pdftops /usr/lib/cups/filter/rastertoescpx /usr/lib/cups/filter/rastertopclx +/usr/lib/cups/filter/rastertoepson +/usr/lib/cups/filter/rastertohp +/usr/lib/cups/filter/rastertolabel +/usr/bin/ghostscript-printer-app +/usr/bin/gs /usr/share/ghostscript-printer-app/testpage.ps /usr/share/ppd/cups-filters-ppds /usr/share/ppd/foomatic-ppds @@ -195,7 +209,7 @@ Create `tests/core-payload.sh`. It must run `just build`, then assert from the i For each archive, run `list`, capture its first URI, run `cat `, and require the extracted text to contain `*PPD-Adobe:`. Run this check now; it must fail because the payload is not yet composed. -- [ ] **Step 2: Add runtime components** +- [x] **Step 2: Add runtime components** Add these dependencies to `core-stack.bst`: @@ -209,15 +223,15 @@ Add these dependencies to `core-stack.bst`: Keep the existing Ghostscript, CUPS, libppd, and libcupsfilters dependencies. -- [ ] **Step 3: Verify HTTP and HTTPS** +- [x] **Step 3: Verify HTTP and HTTPS** The payload test starts the real image on host networking, waits for `Ghostscript Printer Application` over HTTP, and requires the same title over HTTPS with `curl --insecure`. -- [ ] **Step 4: Verify ELF closure** +- [x] **Step 4: Verify ELF closure** Inside the image, run `ldd` for the application and each core filter listed in Step 1. Fail if any output contains `not found`. -- [ ] **Step 5: Add the verification command** +- [x] **Step 5: Add the verification command** Add to `Justfile`: @@ -226,7 +240,7 @@ verify-payload: tests/core-payload.sh ``` -- [ ] **Step 6: Run and commit the runtime checks** +- [x] **Step 6: Run and commit the runtime checks** Run: @@ -245,35 +259,43 @@ git commit -m "test: verify core driver payload" -m "Assisted-by: github-copilot **Files:** - Modify: `tests/core-payload.sh` +- Modify: `ghostscript-printer-app.c` +- Modify: `elements/freedesktop-sdk.bst` +- Modify: `patches/freedesktop-sdk/0001-customize-cups-for-printer-application.patch` +- Create: `patches/libcupsfilters/avoid-global-option-lock-after-fork.patch` +- Create: `tests/socket-sink.py` **Interfaces:** -- Consumes: running Printer Application, `cups-filters-ppds:Generic-PDF_Printer-PDF.ppd`, repository test page, and host `socat`. -- Produces: non-empty printer-language output captured from a real submitted job. +- Consumes: running Printer Application, the generated Generic PCL 6/PCL XL Foomatic driver, repository test page, and a host Python socket sink. +- Produces: non-empty PCL XL output captured from a real submitted job. -- [ ] **Step 1: Add the socket-backed printer test** +- [x] **Step 1: Add the socket-backed printer test** Start a one-shot host sink before the container: ```bash -socat -u "TCP-LISTEN:${sink_port},reuseaddr" "OPEN:${output_file},creat,trunc" & +python3 tests/socket-sink.py "$sink_port" "$output_file" & sink_pid=$! ``` -After HTTP/HTTPS readiness, run inside the image: +After HTTP/HTTPS readiness, add a Generic PCL 6/PCL XL printer and invoke its built-in test-page action: ```bash -ghostscript-printer-app -u "ipp://127.0.0.1:${port}/ipp/system" add core-test \ - -m cups-filters-ppds:Generic-PDF_Printer-PDF.ppd \ - -v "cups:socket://127.0.0.1:${sink_port}" ghostscript-printer-app -u "ipp://127.0.0.1:${port}/ipp/system" \ - -d core-test submit /usr/share/ghostscript-printer-app/testpage.ps + -d core-test \ + -m generic--pcl-6-pcl-xl-printer--pxlcolor-recommended-en \ + -v "cups:socket://127.0.0.1:${sink_port}" add +curl --data 'action=print-test-page' \ + "http://127.0.0.1:${port}/core-test/" ``` -- [ ] **Step 2: Assert conversion output** +The application must request the installed `testpage.ps`. Patch FSDK's libcupsfilters 2.2.1 so `cfFilterExternal()` merges borrowed option records without calling libcups' globally locked string pool after `cfFilterPOpen()` forks. Preserve case-insensitive replacement and the `cupsPrintQuality`/`print-quality` alias rule. -Poll until `${output_file}` is non-empty, then require its first five bytes to be `%PDF-`. A successful CLI return without captured output is a failure. +- [x] **Step 2: Assert conversion output** -- [ ] **Step 3: Re-run all slice gates** +Poll until `${output_file}` is non-empty, wait for the sink and print job to complete, then require the PJL/PCL stream to start with `ESC%-12345X`. A successful CLI return without captured output is a failure. + +- [x] **Step 3: Re-run all slice gates** Run: From eb211e46a3834e965f6d4c6017186f75e27cd045 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 06:00:37 -0400 Subject: [PATCH 17/22] docs: complete core payload plan Assisted-by: github-copilot/gpt-5.6-sol via pi --- docs/superpowers/plans/2026-09-16-fsdk-core-payload.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-09-16-fsdk-core-payload.md b/docs/superpowers/plans/2026-09-16-fsdk-core-payload.md index da8ea04..8883bc3 100644 --- a/docs/superpowers/plans/2026-09-16-fsdk-core-payload.md +++ b/docs/superpowers/plans/2026-09-16-fsdk-core-payload.md @@ -311,6 +311,6 @@ git diff --check Expected: every command succeeds. -- [ ] **Step 4: Resolve and publish the slice** +- [x] **Step 4: Resolve and publish the slice** Set `.scratch/fsdk-container-modernization/issues/03-serve-core-ppd-filter-payload.md` to `resolved`, record exact successful commands and commit IDs, commit this completed plan, push `feat/fsdk-core-payload`, and open a pull request with base `feat/fsdk-core-app`. Do not publish an OCI release. From 4e95b99544dd69572dc107503a814d10d7e81c7c Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 06:09:08 -0400 Subject: [PATCH 18/22] docs: plan standalone FSDK raster drivers Assisted-by: github-copilot/gpt-5.6-sol via pi --- ...26-09-16-fsdk-standalone-raster-drivers.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-16-fsdk-standalone-raster-drivers.md diff --git a/docs/superpowers/plans/2026-09-16-fsdk-standalone-raster-drivers.md b/docs/superpowers/plans/2026-09-16-fsdk-standalone-raster-drivers.md new file mode 100644 index 0000000..8beb20b --- /dev/null +++ b/docs/superpowers/plans/2026-09-16-fsdk-standalone-raster-drivers.md @@ -0,0 +1,128 @@ +# FSDK Standalone Raster Drivers Implementation Plan + +> **For agentic workers:** Execute this plan task-by-task and keep each driver independently buildable. + +**Goal:** Add the c2050, cjet, min12xxw, and pnm2ppa legacy raster converters to the FSDK appliance, preserve the editable pnm2ppa configuration, and prove each executable produces deterministic printer-language output. + +**Architecture:** Add one focused BuildStream element per upstream source package, pinned to the Debian packaging tag commit already used by the existing OCI manifest. Apply the Debian patch series from each source tree before building. Install only runtime executables plus an immutable pnm2ppa configuration template, compose the four elements into `core-stack.bst`, and initialize the writable pnm2ppa configuration under the existing `/var/lib/ghostscript-printer-app` volume without overwriting user changes. + +**Tech Stack:** BuildStream 2, freedesktop-sdk 26.08rc.1, GNU make/autotools, Ghostscript, Bash, Podman. + +## Constraints + +- Preserve the current OCI driver versions: c2050 `debian/0.3-7`, cjet `debian/0.8.9-11`, min12xxw `debian/0.0.9-11`, and pnm2ppa `debian/1.13-14`. +- Pin each source to its peeled immutable commit, not a moving tag alone. +- Reuse FSDK build and runtime components; do not stage Debian packages or toolchains into the image. +- Install converter commands under `/usr/bin`, matching the Foomatic PPD command lines. +- Compile pnm2ppa against `/var/lib/ghostscript-printer-app/pnm2ppa/pnm2ppa.conf`, but ship the pristine seed at `/usr/share/ghostscript-printer-app/pnm2ppa.conf`. +- Initialize the writable pnm2ppa configuration only when absent. +- Do not publish an OCI release from this incomplete migration slice. + +--- + +### Task 1: Add pinned driver elements + +**Files:** +- Create: `elements/printer-app/c2050.bst` +- Create: `elements/printer-app/cjet.bst` +- Create: `elements/printer-app/min12xxw.bst` +- Create: `elements/printer-app/pnm2ppa.bst` +- Modify: `include/aliases.yml` + +- [ ] **Step 1: Add the Salsa source alias** + +Add `salsa: https://salsa.debian.org/` so all four git sources use a named mirrorable alias. + +- [ ] **Step 2: Add c2050** + +Use `salsa:printing-team/c2050.git`, tag `debian/0.3-7`, ref `e2ee50d2da58e552b1ac70ae0b4d8913436921e1`. Build with FSDK's make toolchain and install only `/usr/bin/c2050`. + +- [ ] **Step 3: Add cjet** + +Use `salsa:printing-team/cjet.git`, tag `debian/0.8.9-11`, ref `2de422f1b08cdf8d18a2e2ea8ae0b14751702fba`. Apply the source tree's ordered `debian/patches/series`, build with make, and install only `/usr/bin/cjet`. + +- [ ] **Step 4: Add min12xxw** + +Use `salsa:printing-team/min12xxw.git`, tag `debian/0.0.9-11`, ref `9a1fae0987e757c8d04e728eaec6e7adc1efca89`. Apply the Debian patch series, regenerate autotools files, configure for `/usr`, and install only `/usr/bin/min12xxw`. + +- [ ] **Step 5: Add pnm2ppa** + +Use `salsa:printing-team/pnm2ppa.git`, tag `debian/1.13-14`, ref `ca990cac53fb605d827f707e5f2d0c490639705c`. Apply the Debian patch series, set the default model to 710, regenerate autotools files, and configure with `--sysconfdir=/var/lib/ghostscript-printer-app/pnm2ppa`. Install `/usr/bin/pnm2ppa`, `/usr/bin/calibrate_ppa`, and the seed config at `/usr/share/ghostscript-printer-app/pnm2ppa.conf`. + +- [ ] **Step 6: Build and inspect each element** + +Run: + +```bash +just bst build printer-app/c2050.bst printer-app/cjet.bst printer-app/min12xxw.bst printer-app/pnm2ppa.bst +just bst artifact list-contents printer-app/c2050.bst printer-app/cjet.bst printer-app/min12xxw.bst printer-app/pnm2ppa.bst +``` + +Expected: each artifact contains only its intended runtime executable(s), with the pnm2ppa artifact also containing the immutable configuration template. + +### Task 2: Compose and initialize the drivers + +**Files:** +- Modify: `elements/printer-app/core-stack.bst` +- Modify: `files/container-entrypoint.sh` + +- [ ] **Step 1: Add the driver artifacts to the runtime stack** + +Add all four driver elements to `core-stack.bst`. Keep compiler, autotools, patch, and source data in build dependencies only. + +- [ ] **Step 2: Initialize pnm2ppa state once** + +Create `$state_dir/pnm2ppa` during startup. If `$state_dir/pnm2ppa/pnm2ppa.conf` is absent, copy the immutable template there. Never replace an existing file. + +- [ ] **Step 3: Verify persistence across restart** + +Start the image with a temporary state volume, verify the seeded config contains `version 710`, append a marker, restart with the same volume, and require the marker to remain. + +### Task 3: Exercise driver behavior and runtime closure + +**Files:** +- Create: `tests/standalone-raster-drivers.sh` +- Modify: `Justfile` + +- [ ] **Step 1: Add executable and ELF checks** + +Assert that `c2050`, `cjet`, `min12xxw`, `pnm2ppa`, and `calibrate_ppa` exist in the exported image. Run `ldd` for each ELF and fail on `not found`. Assert compiler and build tools are absent from the runtime. + +- [ ] **Step 2: Exercise c2050 and cjet** + +Generate deterministic raster/PCL input from the packaged PostScript test page using Ghostscript, pipe it through each converter, and require non-empty output with the expected protocol prefix or marker. + +- [ ] **Step 3: Exercise min12xxw and pnm2ppa** + +Generate valid page-sized PBM input and run `min12xxw` for model 1200W. Pipe `calibrate_ppa --center` into `pnm2ppa --bw` using the persisted configuration path. Require successful exits and non-empty outputs. + +- [ ] **Step 4: Add the verification command** + +Add `verify-raster-drivers` to `Justfile` and run the script against the assembled OCI image. + +- [ ] **Step 5: Run all slice gates** + +Run: + +```bash +just validate +just verify-core +just verify-payload +just verify-raster-drivers +just verify-cups-patch-chain +actionlint .github/workflows/*.yml +bash -n files/container-entrypoint.sh tests/*.sh +git diff --check +``` + +Expected: every command succeeds. + +### Task 4: Review and publish the slice + +- [ ] **Step 1: Review the full diff** + +Review from `feat/fsdk-core-payload` through the working tree for source pinning, architecture portability, runtime-only composition, pnm2ppa persistence, and meaningful driver execution. + +- [ ] **Step 2: Resolve and publish** + +Set `.scratch/fsdk-container-modernization/issues/04-restore-standalone-raster-drivers.md` to `resolved`, record successful commands and commit IDs, commit the completed plan, push `feat/fsdk-raster-drivers`, and open a pull request with base `feat/fsdk-core-payload`. From 6448be7f280443e7c7d3d321e5136b28db94bc4d Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 06:35:33 -0400 Subject: [PATCH 19/22] build: add standalone raster drivers Assisted-by: github-copilot/gpt-5.6-sol via pi --- elements/printer-app/c2050.bst | 19 +++++++ elements/printer-app/cjet.bst | 22 ++++++++ elements/printer-app/core-stack.bst | 4 ++ elements/printer-app/min12xxw.bst | 19 +++++++ elements/printer-app/pnm2ppa.bst | 22 ++++++++ files/container-entrypoint.sh | 5 +- include/aliases.yml | 1 + patches/c2050/modern-c.patch | 84 +++++++++++++++++++++++++++++ 8 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 elements/printer-app/c2050.bst create mode 100644 elements/printer-app/cjet.bst create mode 100644 elements/printer-app/min12xxw.bst create mode 100644 elements/printer-app/pnm2ppa.bst create mode 100644 patches/c2050/modern-c.patch diff --git a/elements/printer-app/c2050.bst b/elements/printer-app/c2050.bst new file mode 100644 index 0000000..b23d619 --- /dev/null +++ b/elements/printer-app/c2050.bst @@ -0,0 +1,19 @@ +kind: manual +description: Build the c2050 Lexmark raster converter. + +sources: + - kind: git_repo + url: salsa:printing-team/c2050.git + track: debian/0.3-7 + ref: debian/0.3-7-0-ge2ee50d2da58e552b1ac70ae0b4d8913436921e1 + - kind: patch_queue + path: patches/c2050 + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-make.bst + +config: + build-commands: + - make -j2 + install-commands: + - install -D -m 0755 c2050 "%{install-root}/usr/bin/c2050" diff --git a/elements/printer-app/cjet.bst b/elements/printer-app/cjet.bst new file mode 100644 index 0000000..83ec5bd --- /dev/null +++ b/elements/printer-app/cjet.bst @@ -0,0 +1,22 @@ +kind: manual +description: Build the cjet Canon CaPSL raster converter. + +sources: + - kind: git_repo + url: salsa:printing-team/cjet.git + track: debian/0.8.9-11 + ref: debian/0.8.9-11-0-g2de422f1b08cdf8d18a2e2ea8ae0b14751702fba + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-make.bst + - freedesktop-sdk.bst:components/patch.bst + +config: + build-commands: + - | + while read -r patch_file; do + patch -p1 < "debian/patches/$patch_file" + done < debian/patches/series + - make -j2 + install-commands: + - install -D -m 0755 cjet "%{install-root}/usr/bin/cjet" diff --git a/elements/printer-app/core-stack.bst b/elements/printer-app/core-stack.bst index 70cce63..97b3b7a 100644 --- a/elements/printer-app/core-stack.bst +++ b/elements/printer-app/core-stack.bst @@ -5,6 +5,10 @@ depends: - printer-app/application.bst - printer-app/core-payload.bst - printer-app/runtime-files.bst + - printer-app/c2050.bst + - printer-app/cjet.bst + - printer-app/min12xxw.bst + - printer-app/pnm2ppa.bst - freedesktop-sdk.bst:public-stacks/runtime-gnu.bst - freedesktop-sdk.bst:components/avahi.bst - freedesktop-sdk.bst:components/ca-certificates.bst diff --git a/elements/printer-app/min12xxw.bst b/elements/printer-app/min12xxw.bst new file mode 100644 index 0000000..370b3c2 --- /dev/null +++ b/elements/printer-app/min12xxw.bst @@ -0,0 +1,19 @@ +kind: manual +description: Build the min12xxw Minolta raster converter. + +sources: + - kind: git_repo + url: salsa:printing-team/min12xxw.git + track: debian/0.0.9-11 + ref: debian/0.0.9-11-0-g9a1fae0987e757c8d04e728eaec6e7adc1efca89 + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-autotools.bst + +config: + build-commands: + - autoreconf --install + - ./configure --prefix=/usr + - make -j2 + install-commands: + - install -D -m 0755 min12xxw "%{install-root}/usr/bin/min12xxw" diff --git a/elements/printer-app/pnm2ppa.bst b/elements/printer-app/pnm2ppa.bst new file mode 100644 index 0000000..c164c53 --- /dev/null +++ b/elements/printer-app/pnm2ppa.bst @@ -0,0 +1,22 @@ +kind: manual +description: Build the pnm2ppa HP PPA raster converter. + +sources: + - kind: git_repo + url: salsa:printing-team/pnm2ppa.git + track: debian/1.13-14 + ref: debian/1.13-14-0-gca990cac53fb605d827f707e5f2d0c490639705c + +build-depends: + - freedesktop-sdk.bst:public-stacks/buildsystem-autotools.bst + +config: + build-commands: + - sed -i 's/^version[[:space:]]\+0[[:space:]]\+/version 710 /' pnm2ppa.conf + - autoreconf --install + - ./configure --prefix=/usr --sysconfdir=/var/lib/ghostscript-printer-app/pnm2ppa + - make -j2 + install-commands: + - install -D -m 0755 pnm2ppa "%{install-root}/usr/bin/pnm2ppa" + - install -D -m 0755 calibrate_ppa "%{install-root}/usr/bin/calibrate_ppa" + - install -D -m 0644 pnm2ppa.conf "%{install-root}/usr/share/ghostscript-printer-app/pnm2ppa.conf" diff --git a/files/container-entrypoint.sh b/files/container-entrypoint.sh index 4c8eaf1..e527beb 100755 --- a/files/container-entrypoint.sh +++ b/files/container-entrypoint.sh @@ -7,10 +7,13 @@ if [[ -n "${PORT:-}" && ! "$PORT" =~ ^[0-9]+$ ]]; then fi state_dir=/var/lib/ghostscript-printer-app -mkdir -p "$state_dir/ppd" "$state_dir/spool" "$state_dir/usb" "$state_dir/cups/ssl" /run/dbus /run/avahi-daemon /run/ghostscript-printer-app +mkdir -p "$state_dir/ppd" "$state_dir/spool" "$state_dir/usb" "$state_dir/cups/ssl" "$state_dir/pnm2ppa" /run/dbus /run/avahi-daemon /run/ghostscript-printer-app if [[ ! -e "$state_dir/cups/snmp.conf" ]]; then cp /etc/cups/snmp.conf "$state_dir/cups/snmp.conf" fi +if [[ ! -e "$state_dir/pnm2ppa/pnm2ppa.conf" ]]; then + cp /usr/share/ghostscript-printer-app/pnm2ppa.conf "$state_dir/pnm2ppa/pnm2ppa.conf" +fi export BACKEND_DIR=/usr/lib/ghostscript-printer-app/backend export CUPS_SERVERBIN=/usr/lib/ghostscript-printer-app diff --git a/include/aliases.yml b/include/aliases.yml index bb2c85a..8bb9ced 100644 --- a/include/aliases.yml +++ b/include/aliases.yml @@ -2,3 +2,4 @@ aliases: github: https://github.com/ gitlab: https://gitlab.com/ pypi: https://files.pythonhosted.org/packages/ + salsa: https://salsa.debian.org/ diff --git a/patches/c2050/modern-c.patch b/patches/c2050/modern-c.patch new file mode 100644 index 0000000..4df9e9b --- /dev/null +++ b/patches/c2050/modern-c.patch @@ -0,0 +1,84 @@ +diff --git a/c2050.c b/c2050.c +--- a/c2050.c ++++ b/c2050.c +@@ -11,7 +11,6 @@ + */ + + #include +-#include + #include + #include + +@@ -56,14 +55,14 @@ + */ + void ClearBuffer(char *data, int bytes) + { +- register i; ++ register int i; + for(i = 0; i < bytes; data[i++] = 0); + } /* ClearBuffer */ + + /* + * Initialize a sweep buffer + */ +-SweepBuffer_Init (struct tSweepBuffer *SweepBuffer, int bytesize) ++void SweepBuffer_Init (struct tSweepBuffer *SweepBuffer, int bytesize) + { + SweepBuffer->bytepos = 0; + SweepBuffer->bitpos = 0; +@@ -123,9 +122,9 @@ + register int redleft = 0; + register int redright = 0; + int bstart = 0; +- while ((buffer[redleft] == 0) && (redleft < *bytesize)) redleft++; +- while ((buffer[*bytesize - 1 - redright] == 0) && +- (redright < *bytesize)) redright++; ++ while ((redleft < *bytesize) && (buffer[redleft] == 0)) redleft++; ++ while ((redright < *bytesize) && ++ (buffer[*bytesize - 1 - redright] == 0)) redright++; + *breite -= redleft / bytespercolumn + redright / bytespercolumn; + *leftmargin += redleft / bytespercolumn; + bstart = redleft - (redleft % bytespercolumn); +@@ -140,7 +139,7 @@ + void PrintSweep(char *buffer, char *header, int bytesize, int width, int leftmargin, FILE *out) + { + int bstart; +- register i; ++ register int i; + /* Remove zeros and set a margin instead. Faster Printing. */ + bstart = ReduceBytes(buffer, BYTES_PER_COLUMN, &leftmargin, + &width, &bytesize); +@@ -160,7 +159,7 @@ + + if (width > 0) { /* do not print empty sweeps */ + for(i=0; i Date: Wed, 16 Sep 2026 06:48:44 -0400 Subject: [PATCH 20/22] test: verify standalone raster drivers Assisted-by: github-copilot/gpt-5.6-sol via pi --- Justfile | 3 + tests/standalone-raster-drivers.sh | 109 +++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100755 tests/standalone-raster-drivers.sh diff --git a/Justfile b/Justfile index 57c3d96..0af750b 100644 --- a/Justfile +++ b/Justfile @@ -67,5 +67,8 @@ verify-core: verify-payload: tests/core-payload.sh +verify-raster-drivers: + tests/standalone-raster-drivers.sh + verify-cups-patch-chain: tests/cups-patch-chain.sh diff --git a/tests/standalone-raster-drivers.sh b/tests/standalone-raster-drivers.sh new file mode 100755 index 0000000..f51e21a --- /dev/null +++ b/tests/standalone-raster-drivers.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +set -euo pipefail + +image="ghcr.io/projectbluefin/ghostscript-printer-app:build" +name="ghostscript-printer-app-raster-drivers" +port="${PORT:-18020}" +state_dir="$(mktemp -d)" + +cleanup() { + podman rm -f "$name" >/dev/null 2>&1 || true + podman unshare rm -rf "$state_dir" +} +trap cleanup EXIT +wait_for_http() { + for _ in $(seq 1 60); do + curl --fail --silent --show-error "http://127.0.0.1:${port}/" >/dev/null 2>&1 && return 0 + sleep 1 + done + podman logs "$name" >&2 + return 1 +} + +just build + +podman run --rm --entrypoint /usr/bin/bash "$image" -c ' + set -euo pipefail + executables=(c2050 cjet min12xxw pnm2ppa calibrate_ppa) + for executable in "${executables[@]}"; do + path="/usr/bin/$executable" + test -x "$path" + dependencies="$(ldd "$path")" + [[ "$dependencies" != *"not found"* ]] + done + test -f /usr/share/ghostscript-printer-app/pnm2ppa.conf + ! command -v cc + ! command -v gcc + ! command -v make + ! command -v autoconf + + foomatic_entries="$(/usr/share/ppd/foomatic-ppds list)" + for driver in c2050 cjet min12xxw pnm2ppa; do + [[ "$foomatic_entries" == *"-${driver}.ppd"* ]] + done + printf "OK: driver executables and PPD entries are present\n" + + gs -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=bitcmyk -g2480x3507 -r300 \ + -sOutputFile=/tmp/c2050.cmyk \ + /usr/share/ghostscript-printer-app/testpage.ps + c2050 /tmp/c2050.prn + test -s /tmp/c2050.prn + printf "OK: c2050 conversion\n" + + gs -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=ljet4 -r300 \ + -sOutputFile=/tmp/cjet.pcl \ + /usr/share/ghostscript-printer-app/testpage.ps + cjet -q /tmp/cjet.prn + test -s /tmp/cjet.prn + printf "OK: cjet conversion\n" + + gs -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=pbmraw -r600 \ + -sOutputFile=/tmp/min12xxw.pbm \ + /usr/share/ghostscript-printer-app/testpage.ps + min12xxw -m 1200W /tmp/min12xxw.prn + test -s /tmp/min12xxw.prn + printf "OK: min12xxw conversion\n" +' + +chmod 0777 "$state_dir" +podman run -d \ + --name "$name" \ + --network host \ + -e PORT="$port" \ + -v "$state_dir:/var/lib/ghostscript-printer-app:Z" \ + "$image" >/dev/null +wait_for_http +podman exec "$name" /usr/bin/bash -c ' + set -euo pipefail + config=/var/lib/ghostscript-printer-app/pnm2ppa/pnm2ppa.conf + test -f "$config" + while IFS= read -r line; do + if [[ "$line" == version\ 710* ]]; then + config_initialized=1 + fi + done < "$config" + [[ "${config_initialized:-0}" == 1 ]] + calibrate_ppa --center > /tmp/pnm2ppa.ppm || test -s /tmp/pnm2ppa.ppm + pnm2ppa --bw -i /tmp/pnm2ppa.ppm -o /tmp/pnm2ppa.prn + test -s /tmp/pnm2ppa.prn + printf "OK: pnm2ppa conversion and initial configuration\n" + printf "# persistence-probe\n" >> "$config" +' + +podman stop --time 15 "$name" >/dev/null +podman rm "$name" >/dev/null +podman run -d \ + --name "$name" \ + --network host \ + -e PORT="$port" \ + -v "$state_dir:/var/lib/ghostscript-printer-app:Z" \ + "$image" >/dev/null +wait_for_http +podman exec "$name" /usr/bin/bash -c ' + while IFS= read -r line; do + [[ "$line" == "# persistence-probe" ]] && exit 0 + done < /var/lib/ghostscript-printer-app/pnm2ppa/pnm2ppa.conf + exit 1 +' + +printf 'OK: standalone raster drivers execute and pnm2ppa state persists\n' From 6b8613a9163a181bb16a9b430b86a816a9140049 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 07:06:55 -0400 Subject: [PATCH 21/22] test: harden raster driver verification Assisted-by: github-copilot/gpt-5.6-sol via pi --- elements/printer-app/pnm2ppa.bst | 2 +- tests/standalone-raster-drivers.sh | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/elements/printer-app/pnm2ppa.bst b/elements/printer-app/pnm2ppa.bst index c164c53..e2b0efc 100644 --- a/elements/printer-app/pnm2ppa.bst +++ b/elements/printer-app/pnm2ppa.bst @@ -13,7 +13,7 @@ build-depends: config: build-commands: - sed -i 's/^version[[:space:]]\+0[[:space:]]\+/version 710 /' pnm2ppa.conf - - autoreconf --install + - autoreconf --force --install - ./configure --prefix=/usr --sysconfdir=/var/lib/ghostscript-printer-app/pnm2ppa - make -j2 install-commands: diff --git a/tests/standalone-raster-drivers.sh b/tests/standalone-raster-drivers.sh index f51e21a..31045ef 100755 --- a/tests/standalone-raster-drivers.sh +++ b/tests/standalone-raster-drivers.sh @@ -37,6 +37,16 @@ podman run --rm --entrypoint /usr/bin/bash "$image" -c ' ! command -v make ! command -v autoconf + assert_output() { + local first="$1" second="$2" byte_count="$3" expected="$4" + local first_hash second_hash + test -s "$first" + [[ "$(od -An -tx1 -N "$byte_count" "$first")" == "$expected" ]] + read -r first_hash _ < <(sha256sum "$first") + read -r second_hash _ < <(sha256sum "$second") + [[ "$first_hash" == "$second_hash" ]] + } + foomatic_entries="$(/usr/share/ppd/foomatic-ppds list)" for driver in c2050 cjet min12xxw pnm2ppa; do [[ "$foomatic_entries" == *"-${driver}.ppd"* ]] @@ -47,21 +57,24 @@ podman run --rm --entrypoint /usr/bin/bash "$image" -c ' -sOutputFile=/tmp/c2050.cmyk \ /usr/share/ghostscript-printer-app/testpage.ps c2050 /tmp/c2050.prn - test -s /tmp/c2050.prn + c2050 /tmp/c2050-repeat.prn + assert_output /tmp/c2050.prn /tmp/c2050-repeat.prn 3 " 1b 2a 80" printf "OK: c2050 conversion\n" - gs -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=ljet4 -r300 \ + gs -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=ljet3 -r300 \ -sOutputFile=/tmp/cjet.pcl \ /usr/share/ghostscript-printer-app/testpage.ps cjet -q /tmp/cjet.prn - test -s /tmp/cjet.prn + cjet -q /tmp/cjet-repeat.prn + assert_output /tmp/cjet.prn /tmp/cjet-repeat.prn 4 " 1b 3b 1b 3c" printf "OK: cjet conversion\n" gs -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=pbmraw -r600 \ -sOutputFile=/tmp/min12xxw.pbm \ /usr/share/ghostscript-printer-app/testpage.ps min12xxw -m 1200W /tmp/min12xxw.prn - test -s /tmp/min12xxw.prn + min12xxw -m 1200W /tmp/min12xxw-repeat.prn + assert_output /tmp/min12xxw.prn /tmp/min12xxw-repeat.prn 4 " 1b 40 00 02" printf "OK: min12xxw conversion\n" ' @@ -85,7 +98,12 @@ podman exec "$name" /usr/bin/bash -c ' [[ "${config_initialized:-0}" == 1 ]] calibrate_ppa --center > /tmp/pnm2ppa.ppm || test -s /tmp/pnm2ppa.ppm pnm2ppa --bw -i /tmp/pnm2ppa.ppm -o /tmp/pnm2ppa.prn + pnm2ppa --bw -i /tmp/pnm2ppa.ppm -o /tmp/pnm2ppa-repeat.prn test -s /tmp/pnm2ppa.prn + [[ "$(od -An -tx1 -N 4 /tmp/pnm2ppa.prn)" == " 24 01 00 18" ]] + read -r first_hash _ < <(sha256sum /tmp/pnm2ppa.prn) + read -r second_hash _ < <(sha256sum /tmp/pnm2ppa-repeat.prn) + [[ "$first_hash" == "$second_hash" ]] printf "OK: pnm2ppa conversion and initial configuration\n" printf "# persistence-probe\n" >> "$config" ' From ccace12e1e7139677296247294bf5aa35ab33ba5 Mon Sep 17 00:00:00 2001 From: castrojo Date: Wed, 16 Sep 2026 07:08:56 -0400 Subject: [PATCH 22/22] docs: complete standalone raster plan Assisted-by: github-copilot/gpt-5.6-sol via pi --- ...26-09-16-fsdk-standalone-raster-drivers.md | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/docs/superpowers/plans/2026-09-16-fsdk-standalone-raster-drivers.md b/docs/superpowers/plans/2026-09-16-fsdk-standalone-raster-drivers.md index 8beb20b..8ba0793 100644 --- a/docs/superpowers/plans/2026-09-16-fsdk-standalone-raster-drivers.md +++ b/docs/superpowers/plans/2026-09-16-fsdk-standalone-raster-drivers.md @@ -28,28 +28,29 @@ - Create: `elements/printer-app/min12xxw.bst` - Create: `elements/printer-app/pnm2ppa.bst` - Modify: `include/aliases.yml` +- Create: `patches/c2050/modern-c.patch` -- [ ] **Step 1: Add the Salsa source alias** +- [x] **Step 1: Add the Salsa source alias** Add `salsa: https://salsa.debian.org/` so all four git sources use a named mirrorable alias. -- [ ] **Step 2: Add c2050** +- [x] **Step 2: Add c2050** -Use `salsa:printing-team/c2050.git`, tag `debian/0.3-7`, ref `e2ee50d2da58e552b1ac70ae0b4d8913436921e1`. Build with FSDK's make toolchain and install only `/usr/bin/c2050`. +Use `salsa:printing-team/c2050.git`, tag `debian/0.3-7`, ref `e2ee50d2da58e552b1ac70ae0b4d8913436921e1`. Apply a narrow compatibility patch for explicit C declarations and the two discovered raster-buffer overreads, build with FSDK's make toolchain, and install only `/usr/bin/c2050`. -- [ ] **Step 3: Add cjet** +- [x] **Step 3: Add cjet** Use `salsa:printing-team/cjet.git`, tag `debian/0.8.9-11`, ref `2de422f1b08cdf8d18a2e2ea8ae0b14751702fba`. Apply the source tree's ordered `debian/patches/series`, build with make, and install only `/usr/bin/cjet`. -- [ ] **Step 4: Add min12xxw** +- [x] **Step 4: Add min12xxw** -Use `salsa:printing-team/min12xxw.git`, tag `debian/0.0.9-11`, ref `9a1fae0987e757c8d04e728eaec6e7adc1efca89`. Apply the Debian patch series, regenerate autotools files, configure for `/usr`, and install only `/usr/bin/min12xxw`. +Use `salsa:printing-team/min12xxw.git`, tag `debian/0.0.9-11`, ref `9a1fae0987e757c8d04e728eaec6e7adc1efca89`. The tagged tree already carries its Debian source fixes; regenerate autotools files, configure for `/usr`, and install only `/usr/bin/min12xxw`. -- [ ] **Step 5: Add pnm2ppa** +- [x] **Step 5: Add pnm2ppa** -Use `salsa:printing-team/pnm2ppa.git`, tag `debian/1.13-14`, ref `ca990cac53fb605d827f707e5f2d0c490639705c`. Apply the Debian patch series, set the default model to 710, regenerate autotools files, and configure with `--sysconfdir=/var/lib/ghostscript-printer-app/pnm2ppa`. Install `/usr/bin/pnm2ppa`, `/usr/bin/calibrate_ppa`, and the seed config at `/usr/share/ghostscript-printer-app/pnm2ppa.conf`. +Use `salsa:printing-team/pnm2ppa.git`, tag `debian/1.13-14`, ref `ca990cac53fb605d827f707e5f2d0c490639705c`. The tagged tree already carries its Debian source fixes; set the default model to 710, regenerate autotools files, and configure with `--sysconfdir=/var/lib/ghostscript-printer-app/pnm2ppa`. Install `/usr/bin/pnm2ppa`, `/usr/bin/calibrate_ppa`, and the seed config at `/usr/share/ghostscript-printer-app/pnm2ppa.conf`. -- [ ] **Step 6: Build and inspect each element** +- [x] **Step 6: Build and inspect each element** Run: @@ -66,15 +67,15 @@ Expected: each artifact contains only its intended runtime executable(s), with t - Modify: `elements/printer-app/core-stack.bst` - Modify: `files/container-entrypoint.sh` -- [ ] **Step 1: Add the driver artifacts to the runtime stack** +- [x] **Step 1: Add the driver artifacts to the runtime stack** Add all four driver elements to `core-stack.bst`. Keep compiler, autotools, patch, and source data in build dependencies only. -- [ ] **Step 2: Initialize pnm2ppa state once** +- [x] **Step 2: Initialize pnm2ppa state once** Create `$state_dir/pnm2ppa` during startup. If `$state_dir/pnm2ppa/pnm2ppa.conf` is absent, copy the immutable template there. Never replace an existing file. -- [ ] **Step 3: Verify persistence across restart** +- [x] **Step 3: Verify persistence across restart** Start the image with a temporary state volume, verify the seeded config contains `version 710`, append a marker, restart with the same volume, and require the marker to remain. @@ -84,23 +85,23 @@ Start the image with a temporary state volume, verify the seeded config contains - Create: `tests/standalone-raster-drivers.sh` - Modify: `Justfile` -- [ ] **Step 1: Add executable and ELF checks** +- [x] **Step 1: Add executable and ELF checks** Assert that `c2050`, `cjet`, `min12xxw`, `pnm2ppa`, and `calibrate_ppa` exist in the exported image. Run `ldd` for each ELF and fail on `not found`. Assert compiler and build tools are absent from the runtime. -- [ ] **Step 2: Exercise c2050 and cjet** +- [x] **Step 2: Exercise c2050 and cjet** Generate deterministic raster/PCL input from the packaged PostScript test page using Ghostscript, pipe it through each converter, and require non-empty output with the expected protocol prefix or marker. -- [ ] **Step 3: Exercise min12xxw and pnm2ppa** +- [x] **Step 3: Exercise min12xxw and pnm2ppa** -Generate valid page-sized PBM input and run `min12xxw` for model 1200W. Pipe `calibrate_ppa --center` into `pnm2ppa --bw` using the persisted configuration path. Require successful exits and non-empty outputs. +Generate valid page-sized PBM input and run `min12xxw` for model 1200W. Generate pnm2ppa's documented calibration image, accepting its legacy non-zero status only when it produced the image, then run `pnm2ppa --bw` using the persisted configuration path. Require non-empty driver output. -- [ ] **Step 4: Add the verification command** +- [x] **Step 4: Add the verification command** Add `verify-raster-drivers` to `Justfile` and run the script against the assembled OCI image. -- [ ] **Step 5: Run all slice gates** +- [x] **Step 5: Run all slice gates** Run: @@ -119,10 +120,10 @@ Expected: every command succeeds. ### Task 4: Review and publish the slice -- [ ] **Step 1: Review the full diff** +- [x] **Step 1: Review the full diff** Review from `feat/fsdk-core-payload` through the working tree for source pinning, architecture portability, runtime-only composition, pnm2ppa persistence, and meaningful driver execution. -- [ ] **Step 2: Resolve and publish** +- [x] **Step 2: Resolve and publish** Set `.scratch/fsdk-container-modernization/issues/04-restore-standalone-raster-drivers.md` to `resolved`, record successful commands and commit IDs, commit the completed plan, push `feat/fsdk-raster-drivers`, and open a pull request with base `feat/fsdk-core-payload`.