Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ on:
pull_request:
workflow_dispatch:

# One run per branch/PR: a new push cancels the superseded run instead of
# queueing another full build of code nobody is waiting on. Runs on `main`
# and tags keep their own group and are never cancelled by a later push.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
ubuntu-26-04:
name: Build & test (Ubuntu 26.04, distro mapnik)
Expand Down
133 changes: 133 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
name: release

# Tagging `v*` publishes a release with a static `render` for each
# architecture. `workflow_dispatch` builds the same matrix without publishing,
# which is the only way to find out that an arch broke before a tag is already
# cut and public.
#
# Linux/musl only: a fully static binary needs musl, and macOS can't link
# libSystem statically (see the static-render section of the README).
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
publish:
description: "Publish a GitHub release (off = build and upload artifacts only)"
type: boolean
default: false

permissions:
contents: read

jobs:
build:
name: ${{ matrix.platform }}
strategy:
# One platform failing shouldn't hide whether the others are healthy.
fail-fast: false
matrix:
include:
- platform: x86_64-linux-musl
runner: ubuntu-24.04
- platform: aarch64-linux-musl
runner: ubuntu-24.04-arm
# macOS can't link libSystem statically, so this one bundles mapnik
# and every third-party library but leaves macOS' own dylibs dynamic.
- platform: aarch64-apple-darwin
runner: macos-14
runs-on: ${{ matrix.runner }}

steps:
- uses: actions/checkout@v4

# A tag's archive is named after the tag, so the two can't drift; a
# dispatch run falls back to the project version in CMakeLists.txt.
- name: Resolve version
id: version
run: |
set -euo pipefail
project="$(sed -n 's/^project(osmflat-mapnik-plugin VERSION \([^ )]*\).*/\1/p' CMakeLists.txt)"
if [ "${GITHUB_REF_TYPE}" = tag ]; then
version="${GITHUB_REF_NAME#v}"
if [ "$version" != "$project" ]; then
echo "tag $GITHUB_REF_NAME does not match project VERSION $project" >&2
exit 1
fi
else
version="$project"
fi
echo "version=$version" >> "$GITHUB_OUTPUT"

# No build cache here, unlike the CI workflow: a published binary should
# come from a clean build of the tagged tree, not a tree carried over
# from an earlier run.
- name: Build, test, and package (linux/musl)
if: runner.os == 'Linux'
run: |
docker run --rm -e OSMFLAT_RENDER_VERSION -v "$PWD:/src" -w /src alpine:3.22 \
sh scripts/build-static-render.sh
env:
OSMFLAT_RENDER_VERSION: ${{ steps.version.outputs.version }}

- name: Build, test, and package (macOS)
if: runner.os == 'macOS'
run: sh scripts/build-static-render-macos.sh
env:
OSMFLAT_RENDER_VERSION: ${{ steps.version.outputs.version }}

# Catches an archive that built but can't run on a machine with none of
# the build dependencies present -- the whole point of a static binary.
# (macOS is covered by the otool check inside its build script, since
# there's no dependency-free macOS sandbox to unpack into here.)
- name: Smoke test the packaged archive
if: runner.os == 'Linux'
run: |
set -euo pipefail
docker run --rm -v "$PWD:/src:ro" alpine:3.22 sh -c '
set -eu
cd /tmp && tar xzf /src/dist/*.tar.gz && cd osmflat-render-*-linux-musl
./render /no/plugins /src/test/style-relations.xml \
/src/test/fixtures/baarle-hertog.osm.flat /tmp/out.png \
4.75 51.38 5.02 51.49 500 500
test -s /tmp/out.png'

- uses: actions/upload-artifact@v4
with:
name: osmflat-render-${{ matrix.platform }}
path: dist/*.tar.gz
if-no-files-found: error

release:
name: publish
needs: build
# Only a tag publishes. A dispatch run stops after `build`, leaving the
# archives downloadable from the run for inspection.
if: startsWith(github.ref, 'refs/tags/v') || inputs.publish
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true

# One checksum file over every archive, so a download can be verified
# without trusting the transport.
- name: Checksums
run: |
cd artifacts
sha256sum * > SHA256SUMS
cat SHA256SUMS

- name: Publish
env:
GH_TOKEN: ${{ github.token }}
run: |
tag="${GITHUB_REF_NAME}"
gh release create "$tag" \
--repo "$GITHUB_REPOSITORY" \
--title "$tag" \
--generate-notes \
artifacts/*
81 changes: 81 additions & 0 deletions .github/workflows/static-render.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Static render

# Builds a self-contained `render`: Mapnik compiled from source as a static
# library with the osmflat datasource built in, linked fully static against
# musl. No libmapnik, osmflat.input, or system fonts needed at runtime.

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

# One run per branch/PR: a new push cancels the superseded run instead of
# queueing another full build of code nobody is waiting on. Runs on `main`
# and tags keep their own group and are never cancelled by a later push.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
build:
name: Static render (${{ matrix.platform }})
strategy:
fail-fast: false
matrix:
include:
- platform: x86_64-linux-musl
runner: ubuntu-24.04
- platform: aarch64-linux-musl
runner: ubuntu-24.04-arm
- platform: aarch64-apple-darwin
runner: macos-14
runs-on: ${{ matrix.runner }}

steps:
- name: Check out
uses: actions/checkout@v4

# Mapnik is ~175 translation units and dominates the build (~16 min cold
# on x86_64), so cache the whole build tree: its fetched source + objects,
# the cargo registry, and the downloaded apk packages all live there.
# ninja/cargo decide what to redo, so a source change still rebuilds
# exactly what it must. The key is unique per run (caches are immutable)
# and restore-keys fall back to the newest cache for this arch; a new
# mapnik tag or changed build plumbing starts a fresh lineage.
- name: Cache static build tree
uses: actions/cache@v4
with:
path: build-static
key: static-render-${{ matrix.platform }}-${{ hashFiles('cmake/**', 'scripts/*.sh', 'CMakeLists.txt') }}-${{ github.run_id }}
restore-keys: |
static-render-${{ matrix.platform }}-${{ hashFiles('cmake/**', 'scripts/*.sh', 'CMakeLists.txt') }}-
static-render-${{ matrix.platform }}-

# Run the whole build in Alpine via `docker run` rather than a job-level
# `container:` -- GitHub's JavaScript actions don't run inside Alpine
# containers on arm64 runners, and this is exactly what a local
# `podman run` executes too.
- name: Build, test, and package (linux/musl)
if: runner.os == 'Linux'
run: |
docker run --rm -v "$PWD:/src" -w /src alpine:3.22 \
sh scripts/build-static-render.sh

- name: Build, test, and package (macOS)
if: runner.os == 'macOS'
run: sh scripts/build-static-render-macos.sh

# The container builds as root (apk needs it), and some of what cargo
# leaves behind isn't readable by the runner user -- which makes the
# post-job cache save fail on `tar: ... Permission denied`.
- name: Hand the build tree back to the runner user
if: runner.os == 'Linux'
run: sudo chown -R "$(id -u):$(id -g)" build-static dist

- name: Upload
uses: actions/upload-artifact@v4
with:
name: osmflat-render-${{ matrix.platform }}
path: dist/*.tar.gz
if-no-files-found: error
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
# Render smoke-test output
*.png
/compile_commands.json
/build-static/
/dist/
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ corrosion_import_crate(
PROFILE release
)

# --- Static render (cmake -DOSMFLAT_STATIC_RENDER=ON) ------------------------
# Builds Mapnik from source with osmflat compiled in, and a self-contained
# `render` linked against it -- instead of the osmflat.input module below.
option(OSMFLAT_STATIC_RENDER "Build a static Mapnik with osmflat built in, and link render against it" OFF)
if(OSMFLAT_STATIC_RENDER)
include(cmake/StaticRender.cmake)
return()
endif()

# --- Mapnik ------------------------------------------------------------------
find_package(PkgConfig REQUIRED)
# Mapnik 4 is required: its API returns std::optional where 3.x returned
Expand Down
25 changes: 25 additions & 0 deletions LICENSE-MIT
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Copyright (c) 2026 Boyd Johnson

Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,76 @@ there means building mapnik from source.
crate, so a clone of this repo builds on its own — no sibling checkouts — but
the first build needs network access for cargo to fetch them.

## Static, self-contained `render`

`-DOSMFLAT_STATIC_RENDER=ON` builds mapnik from source (v4.2.2 by default) as a
**static** library with the osmflat datasource compiled in as a built-in
plugin, and links `render` against it. The result loads nothing at runtime — no
`libmapnik`, no `osmflat.input`, no system fonts — so it can be copied to a
machine that has never heard of mapnik:

```sh
cmake -S . -B build-static -DCMAKE_BUILD_TYPE=Release \
-DOSMFLAT_STATIC_RENDER=ON -DOSMFLAT_FULLY_STATIC=ON
cmake --build build-static --target render
```

`OSMFLAT_FULLY_STATIC=ON` adds `-static` (musl only: libc goes in too). On
macOS, leave it off -- libSystem can't be linked statically, so mapnik and
every third-party library go in but macOS' own dylibs and frameworks stay
dynamic; `scripts/build-static-render-macos.sh` builds that and fails if
`otool -L` shows anything outside `/usr/lib` or `/System`. Two macOS
wrinkles it handles: Homebrew's `libharfbuzz.a` needs graphite2, which has no
static archive, so harfbuzz is built from source with just freetype and
CoreText; and icu4c/zlib/bzip2 are keg-only, so their prefixes are passed
explicitly. The
`<plugin_dir>` argument stays in the CLI but is ignored — loading a `.input`
module would pull in a second, shared mapnik.

Mapnik's static-plugin table is compile-time, so
`cmake/static-mapnik/patch-mapnik.cmake` edits the fetched mapnik tree: it adds
`plugins/input/osmflat`, links it into `libmapnik`, registers it in
`datasource_cache_static.cpp`, and teaches `projection` to accept
`+proj=longlat ...` (see below). Every edit anchors on an exact upstream string
and fails loudly if mapnik moved it.

Trimmed to what `render` needs: AGG + PNG, freetype/harfbuzz/ICU for text. No
cairo, PROJ, grid/SVG renderers, or stock input plugins. Dropping PROJ is what
keeps the binary free of a 9 MB `proj.db`, but mapnik then only knows
`epsg:4326` / `epsg:3857` by name — and, more subtly, classifies `epsg:4326` as
*geographic*, which scales `scale_denominator` by ~111319 versus what PROJ
reports for the equivalent `+proj=longlat +datum=WGS84 +no_defs`. Since every
style here is tuned against the degree-based numbers (`0.1` ≈ neighborhood),
the patch makes a PROJ-less mapnik classify `+proj=longlat` exactly as PROJ
does, so styles render identically either way. Other proj4 strings still throw.

Fonts: `$MAPNIK_FONT_DIR` if set, else `fonts/` beside the binary (which is how
the release tarball ships DejaVu).

`scripts/build-static-render.sh` does the whole thing in Alpine — installs the
static dependency archives, builds, runs ctest, and packages
`dist/osmflat-render-<version>-<arch>-linux-musl.tar.gz`:

```sh
podman run --rm -v "$PWD:/src" -w /src alpine:3.22 sh scripts/build-static-render.sh
```

`.github/workflows/static-render.yml` runs that same script under `docker run`
for x86_64 and aarch64, plus the macOS script on `macos-14`, and uploads all
three tarballs, caching the build tree
(mapnik is ~16 min cold, ~1 min warm). `.github/workflows/release.yml` builds
the same three archives from a clean tree on a `v*` tag — the tag has to match
`project(... VERSION)` — smoke-tests each unpacked archive on a bare alpine
image, and publishes them with a `SHA256SUMS`. `workflow_dispatch` on that
workflow builds without publishing, to check an arch before cutting the tag.

Each archive carries `render`, `fonts/`, mapnik's `LICENSE.mapnik`, and a
`NOTICE` naming every statically linked library and how to relink.

**Licensing:** mapnik is LGPL-2.1, so a distributed binary with mapnik linked
in must let recipients relink it against a modified mapnik — publishing these
sources plus the build scripts is what covers that.

## Datasource parameters

| param | required | values | meaning |
Expand Down Expand Up @@ -450,3 +520,18 @@ Two sidecar sources can back this, checked in order:

See `osmflat-ext`'s README for both build sides and `osmflat_ext::coastline` /
`osmflat_ext::land_polygons`'s module docs for the respective algorithms.

## License

MIT ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>).

MIT rather than the usual Rust-ecosystem MIT/Apache-2.0 dual license because
the static `render` links LGPL-2.1 mapnik, and Apache-2.0 is considered
incompatible with LGPL-2.1 for a combined work.

Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you shall be licensed as above,
without any additional terms or conditions.

The statically linked `render` archives are a separate case -- see
"Static, self-contained `render`" above.
Loading
Loading