Add implementation plan for kydriv driver
This commit is contained in:
parent
d02b180300
commit
c4ed95ff68
759
docs/superpowers/plans/2026-06-02-kydriv-implementation.md
Normal file
759
docs/superpowers/plans/2026-06-02-kydriv-implementation.md
Normal file
@ -0,0 +1,759 @@
|
|||||||
|
# kydriv 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 a macOS CUPS printer driver for the Kyocera/TA 3505ci that restores department code (Kostenstelle) support on Apple Silicon, distributed as a signed `.pkg` installer.
|
||||||
|
|
||||||
|
**Architecture:** A modified PPD (derived from the Intel `TA3505ci.PPD`) combined with a Python CUPS filter (`kyofilter`) that injects the department code as a PostScript `setmanagementnumber` command before `%%EndSetup`. Packaged with `pkgbuild`, signed with Developer ID, and notarized.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3 (macOS built-in `/usr/bin/python3`), CUPS PPD format, pytest, macOS `pkgbuild`/`notarytool`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `ppd/TA3505ci_AS.ppd` | Modified PPD — no Intel plugins, points to `kyofilter` |
|
||||||
|
| `filter/kyofilter` | Python CUPS filter — injects department code into PS stream |
|
||||||
|
| `tests/conftest.py` | Makes `kyofilter` importable despite missing `.py` extension |
|
||||||
|
| `tests/test_kyofilter.py` | All unit tests |
|
||||||
|
| `installer/scripts/postinstall` | Sets permissions, restarts CUPS after install |
|
||||||
|
| `installer/build.sh` | Copies files into `installer/root/`, calls `pkgbuild` |
|
||||||
|
| `.gitignore` | Excludes `installer/root/` and `*.pkg` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Scaffolding
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `.gitignore`
|
||||||
|
- Create: directories `ppd/`, `filter/`, `tests/`, `installer/scripts/`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create directories**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ppd filter tests installer/scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create `.gitignore`**
|
||||||
|
|
||||||
|
```
|
||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
|
*.pkg
|
||||||
|
installer/root/
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Install pytest**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip3 install pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `Successfully installed pytest-...` (or `Requirement already satisfied`)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .gitignore
|
||||||
|
git commit -m "chore: project scaffolding"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: PPD modification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `ppd/TA3505ci_AS.ppd`
|
||||||
|
|
||||||
|
The original Intel PPD has three problems: it references Intel-only filter binaries, it references Intel-only Cocoa plugins (APDialogExtension), and its KmManagment section has a fixed list of codes 0–30. We fix all three.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Copy original PPD**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp /Users/oliver/Downloads/TA3505ci.PPD ppd/TA3505ci_AS.ppd
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Remove Intel-only lines and update filter reference**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove the Intel pre-filter binary
|
||||||
|
sed -i '' '/^\*cupsPreFilter:/d' ppd/TA3505ci_AS.ppd
|
||||||
|
|
||||||
|
# Point to our new Python filter instead of kyofilter_E
|
||||||
|
sed -i '' 's/kyofilter_E/kyofilter/' ppd/TA3505ci_AS.ppd
|
||||||
|
|
||||||
|
# Remove all 11 APDialogExtension lines (Intel-only Cocoa print-dialog plugins)
|
||||||
|
sed -i '' '/^\*APDialogExtension:/d' ppd/TA3505ci_AS.ppd
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
```bash
|
||||||
|
grep -c "APDialogExtension\|cupsPreFilter\|kyofilter_E" ppd/TA3505ci_AS.ppd
|
||||||
|
```
|
||||||
|
Expected: `0`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep "cupsFilter" ppd/TA3505ci_AS.ppd
|
||||||
|
```
|
||||||
|
Expected: `*cupsFilter: "application/vnd.cups-postscript 0 kyofilter"`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Replace KmManagment section**
|
||||||
|
|
||||||
|
Run this Python script from the repo root (replaces the entire AccountingOptions group):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 - << 'PYEOF'
|
||||||
|
import re, pathlib
|
||||||
|
|
||||||
|
ppd = pathlib.Path("ppd/TA3505ci_AS.ppd").read_text(encoding="latin-1")
|
||||||
|
|
||||||
|
new_section = (
|
||||||
|
"*OpenGroup: AccountingOptions/Job Accounting\n\n"
|
||||||
|
"*% ---------------------------------------------------------\n"
|
||||||
|
"*% Kostenstelle (5-stellig) eintragen - ohne fuehrende Nullen.\n"
|
||||||
|
"*% Zeile kopieren und Zahl anpassen:\n"
|
||||||
|
"*% *KmManagment MG12345/Code 12345: \"\"\n"
|
||||||
|
"*% Zero-Padding auf 8 Stellen uebernimmt der Filter.\n"
|
||||||
|
"*% ---------------------------------------------------------\n"
|
||||||
|
"*OpenUI *KmManagment/Job Accounting: PickOne\n"
|
||||||
|
"*DefaultKmManagment: Default\n"
|
||||||
|
"*OrderDependency: 60 AnySetup *KmManagment\n"
|
||||||
|
"*KmManagment Default/Aus: \"\"\n"
|
||||||
|
"*KmManagment MG12345/Code 12345: \"\"\n"
|
||||||
|
"*?KmManagment: \"\"\n"
|
||||||
|
"*End\n"
|
||||||
|
"*CloseUI: *KmManagment\n\n"
|
||||||
|
"*CloseGroup: AccountingOptions/Job Accounting\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = re.sub(
|
||||||
|
r'\*OpenGroup: AccountingOptions.*?\*CloseGroup: AccountingOptions[^\n]*\n',
|
||||||
|
new_section, ppd, flags=re.DOTALL
|
||||||
|
)
|
||||||
|
|
||||||
|
pathlib.Path("ppd/TA3505ci_AS.ppd").write_text(result, encoding="latin-1")
|
||||||
|
print("Done. Verify with: grep -n 'KmManagment' ppd/TA3505ci_AS.ppd | head -8")
|
||||||
|
PYEOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output: `Done. Verify with: ...`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Replace placeholder code with actual department code**
|
||||||
|
|
||||||
|
Open `ppd/TA3505ci_AS.ppd` in a text editor. Find the two lines containing `MG12345` and replace `12345` with your real 5-digit department code. Example — if your code is `54321`:
|
||||||
|
|
||||||
|
```
|
||||||
|
*KmManagment MG54321/Code 54321: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Validate PPD**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cupstestppd ppd/TA3505ci_AS.ppd
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: the output ends with `PASS`. Warnings about missing options are acceptable; errors are not.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ppd/TA3505ci_AS.ppd
|
||||||
|
git commit -m "feat: add Apple Silicon PPD derived from Intel TA3505ci.PPD"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Filter — option parsing (TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/conftest.py`
|
||||||
|
- Create: `tests/test_kyofilter.py` (partial — only TestParseOptions)
|
||||||
|
- Create: `filter/kyofilter` (partial — only `parse_options`)
|
||||||
|
|
||||||
|
CUPS filters have no `.py` extension. `conftest.py` makes the file importable via `importlib`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `tests/conftest.py`**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import importlib.util, pathlib, sys
|
||||||
|
|
||||||
|
_path = pathlib.Path(__file__).parent.parent / "filter" / "kyofilter"
|
||||||
|
_spec = importlib.util.spec_from_file_location("kyofilter", _path)
|
||||||
|
_mod = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_mod)
|
||||||
|
sys.modules["kyofilter"] = _mod
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write failing tests for `parse_options`**
|
||||||
|
|
||||||
|
Create `tests/test_kyofilter.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import kyofilter
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseOptions:
|
||||||
|
def test_single_key_value(self):
|
||||||
|
assert kyofilter.parse_options("KmManagment=MG12345") == {"KmManagment": "MG12345"}
|
||||||
|
|
||||||
|
def test_multiple_options(self):
|
||||||
|
result = kyofilter.parse_options("KmManagment=MG12345 Duplex=DuplexNoTumble")
|
||||||
|
assert result == {"KmManagment": "MG12345", "Duplex": "DuplexNoTumble"}
|
||||||
|
|
||||||
|
def test_empty_string(self):
|
||||||
|
assert kyofilter.parse_options("") == {}
|
||||||
|
|
||||||
|
def test_flag_without_value(self):
|
||||||
|
assert kyofilter.parse_options("SomeFlag") == {"SomeFlag": "true"}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run tests — expect failure**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_kyofilter.py::TestParseOptions -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `ERROR` — module `kyofilter` not found yet (file doesn't exist).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Create `filter/kyofilter` with `parse_options`**
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""CUPS filter for TA 3505ci - injects department account code into PostScript stream."""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def parse_options(options_str):
|
||||||
|
options = {}
|
||||||
|
for token in options_str.split():
|
||||||
|
if '=' in token:
|
||||||
|
k, v = token.split('=', 1)
|
||||||
|
options[k] = v
|
||||||
|
else:
|
||||||
|
options[token] = 'true'
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x filter/kyofilter
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run tests — expect pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_kyofilter.py::TestParseOptions -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
```
|
||||||
|
PASSED tests/test_kyofilter.py::TestParseOptions::test_single_key_value
|
||||||
|
PASSED tests/test_kyofilter.py::TestParseOptions::test_multiple_options
|
||||||
|
PASSED tests/test_kyofilter.py::TestParseOptions::test_empty_string
|
||||||
|
PASSED tests/test_kyofilter.py::TestParseOptions::test_flag_without_value
|
||||||
|
4 passed
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add filter/kyofilter tests/conftest.py tests/test_kyofilter.py
|
||||||
|
git commit -m "feat: filter option parsing with tests"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Filter — account code extraction (TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/test_kyofilter.py` (add TestGetAccountCode)
|
||||||
|
- Modify: `filter/kyofilter` (add `get_account_code`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing tests for `get_account_code`**
|
||||||
|
|
||||||
|
Append to `tests/test_kyofilter.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
class TestGetAccountCode:
|
||||||
|
def test_five_digit_code_zero_padded_to_eight(self):
|
||||||
|
assert kyofilter.get_account_code({"KmManagment": "MG12345"}) == "00012345"
|
||||||
|
|
||||||
|
def test_one_digit_code_zero_padded(self):
|
||||||
|
assert kyofilter.get_account_code({"KmManagment": "MG1"}) == "00000001"
|
||||||
|
|
||||||
|
def test_eight_digit_code_unchanged(self):
|
||||||
|
assert kyofilter.get_account_code({"KmManagment": "MG12345678"}) == "12345678"
|
||||||
|
|
||||||
|
def test_default_returns_none(self):
|
||||||
|
assert kyofilter.get_account_code({"KmManagment": "Default"}) is None
|
||||||
|
|
||||||
|
def test_missing_key_returns_none(self):
|
||||||
|
assert kyofilter.get_account_code({}) is None
|
||||||
|
|
||||||
|
def test_no_mg_prefix_returns_none(self):
|
||||||
|
assert kyofilter.get_account_code({"KmManagment": "12345"}) is None
|
||||||
|
|
||||||
|
def test_non_numeric_code_returns_none(self):
|
||||||
|
assert kyofilter.get_account_code({"KmManagment": "MGabc45"}) is None
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests — expect failure**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_kyofilter.py::TestGetAccountCode -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `AttributeError: module 'kyofilter' has no attribute 'get_account_code'`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `get_account_code` to `filter/kyofilter`**
|
||||||
|
|
||||||
|
Insert after `parse_options` (before `if __name__`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
def get_account_code(options):
|
||||||
|
km = options.get('KmManagment', 'Default')
|
||||||
|
if km == 'Default' or not km.startswith('MG'):
|
||||||
|
return None
|
||||||
|
code = km[2:]
|
||||||
|
if not code.isdigit():
|
||||||
|
return None
|
||||||
|
return code.zfill(8)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests — expect pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_kyofilter.py::TestGetAccountCode -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `7 passed`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add filter/kyofilter tests/test_kyofilter.py
|
||||||
|
git commit -m "feat: account code extraction with zero-padding"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Filter — stream processing (TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/test_kyofilter.py` (add TestProcessStream)
|
||||||
|
- Modify: `filter/kyofilter` (add `process_stream`)
|
||||||
|
|
||||||
|
`process_stream` takes an iterable of `bytes` lines and yields `bytes` lines, injecting the account code command before `%%EndSetup` (or before `%%Page:` as fallback).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing tests for `process_stream`**
|
||||||
|
|
||||||
|
Append to `tests/test_kyofilter.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
class TestProcessStream:
|
||||||
|
def _stream(self, text):
|
||||||
|
return iter(line.encode() for line in text.splitlines(keepends=True))
|
||||||
|
|
||||||
|
def _collect(self, text, code):
|
||||||
|
return b"".join(kyofilter.process_stream(self._stream(text), code))
|
||||||
|
|
||||||
|
def test_passthrough_when_no_code(self):
|
||||||
|
ps = "%!PS\n%%BeginSetup\n%%EndSetup\n%%Page: 1 1\n"
|
||||||
|
assert self._collect(ps, None) == ps.encode()
|
||||||
|
|
||||||
|
def test_injects_before_end_setup(self):
|
||||||
|
ps = "%!PS\n%%BeginSetup\n%%EndSetup\n%%Page: 1 1\n"
|
||||||
|
result = self._collect(ps, "00012345")
|
||||||
|
assert b"(00012345) statusdict /setmanagementnumber get exec\n%%EndSetup\n" in result
|
||||||
|
|
||||||
|
def test_end_setup_preserved_after_injection(self):
|
||||||
|
ps = "%!PS\n%%BeginSetup\n%%EndSetup\n"
|
||||||
|
result = self._collect(ps, "00012345")
|
||||||
|
assert b"%%EndSetup\n" in result
|
||||||
|
|
||||||
|
def test_fallback_injects_before_first_page_when_no_end_setup(self):
|
||||||
|
ps = "%!PS\n%%Page: 1 1\nshowpage\n"
|
||||||
|
result = self._collect(ps, "00012345")
|
||||||
|
assert b"(00012345) statusdict /setmanagementnumber get exec\n%%Page: 1 1\n" in result
|
||||||
|
|
||||||
|
def test_injects_only_once(self):
|
||||||
|
ps = "%!PS\n%%BeginSetup\n%%EndSetup\n%%Page: 1 1\n%%Page: 2 1\n"
|
||||||
|
result = self._collect(ps, "00012345")
|
||||||
|
assert result.count(b"setmanagementnumber") == 1
|
||||||
|
|
||||||
|
def test_content_before_injection_point_preserved(self):
|
||||||
|
ps = "%!PS\n%%BeginSetup\n/mydict 10 dict def\n%%EndSetup\n"
|
||||||
|
result = self._collect(ps, "00012345")
|
||||||
|
assert b"/mydict 10 dict def\n" in result
|
||||||
|
|
||||||
|
def test_content_after_injection_point_preserved(self):
|
||||||
|
ps = "%!PS\n%%BeginSetup\n%%EndSetup\nshowpage\n"
|
||||||
|
result = self._collect(ps, "00012345")
|
||||||
|
assert b"showpage\n" in result
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests — expect failure**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_kyofilter.py::TestProcessStream -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `AttributeError: module 'kyofilter' has no attribute 'process_stream'`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `process_stream` to `filter/kyofilter`**
|
||||||
|
|
||||||
|
Insert after `get_account_code` (before `if __name__`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
def process_stream(lines, account_code):
|
||||||
|
if not account_code:
|
||||||
|
yield from lines
|
||||||
|
return
|
||||||
|
|
||||||
|
inject = f"({account_code}) statusdict /setmanagementnumber get exec\n".encode()
|
||||||
|
injected = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.rstrip(b'\r\n')
|
||||||
|
if not injected and (stripped == b'%%EndSetup' or stripped.startswith(b'%%Page:')):
|
||||||
|
yield inject
|
||||||
|
injected = True
|
||||||
|
yield line
|
||||||
|
|
||||||
|
if not injected:
|
||||||
|
sys.stderr.write("kyofilter: WARNING: no injection point found in PostScript stream\n")
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests — expect pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_kyofilter.py::TestProcessStream -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `7 passed`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run all tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `18 passed`
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add filter/kyofilter tests/test_kyofilter.py
|
||||||
|
git commit -m "feat: PostScript stream processing with account code injection"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Filter — `main()` and offline smoke test
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `filter/kyofilter` (replace `if __name__ == '__main__': pass` with full `main()`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the `main()` stub**
|
||||||
|
|
||||||
|
Replace the last three lines of `filter/kyofilter`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Remove:
|
||||||
|
if __name__ == '__main__':
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
With:
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 6:
|
||||||
|
sys.stderr.write("Usage: kyofilter job-id user title copies options [file]\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
options = parse_options(sys.argv[5])
|
||||||
|
account_code = get_account_code(options)
|
||||||
|
|
||||||
|
infile = open(sys.argv[6], 'rb') if len(sys.argv) > 6 else sys.stdin.buffer
|
||||||
|
try:
|
||||||
|
for chunk in process_stream(infile, account_code):
|
||||||
|
sys.stdout.buffer.write(chunk)
|
||||||
|
finally:
|
||||||
|
if len(sys.argv) > 6:
|
||||||
|
infile.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Smoke test — with department code**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf '%%!PS\n%%%%BeginSetup\n%%%%EndSetup\n%%%%Page: 1 1\nshowpage\n' \
|
||||||
|
| python3 filter/kyofilter 1 user "Test" 1 "KmManagment=MG12345"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output contains (in this order):
|
||||||
|
```
|
||||||
|
%%BeginSetup
|
||||||
|
(00012345) statusdict /setmanagementnumber get exec
|
||||||
|
%%EndSetup
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Smoke test — no department code (passthrough)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf '%%!PS\n%%%%BeginSetup\n%%%%EndSetup\n' \
|
||||||
|
| python3 filter/kyofilter 1 user "Test" 1 ""
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: output identical to input — no `setmanagementnumber` line.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run all tests once more**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `18 passed`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add filter/kyofilter
|
||||||
|
git commit -m "feat: complete kyofilter with CUPS main() entrypoint"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Installer structure
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `installer/scripts/postinstall`
|
||||||
|
- Create: `installer/build.sh`
|
||||||
|
|
||||||
|
`installer/root/` is generated by `build.sh` at build time and is git-ignored.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `installer/scripts/postinstall`**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
chmod 755 /usr/libexec/cups/filter/kyofilter
|
||||||
|
chown root:wheel /usr/libexec/cups/filter/kyofilter
|
||||||
|
launchctl kickstart -k system/org.cups.cupsd
|
||||||
|
exit 0
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x installer/scripts/postinstall
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create `installer/build.sh`**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
VERSION="1.0.0"
|
||||||
|
IDENTIFIER="de.kydriv.driver"
|
||||||
|
PKG_NAME="kydriv-driver-${VERSION}.pkg"
|
||||||
|
|
||||||
|
# Build installer/root from source files
|
||||||
|
mkdir -p installer/root/Library/Printers/PPDs/Contents/Resources
|
||||||
|
mkdir -p installer/root/usr/libexec/cups/filter
|
||||||
|
|
||||||
|
cp ppd/TA3505ci_AS.ppd \
|
||||||
|
installer/root/Library/Printers/PPDs/Contents/Resources/
|
||||||
|
cp filter/kyofilter \
|
||||||
|
installer/root/usr/libexec/cups/filter/
|
||||||
|
chmod 755 installer/root/usr/libexec/cups/filter/kyofilter
|
||||||
|
|
||||||
|
# Build (unsigned if SIGN_IDENTITY not set)
|
||||||
|
SIGN_ARGS=()
|
||||||
|
if [ -n "${SIGN_IDENTITY}" ]; then
|
||||||
|
SIGN_ARGS=(--sign "${SIGN_IDENTITY}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
pkgbuild \
|
||||||
|
--root installer/root \
|
||||||
|
--scripts installer/scripts \
|
||||||
|
--identifier "${IDENTIFIER}" \
|
||||||
|
--version "${VERSION}" \
|
||||||
|
"${SIGN_ARGS[@]}" \
|
||||||
|
"${PKG_NAME}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Built: ${PKG_NAME}"
|
||||||
|
if [ -z "${SIGN_IDENTITY}" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "To sign: SIGN_IDENTITY='Developer ID Installer: NAME (TEAMID)' bash installer/build.sh"
|
||||||
|
echo ""
|
||||||
|
echo "To notarize after signing:"
|
||||||
|
echo " xcrun notarytool submit ${PKG_NAME} --apple-id YOUR@EMAIL --team-id TEAMID --password APP-PW --wait"
|
||||||
|
echo " xcrun stapler staple ${PKG_NAME}"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x installer/build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build unsigned package to verify structure**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash installer/build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
```
|
||||||
|
kydriv-driver-1.0.0.pkg
|
||||||
|
Built: kydriv-driver-1.0.0.pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pkgutil --payload-files kydriv-driver-1.0.0.pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output includes:
|
||||||
|
```
|
||||||
|
./Library/Printers/PPDs/Contents/Resources/TA3505ci_AS.ppd
|
||||||
|
./usr/libexec/cups/filter/kyofilter
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add installer/scripts/postinstall installer/build.sh
|
||||||
|
git commit -m "feat: pkg installer with postinstall and build script"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Local install and print test (VPN required)
|
||||||
|
|
||||||
|
**Voraussetzung:** VPN-Verbindung zum Büronetz aktiv. Druckeradresse (IP oder Hostname) bekannt.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Validate PPD (offline)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cupstestppd ppd/TA3505ci_AS.ppd
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: ends with `PASS`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Install filter and PPD manually**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp filter/kyofilter /usr/libexec/cups/filter/kyofilter
|
||||||
|
sudo chmod 755 /usr/libexec/cups/filter/kyofilter
|
||||||
|
sudo chown root:wheel /usr/libexec/cups/filter/kyofilter
|
||||||
|
sudo cp ppd/TA3505ci_AS.ppd \
|
||||||
|
/Library/Printers/PPDs/Contents/Resources/TA3505ci_AS.ppd
|
||||||
|
sudo launchctl kickstart -k system/org.cups.cupsd
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add printer in System Settings**
|
||||||
|
|
||||||
|
Systemeinstellungen → Drucker & Scanner → `+` → Drucker per IP hinzufügen:
|
||||||
|
- Protokoll: Line Printer Daemon – LPD
|
||||||
|
- Adresse: IP/Hostname des 3505ci
|
||||||
|
- Treiber: „Software auswählen…" → `3505ci (KPDL)` (unsere PPD) wählen
|
||||||
|
|
||||||
|
- [ ] **Step 4: Test print without department code**
|
||||||
|
|
||||||
|
Beliebiges Dokument drucken, Job Accounting auf „Aus" lassen. Seite muss ankommen.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Test print with department code**
|
||||||
|
|
||||||
|
Druckdialog → Job Accounting → Ihren Code wählen → drucken.
|
||||||
|
|
||||||
|
Im Kyocera-Webinterface (http://\<Drucker-IP\>) unter „Auftragsstatus" oder „Auftragsverlauf" prüfen, dass die Kostenstelle korrekt erfasst wurde.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Verify injection via CUPS log**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tail -50 /var/log/cups/error_log | grep -i "kyofilter\|account\|managment"
|
||||||
|
```
|
||||||
|
|
||||||
|
Keine Fehler oder Warnungen aus `kyofilter` sollten erscheinen.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Save preset**
|
||||||
|
|
||||||
|
Druckdialog → Kostenstelle gewählt → Vorgaben → „Aktuelle Einstellungen als Vorgabe sichern…" → Name vergeben → `OK`. Preset beim nächsten Druckjob verwenden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: Build and sign .pkg
|
||||||
|
|
||||||
|
**Voraussetzung:** Developer ID Installer-Zertifikat im Keychain. Team-ID und App-spezifisches Passwort (appleid.apple.com) bereit.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Build signed package**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SIGN_IDENTITY="Developer ID Installer: IHR NAME (TEAMID)" \
|
||||||
|
bash installer/build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `Built: kydriv-driver-1.0.0.pkg` (ohne unsigned-Warnung)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify signature**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pkgutil --check-signature kydriv-driver-1.0.0.pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `Status: signed by a certificate trusted by macOS`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Notarize**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun notarytool submit kydriv-driver-1.0.0.pkg \
|
||||||
|
--apple-id IHR@EMAIL \
|
||||||
|
--team-id TEAMID \
|
||||||
|
--password APP-SPEZIFISCHES-PASSWORT \
|
||||||
|
--wait
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `status: Accepted`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Staple notarization ticket**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun stapler staple kydriv-driver-1.0.0.pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `The staple and validate action worked!`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Test .pkg on clean setup**
|
||||||
|
|
||||||
|
Filter und PPD deinstallieren, dann `.pkg` doppelklicken → installieren → Drucker neu einrichten → Preset testen.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cleanup für sauberen Test:
|
||||||
|
sudo rm /usr/libexec/cups/filter/kyofilter
|
||||||
|
sudo rm /Library/Printers/PPDs/Contents/Resources/TA3505ci_AS.ppd
|
||||||
|
sudo launchctl kickstart -k system/org.cups.cupsd
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit and tag**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -f kydriv-driver-1.0.0.pkg # optional: binary in repo
|
||||||
|
git commit -m "release: v1.0.0"
|
||||||
|
git tag v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder: `.pkg` separat aufbewahren (nicht im Repo), nur Source-Code committen.
|
||||||
Loading…
x
Reference in New Issue
Block a user