feat: add PyInstaller CI binaries and Streamlit web UI#50
feat: add PyInstaller CI binaries and Streamlit web UI#50ambicuity wants to merge 5 commits intoSUPAIDEAS:mainfrom
Conversation
…EAS#44, closes SUPAIDEAS#45) - feat(ci): add .github/workflows/build-binaries.yml — GitHub Actions pipeline using PyInstaller to build standalone executables for Linux (x86_64), macOS (arm64) and Windows (x64) on every tag push; binaries are uploaded as artifacts and attached to GitHub Releases - feat(ux): add app/streamlit_app.py — a local Streamlit web UI wrapper that lets users upload a PDF, set a password with confirmation, and download the encrypted file without any CLI knowledge - feat: add requirements-webui.txt with streamlit dependency (kept separate from core dependencies to keep the CLI install lean) - docs: update README with Web UI usage instructions, pre-built binary download section, and fix existing typos
There was a problem hiding this comment.
Pull request overview
Adds distribution and UX improvements by introducing a Streamlit-based local web UI and a GitHub Actions workflow intended to publish PyInstaller-built binaries on tagged releases.
Changes:
- Adds a Streamlit app for encrypting uploaded PDFs via a browser-based local UI.
- Adds a new release workflow to build and attach standalone binaries for Linux/macOS/Windows.
- Updates documentation and adds a separate requirements file for the optional web UI.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
uv.lock |
Introduces a uv lockfile intended to support uv sync in the binary-build workflow. |
requirements-webui.txt |
Adds a minimal dependency list for running the optional Streamlit UI. |
app/streamlit_app.py |
Implements the Streamlit web UI wrapper around passifypdf.encryptpdf.encrypt_pdf. |
README.md |
Documents Web UI usage and pre-built binary downloads; fixes typos in “Known Issues”/“Note”. |
.github/workflows/build-binaries.yml |
Adds CI for building PyInstaller binaries and attaching them to GitHub Releases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - name: Install uv | ||
| uses: astral-sh/setup-uv@v4 | ||
|
|
||
| - name: Install dependencies | ||
| run: uv sync | ||
|
|
||
| - name: Install PyInstaller | ||
| run: uv pip install pyinstaller | ||
|
|
||
| - name: Build binary | ||
| run: | | ||
| uv run pyinstaller \ |
There was a problem hiding this comment.
This workflow uses uv sync, but the repo's dependency management is Poetry-based (see .github/workflows/ci.yml and pyproject.toml). As-is, uv sync depends on uv.lock, which currently doesn't list any dependencies (e.g., pypdf), so the build environment may be missing runtime deps needed by PyInstaller. Consider either switching this workflow to poetry install (matching CI) or generating/committing a complete uv.lock produced by uv lock so uv sync installs the same deps as Poetry.
| - name: Install uv | |
| uses: astral-sh/setup-uv@v4 | |
| - name: Install dependencies | |
| run: uv sync | |
| - name: Install PyInstaller | |
| run: uv pip install pyinstaller | |
| - name: Build binary | |
| run: | | |
| uv run pyinstaller \ | |
| - name: Install Poetry | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install poetry | |
| - name: Install dependencies | |
| run: | | |
| poetry install --no-interaction --no-ansi | |
| - name: Install PyInstaller | |
| run: | | |
| poetry run python -m pip install pyinstaller | |
| - name: Build binary | |
| run: | | |
| poetry run pyinstaller \ |
| --onefile \ | ||
| --name passifypdf \ | ||
| --console \ | ||
| passifypdf/encryptpdf.py |
There was a problem hiding this comment.
PyInstaller is being pointed at passifypdf/encryptpdf.py, but that file uses a relative import (from .cli import get_arg_parser). When executed as a script (which is how the frozen app runs the entry module), that relative import will fail. Use an entry point that imports the package module (e.g., a small top-level main.py that calls passifypdf.encryptpdf.main, or a passifypdf/__main__.py) and build from that instead.
| passifypdf/encryptpdf.py | |
| -m passifypdf.encryptpdf |
| - os: ubuntu-latest | ||
| artifact_name: passifypdf-linux-x86_64 | ||
| binary_path: dist/passifypdf | ||
| - os: macos-latest |
There was a problem hiding this comment.
The matrix labels the macOS artifact as arm64, but macos-latest isn't guaranteed to stay on Apple Silicon. Pin the runner to an explicit version/arch (e.g., macos-14 for arm64) or rename the artifact to avoid claiming a specific architecture when it's not enforced.
| - os: macos-latest | |
| - os: macos-14 |
| streamlit run app/streamlit_app.py | ||
| """ | ||
|
|
||
| import io |
There was a problem hiding this comment.
io is imported but never used. Please remove it to avoid lint/quality noise.
| import io |
| try: | ||
| # Write uploaded bytes to a temp input file | ||
| with tempfile.NamedTemporaryFile( | ||
| suffix=".pdf", delete=False | ||
| ) as tmp_in: | ||
| tmp_in.write(uploaded_file.read()) | ||
| tmp_in_path = Path(tmp_in.name) | ||
|
|
||
| # Encrypt to a temp output file | ||
| with tempfile.NamedTemporaryFile( | ||
| suffix=".pdf", delete=False | ||
| ) as tmp_out: | ||
| tmp_out_path = Path(tmp_out.name) | ||
|
|
||
| encrypt_pdf(tmp_in_path, tmp_out_path, password) | ||
|
|
||
| # Read the encrypted bytes and offer download | ||
| encrypted_bytes = tmp_out_path.read_bytes() | ||
|
|
||
| st.success("✅ PDF encrypted successfully!") | ||
| st.download_button( | ||
| label="⬇️ Download encrypted PDF", | ||
| data=encrypted_bytes, | ||
| file_name=output_name, | ||
| mime="application/pdf", | ||
| use_container_width=True, | ||
| ) | ||
|
|
||
| except Exception as exc: | ||
| st.error(f"❌ Encryption failed: {exc}") | ||
| finally: | ||
| # Clean up temp files | ||
| tmp_in_path.unlink(missing_ok=True) | ||
| tmp_out_path.unlink(missing_ok=True) |
There was a problem hiding this comment.
tmp_in_path / tmp_out_path are created inside the try, but the finally always calls tmp_in_path.unlink(...) / tmp_out_path.unlink(...). If an exception occurs before either variable is assigned (e.g., temp file creation fails), the finally will raise UnboundLocalError and mask the original error. Initialize these paths to None before the try and guard the cleanup accordingly.
Added instructions for launching the UI and running commands.
Updated UI-README to clarify PDF file functionality.
Summary
Closes #44 - Cross-platform native binaries via PyInstaller
Closes #45 - Streamlit web UI wrapper
Changes
PyInstaller CI (#44)
.github/workflows/build-binaries.yml- GitHub Actions pipeline that builds standalone executables for Linux (x86_64), macOS (arm64) and Windows (x64) on everyv*tag push using PyInstaller--onefile. Includes a smoke test step and uploads binaries as artifacts + attaches them to GitHub Releases viasoftprops/action-gh-release.Streamlit Web UI (#45)
app/streamlit_app.py- A local Streamlit web interface: users upload a PDF, enter (and confirm) a password, and download the encrypted file with one click. No CLI knowledge required.requirements-webui.txt- Separate requirements file (streamlit>=1.35.0) so the core CLI install stays lean.README.md- Added Web UI usage instructions and a pre-built binary download section.Testing
All 4 tests pass. The Streamlit app can be tested locally with
streamlit run app/streamlit_app.py.