Requirements: Python 3.12+, Rust 1.70+
CI should orchestrate Just recipes. This project follows the principle that:
- Local development and CI should use the same commands
- CI workflows call
justrecipes (defined inJustfile) - Tools are managed by mise (defined in
.mise.toml) - Build logic lives in
Justfile, not in.github/workflows/
This ensures reproducibility and makes it easy to test CI steps locally.
mise manages all development tools:
# Install tools specified in .mise.toml
mise install
# Run tasks defined in mise.toml
mise run setup
mise run test
mise run lintTools are pinned in .mise.toml:
- Python 3.12
- Rust (latest stable)
# Setup development environment
just setup
# Run all quality checks (lint + format + typecheck + test)
just check
# Run all tests
just test
# Run single test
just test-single tests/test_render.py::TestMap::test_basic_render
# Run tests matching pattern
just test-filter "test_render"
# Run only unit tests (skip integration)
just test-unit
# Run tests in Docker (if system libraries are incompatible)
just test-docker
# Linting and formatting
just lint # Check linting
just lint-fix # Auto-fix issues
just format # Format code
just format-check # Check formatting (CI)
# Using ruff directly (if just is not available)
ruff check mlnative/ tests/ examples/ # Check for issues
ruff check mlnative/ tests/ examples/ --fix # Auto-fix issues
ruff format mlnative/ tests/ examples/ # Format code
ruff format mlnative/ tests/ examples/ --check # Check formatting
# Type checking
just typecheck
# Build and clean
just build
just build-rust # Build Rust binary only
just clean
# Build wheels for distribution
just build-wheels # All platforms (requires Docker)
# Run examples
just serve # FastAPI static maps API
just serve-test # Interactive web test interface
just example # Basic usage example
# Show code stats
just stats
# CI-specific commands (used by GitHub Actions)
just ci-build-binary <platform> # Build binary for specific platform
just ci-build-wheel <platform> # Build wheel for specific platform
just ci-build-sdist # Build source distribution# Setup
cd /var/home/adonm/dev/maplibre-native/mlnative
uv venv
uv pip install -e ".[dev,web]"
# Build Rust binary
cd rust && cargo build --release
# Run tests
uv run python -m pytest tests/ -v --tb=short
# Linting with ruff
uv run ruff check mlnative/ tests/ examples/
uv run ruff check mlnative/ tests/ examples/ --fix
uv run ruff format mlnative/ tests/ examples/
# Type checking with mypy
uv run mypy mlnative/
# Build package
uv build- Grug-brained: Simple > Complex. One class, 4 methods max.
- Explicit > Implicit: No magic, clear error messages
- 80/20 solution: Handle common cases simply
- Minimum: Python 3.12
- Use modern syntax:
dict[str, Any]instead ofDict[str, Any] - Use
|union operator:str | intinstead ofUnion[str, int]
- Group: stdlib → third-party → local
- Use
from typing importfor common types - Absolute imports for local modules:
from .module import Thing - Sorted by ruff (configured in pyproject.toml)
- Line length: 100 characters
- Double quotes for strings
- 4 spaces indentation
- Run
just formatbefore committing
- Use type hints for all function signatures
- Use
str | Noneinstead ofOptional[str](Python 3.10+ style) - Use
dict[str, Any]instead ofDict[str, Any] - Run
just typecheckto verify
snake_casefor functions/variablesPascalCasefor classesUPPER_CASEfor constants- Private modules:
_bridge.py - Private functions:
_helper_function()
- Raise
MlnativeError(or subclass) for all errors - Include helpful context in error messages
- Use
hasattr()checks in__del__to avoid AttributeError - Keep error messages actionable
- Google style:
Args:,Returns:,Raises: - Keep docstrings concise
- Include usage examples for public API
- Integration tests preferred over unit tests
- Test class naming:
TestFeature - Use pytest fixtures where appropriate
- Run
just test-unitfor quick feedback
mlnative/
├── mlnative/ # Main package
│ ├── __init__.py # Public exports
│ ├── map.py # Main Map class
│ ├── _bridge.py # Rust subprocess wrapper
│ ├── exceptions.py # MlnativeError
│ └── bin/ # Platform-specific binaries
├── rust/ # Rust native renderer
│ ├── Cargo.toml
│ └── src/
│ └── main.rs # JSON daemon
├── examples/ # Usage examples
│ ├── basic.py # Simple example
│ ├── fastapi_server.py # Static maps API
│ ├── web_test_server.py # Interactive test UI
│ └── templates/ # HTML templates for web UI
├── tests/ # pytest tests
├── scripts/ # Build helpers
├── .github/workflows/ # CI/CD
├── Justfile # Task runner commands
└── pyproject.toml # Config (ruff, mypy, deps)
pytest>=8.0- Testing (dev)ruff>=0.8.0- Linting and formatting (dev)mypy>=1.13.0- Type checking (dev)cibuildwheel>=2.16- Wheel building (dev)fastapi>=0.115- Web framework (optional)uvicorn[standard]>=0.32- ASGI server (optional)jinja2>=3.1- Templating for web UI (optional)httpx>=0.27- HTTP client for tests (dev)
All CI jobs use GitHub Actions:
- lint job:
just check(lint + format-check + typecheck + test-unit) - test job:
just test-unit - build-wheels job:
cibuildwheel(Linux, macOS, Windows) - publish jobs: PyPA publish action
Tools are managed manually:
- uv - Python package manager (install via
curl -LsSf https://astral.sh/uv/install.sh | sh) - Rust - Install via rustup (see https://rustup.rs/)
- just - Task runner (install via
cargo install justor package manager)
Update tools: Check upstream for latest versions
- Python 3.12+ required - Uses modern syntax throughout
- Uses uv for Python operations (not pip directly)
- Uses just for task running (see Justfile)
- Rust binary must be built before testing:
just build-rust - OpenFreeMap Liberty is the default style
- Returns PNG bytes (not PIL Image objects)
- Web test interface available at
just serve-test
Python (mlnative)
↓ JSON over stdin/stdout
Rust (mlnative-render daemon)
↓ FFI
MapLibre Native (C++ core)
↓
Pre-built amalgam libraries (statically linked ICU, jpeg, etc.)
The native renderer uses pre-built "amalgam" libraries from MapLibre Native which include all dependencies (ICU, libjpeg, libpng, etc.) statically linked. This eliminates system dependency issues.
cd rust
cargo build --releaseThe binary will be at target/release/mlnative-render.
The CI builds for multiple platforms:
- Linux x64 (x86_64-unknown-linux-gnu)
- Linux ARM64 (aarch64-unknown-linux-gnu)
- macOS x64 (x86_64-apple-darwin)
- macOS ARM64 (aarch64-apple-darwin)
- Windows x64 (x86_64-pc-windows-msvc)
The daemon accepts JSON commands on stdin and outputs JSON responses on stdout.
Commands:
init- Initialize with width, height, stylerender- Render single viewrender_batch- Render multiple views efficientlyquit- Stop daemon
Responses:
{"status": "ok", "png": "base64_encoded_data"}
{"status": "error", "error": "message"}Critical: Always keep pyproject.toml version and git tags in sync:
- Update version in
pyproject.tomlbefore creating a git tag - Use semantic versioning (MAJOR.MINOR.PATCH)
- Tag format:
v{version}(e.g.,v0.3.8)
# 1. Update version in pyproject.toml
version = "0.3.8" # Update this line
# 2. Commit the version bump
git add pyproject.toml
git commit -m "Bump version to 0.3.8"
# 3. Create annotated tag
git tag -a v0.3.8 -m "Release v0.3.8 - Description of changes"
# 4. Push to origin
git push origin main --tagsIf you need to remove unstable pre-releases:
Delete GitHub releases (keeps git tags):
# List releases
gh release list
# Delete specific release
gh release delete v0.3.7 --yesYank PyPI versions (prevents installation, keeps history):
# Install twine if needed
pip install twine
# Yank a version
twine yank mlnative-0.3.7
# Or use PyPI web interface:
# https://pypi.org/manage/project/mlnative/releases/Delete git tags (optional, destructive):
# Delete local tag
git tag -d v0.3.7
# Delete remote tag
git push --delete origin v0.3.7Pushing a tag starting with v automatically triggers:
- Build binaries for all platforms
- Build wheels and sdist
- Create GitHub Release with artifacts
- Publish to PyPI