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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/no-vendor-blob.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: no-vendor-blob

# reMarkable's libqsgepaper.so must never be redistributed by this repository.
# quill/ links against it, but each user extracts it from their own device.
# This job fails the build if the library — or anything under the ignored
# vendor/build directories — ever becomes tracked.

on:
push:
branches: ["**"]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
check:
name: No proprietary vendor artifacts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Full history: a blob added in an earlier commit on this branch is
# still a leak, even if a later commit removed it.
fetch-depth: 0

- name: Check working tree
run: ./scripts/ci/check-no-vendor-blob.sh

- name: Check full history
run: |
# Any object ever named libqsgepaper.so, anywhere in reachable
# history. Documentation under quill/vendor/ is legitimate (it tells
# users how to fetch the library themselves), so match the library
# filename rather than the directory, and confirm by size — the real
# library is ~350KB, a README is under a kilobyte.
fail=0
while read -r sha path; do
[ -n "${path:-}" ] || continue
size=$(git cat-file -s "$sha" 2>/dev/null || echo 0)
if [ "$size" -ge 102400 ]; then
echo " $path ($size bytes, object $sha)" >&2
fail=1
fi
done < <(git rev-list --objects --all \
| grep -E '(^|/)libqsgepaper\.so(\.[0-9.]+)?$' || true)

if [ "$fail" -ne 0 ]; then
echo >&2
echo "ERROR: reMarkable's proprietary library is present in git history." >&2
echo "History must be rewritten to remove it before this branch can ship." >&2
exit 1
fi
echo "OK: history is clean."
66 changes: 66 additions & 0 deletions scripts/ci/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# CI guards

## `check-no-vendor-blob.sh`

Refuses to let reMarkable's proprietary `libqsgepaper.so` into the repository.

`quill/` is a clean-room MIT implementation that *links against* that library,
but the library itself is reMarkable's property. Every user extracts it from a
device they own — `quill/build.sh` pulls it over SSH — and we never
redistribute it. See `quill/README.md` and `quill/CLEANROOM.md` for the
clean-room boundary this protects.

`.gitignore` already covers `quill/vendor/` and `quill/build/`. This guard is
the backstop for the cases `.gitignore` does not cover:

| Case | Caught by |
|---|---|
| `git add -f quill/vendor/libqsgepaper.so` | filename match |
| Blob renamed and hidden elsewhere in the tree | ELF content scan |
| `quill/build/` output committed | path match |
| Blob already in a previous commit | CI history scan |

It deliberately **allows** documentation under `quill/vendor/` — that directory
legitimately holds a README telling users how to fetch the library themselves.
The library is matched by filename and by content, not by living in that
directory.

### Run it

```sh
./scripts/ci/check-no-vendor-blob.sh
```

### Install the pre-commit hook

```sh
./scripts/ci/install-hooks.sh
```

CI catches a leak after it is pushed; the hook catches it before it enters
history — the difference between `git restore --staged` and rewriting a branch.
Run once per clone. The hook installs into the shared `.git/hooks/`, so it
covers every worktree of this repository.

### In CI

`.github/workflows/no-vendor-blob.yml` runs the working-tree check on every
push and pull request, then scans full reachable history for any object named
`libqsgepaper.so` that is large enough to be the real library (~350KB).

### If it fires

```sh
git restore --staged <path> # not yet committed
```

If the blob is already in history, it must be removed with a history rewrite
(`git filter-repo`) before the branch can ship publicly. Verified clean as of
the commit that added this guard.

## Downstream repositories

Any repository that vendors `quill/` — including a future g-pad — must carry
this guard and the matching `.gitignore` rules. The ignore rules do not travel
with copied source; this check is what makes the boundary enforceable rather
than a convention.
89 changes: 89 additions & 0 deletions scripts/ci/check-no-vendor-blob.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Refuse to let reMarkable's proprietary display library into the repository.
#
# quill/ is a clean-room MIT implementation that LINKS AGAINST libqsgepaper.so,
# but that library is reMarkable's property. Every user extracts it from a
# device they own (quill/build.sh pulls it over SSH); we never redistribute it.
#
# .gitignore already covers quill/vendor/ and quill/build/, but a stray
# `git add -f`, a moved build directory, or a downstream repository that forgets
# the ignore rule would leak it. This check is the backstop that makes the leak
# hard to commit and impossible to push unnoticed.
#
# Run manually: ./scripts/ci/check-no-vendor-blob.sh
# Install hook: ./scripts/ci/install-hooks.sh
set -euo pipefail

cd "$(git rev-parse --show-toplevel)"

# Filenames that are always the vendor library, wherever they appear.
FORBIDDEN_NAME='libqsgepaper\.so'
# Build output that must never be tracked. quill/vendor/ is deliberately NOT
# listed: it legitimately holds documentation telling users how to fetch the
# library from their own device. The library itself is caught by name above
# and by the ELF content scan below.
FORBIDDEN_PATH='^quill/build/'
# Smallest plausible size for the real library (~350KB); skip smaller files
# when doing the expensive content scan.
MIN_BLOB_BYTES=102400

fail=0

report() {
if [ "$fail" -eq 0 ]; then
echo "ERROR: proprietary vendor artifacts must never be committed." >&2
echo >&2
fi
fail=1
echo " $1" >&2
}

matches_forbidden() {
grep -E "${FORBIDDEN_PATH}|(^|/)${FORBIDDEN_NAME}(\.[0-9.]+)?$" || true
}

# 1. Nothing currently tracked may match.
while IFS= read -r path; do
[ -n "$path" ] && report "tracked: $path"
done < <(git ls-files | matches_forbidden)

# 2. Nothing staged may match. This is the pre-commit path: it catches the
# blob before it ever enters history, where removing it means a rewrite.
if git rev-parse --verify --quiet HEAD >/dev/null 2>&1; then
while IFS= read -r path; do
[ -n "$path" ] && report "staged: $path"
done < <(git diff --cached --name-only --diff-filter=ACMR | matches_forbidden)
fi

# 3. Content check: a renamed blob still carries the vendor's ELF symbols.
# Catches `cp libqsgepaper.so quill/src/display.bin` and similar.
while IFS= read -r path; do
[ -n "$path" ] || continue
[ -f "$path" ] || continue
size=$(wc -c < "$path" 2>/dev/null || echo 0)
[ "$size" -lt "$MIN_BLOB_BYTES" ] && continue
# ELF magic: 0x7F 'E' 'L' 'F'
[ "$(head -c 4 "$path" | od -An -tx1 | tr -d ' \n')" = "7f454c46" ] || continue
# NOTE: `grep -q` exits on first match, which SIGPIPEs `strings`; under
# `set -o pipefail` that turns a successful match into a failed pipeline
# and the check silently passes. Count matches instead so the whole
# stream is consumed and the exit status reflects the search, not the pipe.
hits=$(strings -a "$path" 2>/dev/null | grep -cE 'qsgepaper|EPFramebuffer' || true)
if [ "${hits:-0}" -gt 0 ]; then
report "vendor ELF content in tracked file: $path"
fi
done < <(git ls-files)

if [ "$fail" -ne 0 ]; then
cat >&2 <<'MSG'

quill/vendor/ and quill/build/ are gitignored on purpose.
Users obtain libqsgepaper.so from their own device via quill/build.sh.
See quill/README.md and quill/CLEANROOM.md for the clean-room boundary.

If a file is already staged, unstage it: git restore --staged <path>
MSG
exit 1
fi

echo "OK: no proprietary vendor artifacts tracked or staged."
28 changes: 28 additions & 0 deletions scripts/ci/install-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Install the local pre-commit guard against committing the vendor library.
#
# CI catches a leak after the fact; the hook catches it before it enters
# history, which is the difference between "unstage the file" and "rewrite the
# branch". Run once per clone.
set -euo pipefail

root="$(git rev-parse --show-toplevel)"
hook="$(git rev-parse --git-path hooks/pre-commit)"

if [ -e "$hook" ] && ! grep -q 'check-no-vendor-blob' "$hook" 2>/dev/null; then
echo "A pre-commit hook already exists and does not call the guard:" >&2
echo " $hook" >&2
echo "Add this line to it manually:" >&2
echo ' "$(git rev-parse --show-toplevel)"/scripts/ci/check-no-vendor-blob.sh' >&2
exit 1
fi

mkdir -p "$(dirname "$hook")"
cat > "$hook" <<'HOOK'
#!/usr/bin/env sh
# Block commits that would add reMarkable's proprietary libqsgepaper.so.
"$(git rev-parse --show-toplevel)"/scripts/ci/check-no-vendor-blob.sh
HOOK
chmod +x "$hook"

echo "Installed pre-commit guard at ${hook#"$root"/}"