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..0af750b 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,34 @@ 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-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/docs/skills/fsdk-cups-patching.md b/docs/skills/fsdk-cups-patching.md index ef01c73..2a93473 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. 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 @@ -49,6 +52,11 @@ 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`. +- 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 @@ -58,3 +66,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. 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..455025f --- /dev/null +++ b/docs/superpowers/plans/2026-09-16-fsdk-core-appliance.md @@ -0,0 +1,383 @@ +# 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 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' +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 ' + 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 + 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` +- 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` + +**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 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 without a `--user` override using host networking and a temporary state volume, require an application-specific HTTP title, then execute these checks: + +```bash +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 +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, 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" == avahi-daemon ]]; then + kill -TERM "${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 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** + +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. + +- [x] **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. 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..8883bc3 --- /dev/null +++ b/docs/superpowers/plans/2026-09-16-fsdk-core-payload.md @@ -0,0 +1,316 @@ +# 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. + +## 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/bin/foomatic-compiledb` commands. + +- [x] **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 +``` + +- [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. + +- [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: + +```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="$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/" +``` + +- [x] **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/bin/foomatic-compiledb`; no second CUPS library is introduced. + +- [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 +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. + +- [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`. + +- [x] **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 +``` + +- [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: + +```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 +``` + +- [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. + +- [x] **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`. + +- [x] **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. + +- [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 +/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. + +- [x] **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. + +- [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`. + +- [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`. + +- [x] **Step 5: Add the verification command** + +Add to `Justfile`: + +```just +verify-payload: + tests/core-payload.sh +``` + +- [x] **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` +- 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, 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. + +- [x] **Step 1: Add the socket-backed printer test** + +Start a one-shot host sink before the container: + +```bash +python3 tests/socket-sink.py "$sink_port" "$output_file" & +sink_pid=$! +``` + +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" \ + -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/" +``` + +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. + +- [x] **Step 2: Assert conversion output** + +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: + +```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. + +- [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. 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..8ba0793 --- /dev/null +++ b/docs/superpowers/plans/2026-09-16-fsdk-standalone-raster-drivers.md @@ -0,0 +1,129 @@ +# 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` +- Create: `patches/c2050/modern-c.patch` + +- [x] **Step 1: Add the Salsa source alias** + +Add `salsa: https://salsa.debian.org/` so all four git sources use a named mirrorable alias. + +- [x] **Step 2: Add 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`. + +- [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`. + +- [x] **Step 4: Add 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`. + +- [x] **Step 5: Add pnm2ppa** + +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`. + +- [x] **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` + +- [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. + +- [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. + +- [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. + +### Task 3: Exercise driver behavior and runtime closure + +**Files:** +- Create: `tests/standalone-raster-drivers.sh` +- Modify: `Justfile` + +- [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. + +- [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. + +- [x] **Step 3: Exercise min12xxw and pnm2ppa** + +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. + +- [x] **Step 4: Add the verification command** + +Add `verify-raster-drivers` to `Justfile` and run the script against the assembled OCI image. + +- [x] **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 + +- [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. + +- [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`. 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 new file mode 100644 index 0000000..9cf7a30 --- /dev/null +++ b/elements/oci/ghostscript-printer-app.bst @@ -0,0 +1,58 @@ +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' \ + 'root:x:0:0:root:/root:/usr/bin/bash' \ + 'nonroot:x:65532:65532:Printer Application:/var/lib/ghostscript-printer-app:/usr/sbin/nologin' \ + >> /layer/etc/passwd + printf '%s\n' \ + 'root:x:0:' \ + '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 1777 /layer/tmp + 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 '/messagebus<\/user>/d; s///g' /layer/usr/share/dbus-1/system.conf + fi + 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 <&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" "$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 +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 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 +} +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+=("$!") +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+=("$!") + +if wait -n "${children[@]}"; then + status=1 +else + status=$? +fi +stop_children +trap - TERM INT EXIT +exit "$status" 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/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{'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;'); +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/freedesktop-sdk/0001-customize-cups-for-printer-application.patch b/patches/freedesktop-sdk/0001-customize-cups-for-printer-application.patch index 39e7ae6..38b4857 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,24 @@ 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/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..9b1caf0 --- /dev/null +++ b/patches/libcupsfilters/avoid-global-option-lock-after-fork.patch @@ -0,0 +1,92 @@ +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, pass; + int is_backend = 0; // Do we call a CUPS backend? + int pid, // Process ID of filter + stderrpid, // Process ID for stderr logging process +@@ -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 ++ // 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 (pass = 0; pass < 2; pass ++) ++ { ++ 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,11 +1623,14 @@ + 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]); +- free(argv); ++ if (argv) ++ { ++ free(argv[0]); ++ free(argv); ++ } + for (i = 0; envp[i]; i ++) + free(envp[i]); + free(envp); 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/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 97d7e90..16a10a5 100644 --- a/project.conf +++ b/project.conf @@ -14,15 +14,37 @@ options: - aarch64 - x86_64 +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" + 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 + elements: + - makemaker sources: - git_repo - patch_queue + - cpan sources: git_repo: diff --git a/tests/core-appliance.sh b/tests/core-appliance.sh new file mode 100755 index 0000000..a28c47e --- /dev/null +++ b/tests/core-appliance.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +set -euo pipefail + +image="ghcr.io/projectbluefin/ghostscript-printer-app:build" +name="ghostscript-printer-app-smoke" +failure_name="ghostscript-printer-app-child-failure" +invalid_name="ghostscript-printer-app-invalid-port" +port="${PORT:-18000}" +failure_port="$((port + 1))" +state_dir="$(mktemp -d)" + +cleanup() { + podman rm -f "$name" "$failure_name" "$invalid_name" >/dev/null 2>&1 || true + podman unshare rm -rf "$state_dir" +} +trap cleanup EXIT + +wait_for_http() { + local target_port="$1" + local response + for _ in $(seq 1 60); do + 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 + done + return 1 +} + +just build +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 "$port" +podman exec "$name" /usr/bin/bash -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 + 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" +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 + 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 \ + -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" == avahi-daemon ]]; then + kill -TERM "${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' diff --git a/tests/core-payload.sh b/tests/core-payload.sh new file mode 100755 index 0000000..30cf11a --- /dev/null +++ b/tests/core-payload.sh @@ -0,0 +1,161 @@ +#!/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)" +sink_port="$((port + 1000))" +output_file="$(mktemp)" +cookie_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" "$cookie_file" +} +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 + + executables=( + /usr/bin/ghostscript-printer-app + /usr/bin/gs + /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 + ) + for executable in "${executables[@]}"; do + test -x "$executable" + dependencies="$(ldd "$executable")" + [[ "$dependencies" != *"not found"* ]] + done + + devices="$(gs -h 2>&1)" + [[ "$devices" == *"cups"* ]] + [[ "$devices" == *"pxlcolor"* ]] + + 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 + + /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 \ + -e PORT="$port" \ + -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 + ready=1 + break + fi + sleep 1 +done + +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 +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 + 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) diff --git a/tests/standalone-raster-drivers.sh b/tests/standalone-raster-drivers.sh new file mode 100755 index 0000000..31045ef --- /dev/null +++ b/tests/standalone-raster-drivers.sh @@ -0,0 +1,127 @@ +#!/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 + + 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"* ]] + 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 + 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=ljet3 -r300 \ + -sOutputFile=/tmp/cjet.pcl \ + /usr/share/ghostscript-printer-app/testpage.ps + cjet -q /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 + 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" +' + +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 + 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" +' + +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'