Development¶
Setup¶
git clone https://github.com/ykus4/fuin.git && cd fuin
uv sync --all-extras # --all-extras: the tests need the server deps
uv run pre-commit install
uv sync alone installs the packer only. Use --all-extras.
See Installation for the Android build-tools and JDK setup, both optional.
Everyday commands¶
| Command | What it does |
|---|---|
uv run pytest |
Run the test suite |
uv run pytest --cov |
With a coverage report |
uv run ruff check src/ tests/ |
Lint |
uv run ruff format src/ tests/ |
Format |
uv run mypy |
Type-check |
uv run pre-commit run --all-files |
Everything the hooks run |
uv run mkdocs serve |
Preview this site at http://localhost:8000 |
Warnings are errors in the test suite (filterwarnings = ["error"]), so a new
DeprecationWarning or ResourceWarning fails the build rather than accumulating.
Layout¶
fuin uses a src layout: the package lives at src/fuin/, so tests run against the
installed package rather than the source tree, and packaging mistakes surface in CI.
The fuin directory itself is the import name — modules cannot move up to src/
directly, or every subpackage would become a top-level import.
The core packer is grouped by concern rather than kept flat:
| Package | Owns |
|---|---|
axml/ |
Android Binary XML, byte level only — no ZIP, no filesystem |
apk/ |
ZIP I/O, signing, alignment, the Android SDK toolchain |
encryption/ |
AES, DEX strings, native libraries, assets |
reporting/ |
Read-only views: analysis and the pack diff |
contract.py |
The asset paths shared with the Kotlin stub |
Dependencies run downward only: packer.py → apk/ → axml/. If you find yourself
importing apk from axml, the code probably belongs on the other side of that line.
src/fuin/assets/stub.dex is committed and ships inside the wheel. That is why
pip install fuin can pack without an Android SDK — see
Architecture.
Tests¶
tests/
├── fixtures/ # importable builders — not conftest
│ ├── axml.py # hand-rolled binary AXML
│ └── apk.py # minimal APK (ZIP)
├── conftest.py # fixtures only
├── test_apk.py
├── test_crypto.py
├── test_manifest.py
├── test_pipeline.py # end-to-end pack
├── test_server.py # FastAPI endpoints
├── test_signing.py # v1/v2 signing, cert fingerprint
└── test_string_encrypt.py
Builders live in tests/fixtures/ rather than conftest.py on purpose: pytest
treats conftest specially, and importing helpers from it leaks that into anything
else that needs them, including CI.
The AXML builder is deliberately independent of fuin.axml. A builder sharing the
parser's code could not catch the parser being wrong.
Writing tests¶
Prefer asserting on behaviour over shape. assert zipfile.is_zipfile(out) passes for
almost any zip; assert on the entries, the digests and the header fields that matter.
For refactors with no intended behaviour change, differential testing beats the suite. Check the previous revision out into a worktree and compare outputs directly:
git worktree add --detach /tmp/before <ref>
# run both, compare hashes of the artefacts
git worktree remove /tmp/before --force
Type checking¶
mypy runs over src/fuin and must stay clean. Settings are deliberately moderate —
the codebase was annotated but never checked, so the configuration starts at a level
that passes today and should tighten as untyped edges get annotated.
SQLAlchemy models use 2.0 Mapped[...] annotations. Adding a column with a bare
Column(...) assignment will type every read of it as Column[str] and break call
sites downstream.
Database migrations¶
Models live in src/fuin/server/database.py; migrations in migrations/versions/.
uv run alembic revision --autogenerate -m "describe the change"
uv run alembic upgrade head
uv run alembic downgrade -1 # always test the way back
CI runs upgrade → downgrade → upgrade and asserts the migrated schema matches
Base.metadata. The test suite builds its schema with create_all, so without that
job migrations could silently drift from the models.
Rebuilding the stub¶
fuin.stub_dex extracts classes.jar from the AAR and converts it with d8. The
result is cached to src/fuin/assets/stub.dex. Commit it when the stub changes.
Point FUIN_STUB_DEX at a file to override it
without rebuilding.
Docs¶
This site is MkDocs Material. Pages live in docs/, navigation in mkdocs.yml.
--strict fails on broken internal links, so a renamed page cannot silently orphan
its references.
CI¶
.github/workflows/ci.yml runs on every push and pull request:
| Job | Purpose |
|---|---|
| Lint & format | ruff check, ruff format, mypy |
| Tests | pytest on Python 3.12, 3.13 and 3.14, with coverage |
| Packer-only install | Builds the wheel, installs it with no extras, packs an APK |
| Alembic migrations | upgrade → downgrade → upgrade, plus a schema-drift check |
| Docs | mkdocs build --strict |
| Gradle plugin | Builds the plugin |
| Android stub | Builds the stub AAR |
| Docker build | Builds the image, starts it, and serves a request |
The Python matrix includes 3.14 because that is what the Docker image ships.
The packer-only job guards two regressions this repository has already had:
stub.dex missing from the wheel, and server dependencies leaking into the base
install.
Releasing¶
fuin is not published to PyPI yet. The Docker image is built and pushed by
.github/workflows/docker.yml.
When adding a release process, note that action.yml currently installs from the
repository checkout. It can become a thin wrapper over a published wheel once one
exists.