From c88ba464992f9a1f553a06c0bf99b2297e9e975d Mon Sep 17 00:00:00 2001 From: Boyd Johnson Date: Thu, 17 Sep 2026 12:52:46 -0500 Subject: [PATCH 1/8] Add a static, self-contained render (mapnik built in) OSMFLAT_STATIC_RENDER=ON fetches mapnik v4.2.2, patches osmflat into its compile-time static-plugin table, and links render against the static libmapnik. OSMFLAT_FULLY_STATIC=ON adds -static, so on musl the binary needs nothing at runtime -- no libmapnik, no osmflat.input, no fonts. Dropping PROJ avoids shipping proj.db, but a PROJ-less mapnik knows only epsg:4326 and classifies it as geographic, which scales scale_denominator by ~111319x against what PROJ reports for the equivalent '+proj=longlat +datum=WGS84 +no_defs' -- silently breaking every style's MaxScaleDenominator gating. The patch teaches projection to classify +proj=longlat the way PROJ does, so both builds agree (verified: 1.92857 for the same style/bbox/size on both). scripts/build-static-render.sh builds, tests, and packages it in Alpine; static-render.yml runs that script for x86_64 and aarch64. Verified on aarch64: 16.5M stripped binary, ctest passes, the unpacked tarball renders on a bare alpine image, and fonts/ beside the binary is picked up (8173-byte labelled PNG vs 2785 without). Co-Authored-By: Claude Opus 5 --- .github/workflows/static-render.yml | 44 ++++++ .gitignore | 2 + CMakeLists.txt | 9 ++ README.md | 53 +++++++ cmake/StaticRender.cmake | 139 ++++++++++++++++++ cmake/static-mapnik/patch-mapnik.cmake | 100 +++++++++++++ cmake/static-mapnik/plugin/CMakeLists.txt | 21 +++ .../plugin/osmflat_datasource.hpp.in | 13 ++ scripts/build-static-render.sh | 48 ++++++ test/render.cpp | 27 ++++ 10 files changed, 456 insertions(+) create mode 100644 .github/workflows/static-render.yml create mode 100644 cmake/StaticRender.cmake create mode 100644 cmake/static-mapnik/patch-mapnik.cmake create mode 100644 cmake/static-mapnik/plugin/CMakeLists.txt create mode 100644 cmake/static-mapnik/plugin/osmflat_datasource.hpp.in create mode 100755 scripts/build-static-render.sh diff --git a/.github/workflows/static-render.yml b/.github/workflows/static-render.yml new file mode 100644 index 0000000..0300c51 --- /dev/null +++ b/.github/workflows/static-render.yml @@ -0,0 +1,44 @@ +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: + +jobs: + linux-musl: + name: Static render (${{ matrix.arch }}-linux-musl) + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-24.04 + - arch: aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + + steps: + - name: Check out + uses: actions/checkout@v4 + + # 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 + run: | + docker run --rm -v "$PWD:/src" -w /src alpine:3.22 \ + sh scripts/build-static-render.sh + + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: osmflat-render-${{ matrix.arch }}-linux-musl + path: dist/*.tar.gz + if-no-files-found: error diff --git a/.gitignore b/.gitignore index ee3d5a5..c48fc8e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ # Render smoke-test output *.png /compile_commands.json +/build-static/ +/dist/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cdf036..56536c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/README.md b/README.md index a53233c..9df12e1 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,59 @@ 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). The +`` 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---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 and uploads both tarballs. + +**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 | diff --git a/cmake/StaticRender.cmake b/cmake/StaticRender.cmake new file mode 100644 index 0000000..a0ca0b7 --- /dev/null +++ b/cmake/StaticRender.cmake @@ -0,0 +1,139 @@ +# OSMFLAT_STATIC_RENDER=ON: build Mapnik from source as a static library with +# the osmflat datasource compiled in as a built-in plugin, and link `render` +# against it. The result has no libmapnik / osmflat.input to find at runtime. +# +# Included from the top-level CMakeLists after the Rust crate is imported +# (the plugin sources need the `osmflat_capi` target and its cbindgen header). + +set(OSMFLAT_MAPNIK_GIT_TAG "v4.2.2" CACHE STRING "Mapnik tag to build statically") +option(OSMFLAT_FULLY_STATIC "Link render with -static (Linux/musl: no shared libs at all)" OFF) + +# --- Mapnik build options ---------------------------------------------------- +# Only what `render` uses: the AGG renderer writing PNG, fonts via FreeType + +# HarfBuzz + ICU. No PROJ (render rewrites the WGS84 / web-mercator proj4 +# strings to their EPSG codes, which Mapnik handles natively), no Cairo, no +# stock input plugins. +set(OSMFLAT_MAPNIK_OPTIONS + BUILD_SHARED_LIBS=OFF + BUILD_SHARED_PLUGINS=OFF + BUILD_TESTING=OFF + BUILD_BENCHMARK=OFF + BUILD_DEMO_VIEWER=OFF + BUILD_DEMO_CPP=OFF + BUILD_UTILITY_GEOMETRY_TO_WKB=OFF + BUILD_UTILITY_MAPNIK_INDEX=OFF + BUILD_UTILITY_MAPNIK_RENDER=OFF + BUILD_UTILITY_OGRINDEX=OFF + BUILD_UTILITY_PGSQL2SQLITE=OFF + BUILD_UTILITY_SHAPEINDEX=OFF + BUILD_UTILITY_SVG2PNG=OFF + INSTALL_DEPENDENCIES=OFF + USE_PNG=ON + USE_JPEG=OFF + USE_TIFF=OFF + USE_WEBP=OFF + USE_AVIF=OFF + USE_LIBXML2=OFF + USE_CAIRO=OFF + USE_PROJ=OFF + USE_GRID_RENDERER=OFF + USE_SVG_RENDERER=OFF + # The plugin's MAPNIK_LOG_DEBUG lines (OSMFLAT_LOG_DEBUG=1) need this. + USE_LOG=ON + USE_PLUGIN_INPUT_CSV=OFF + USE_PLUGIN_INPUT_GDAL=OFF + USE_PLUGIN_INPUT_OGR=OFF + USE_PLUGIN_INPUT_GDAL_OGR=OFF + USE_PLUGIN_INPUT_GEOBUF=OFF + USE_PLUGIN_INPUT_GEOJSON=OFF + USE_PLUGIN_INPUT_POSTGIS=OFF + USE_PLUGIN_INPUT_PGRASTER=OFF + USE_PLUGIN_INPUT_POSTGIS_PGRASTER=OFF + USE_PLUGIN_INPUT_RASTER=OFF + USE_PLUGIN_INPUT_SHAPE=OFF + USE_PLUGIN_INPUT_SQLITE=OFF + USE_PLUGIN_INPUT_TILES=OFF + USE_PLUGIN_INPUT_TOPOJSON=OFF +) +foreach(opt IN LISTS OSMFLAT_MAPNIK_OPTIONS) + string(REPLACE "=" ";" kv "${opt}") + list(GET kv 0 key) + list(GET kv 1 value) + set(${key} ${value} CACHE BOOL "" FORCE) +endforeach() + +# Static dependency archives (libicuuc.a, libfreetype.a, ...) rather than .so. +set(Boost_USE_STATIC_LIBS ON) +set(ICU_USE_STATIC_LIBS ON) +if(OSMFLAT_FULLY_STATIC) + set(CMAKE_FIND_LIBRARY_SUFFIXES .a) + # --static so pkg-config also reports Libs.private (harfbuzz needs glib, + # graphite2, freetype's bz2/brotli, ...), which only matters when linking + # against the archives. + set(PKG_CONFIG_ARGN --static) + # harfbuzz's own CMake config names the shared library, which a -static + # link refuses ("attempted static link of dynamic object"). Hiding the + # config package makes Mapnik take its pkg-config fallback instead. + set(CMAKE_DISABLE_FIND_PACKAGE_harfbuzz ON) + # pkg_check_modules caches harfbuzz_FOUND; left over from an earlier + # configure it would send Mapnik down the (now disabled) config branch and + # fail on a missing harfbuzz::harfbuzz target. + unset(harfbuzz_FOUND CACHE) +endif() + +# Read by the generated plugins/input/osmflat/CMakeLists.txt. +set(OSMFLAT_SOURCE_DIR ${CMAKE_SOURCE_DIR}) + +FetchContent_Declare( + mapnik + GIT_REPOSITORY https://github.com/mapnik/mapnik.git + GIT_TAG ${OSMFLAT_MAPNIK_GIT_TAG} + GIT_SHALLOW TRUE + GIT_SUBMODULES deps/mapbox/geometry deps/mapbox/polylabel deps/mapbox/protozero deps/mapbox/variant + PATCH_COMMAND ${CMAKE_COMMAND} + -DMAPNIK_SOURCE_DIR= + -DOSMFLAT_SOURCE_DIR=${CMAKE_SOURCE_DIR} + -P ${CMAKE_SOURCE_DIR}/cmake/static-mapnik/patch-mapnik.cmake + UPDATE_DISCONNECTED TRUE + EXCLUDE_FROM_ALL +) +FetchContent_MakeAvailable(mapnik) + +# libmapnik now compiles our plugin sources, which include the cbindgen header +# that `cargo build` writes -- so cargo has to run first. +add_dependencies(mapnik cargo-build_osmflat_capi) + +# Cargo reports `gcc_s` among the native libs its staticlib needs, but musl +# toolchains ship only the shared libgcc_s -- the static unwinder there is +# libgcc_eh.a. Swap it in Corrosion's link interface, or `-static` fails with +# "cannot find -lgcc_s". +if(OSMFLAT_FULLY_STATIC) + foreach(tgt osmflat_capi osmflat_capi-static) + if(TARGET ${tgt}) + get_target_property(_libs ${tgt} INTERFACE_LINK_LIBRARIES) + if(_libs) + string(REPLACE "gcc_s" "gcc_eh" _libs "${_libs}") + set_target_properties(${tgt} PROPERTIES INTERFACE_LINK_LIBRARIES "${_libs}") + endif() + endif() + endforeach() +endif() + +# --- render ------------------------------------------------------------------ +add_executable(render test/render.cpp) +# mapnik::mapnik before osmflat_capi: libmapnik.a references the Rust symbols, +# and single-pass linkers resolve left to right. +target_link_libraries(render PRIVATE mapnik::mapnik osmflat_capi) +target_compile_definitions(render PRIVATE OSMFLAT_STATIC_RENDER) +if(OSMFLAT_FULLY_STATIC) + target_link_options(render PRIVATE -static) +endif() + +enable_testing() +set(FIXTURES ${CMAKE_SOURCE_DIR}/test/fixtures) +# Same smoke test as the dynamic build. The plugin_dir argument is ignored by +# a static render, so pass a path that doesn't exist to prove it. +add_test(NAME render_relations + COMMAND render ${CMAKE_BINARY_DIR}/no-plugins ${CMAKE_SOURCE_DIR}/test/style-relations.xml + ${FIXTURES}/baarle-hertog.osm.flat ${CMAKE_BINARY_DIR}/render_relations.png + 4.75 51.38 5.02 51.49 500 500) diff --git a/cmake/static-mapnik/patch-mapnik.cmake b/cmake/static-mapnik/patch-mapnik.cmake new file mode 100644 index 0000000..9f10be7 --- /dev/null +++ b/cmake/static-mapnik/patch-mapnik.cmake @@ -0,0 +1,100 @@ +# Script mode (cmake -P): wire the osmflat datasource into a Mapnik source +# tree as a *built-in* plugin, so a static libmapnik (BUILD_SHARED_PLUGINS=OFF) +# carries it the same way it carries csv/geojson/shape. +# +# cmake -DMAPNIK_SOURCE_DIR= -DOSMFLAT_SOURCE_DIR= -P patch-mapnik.cmake +# +# Mapnik's static-plugin list is compile-time (datasource_cache_static.cpp + +# src/CMakeLists.txt), so there is no way to register an out-of-tree plugin +# without editing those files. Every edit anchors on an exact upstream string +# and fails loudly if the anchor is missing (i.e. Mapnik changed under us), and +# is skipped if already applied, so re-running on a populated tree is safe. + +foreach(var MAPNIK_SOURCE_DIR OSMFLAT_SOURCE_DIR) + if(NOT ${var}) + message(FATAL_ERROR "patch-mapnik.cmake: ${var} is required") + endif() +endforeach() + +function(osmflat_patch_file file anchor replacement marker) + set(path "${MAPNIK_SOURCE_DIR}/${file}") + file(READ "${path}" content) + string(FIND "${content}" "${marker}" already) + if(NOT already EQUAL -1) + return() + endif() + string(FIND "${content}" "${anchor}" found) + if(found EQUAL -1) + message(FATAL_ERROR "patch-mapnik.cmake: anchor not found in ${file}:\n${anchor}") + endif() + string(REPLACE "${anchor}" "${replacement}" content "${content}") + file(WRITE "${path}" "${content}") +endfunction() + +# 1. The plugin directory: a CMakeLists that adds our sources to an +# `input-osmflat` target, plus the header datasource_cache_static.cpp +# includes to see `osmflat_datasource_plugin`. +set(plugin_dir "${MAPNIK_SOURCE_DIR}/plugins/input/osmflat") +file(MAKE_DIRECTORY "${plugin_dir}") +configure_file("${CMAKE_CURRENT_LIST_DIR}/plugin/CMakeLists.txt" "${plugin_dir}/CMakeLists.txt" COPYONLY) +configure_file("${CMAKE_CURRENT_LIST_DIR}/plugin/osmflat_datasource.hpp.in" "${plugin_dir}/osmflat_datasource.hpp" @ONLY) + +# 2. Build it alongside the stock plugins. +osmflat_patch_file(plugins/input/CMakeLists.txt + "if(USE_PLUGIN_INPUT_TILES)" + "if(TARGET osmflat_capi)\n add_subdirectory(osmflat)\n list(APPEND m_build_plugins input-osmflat)\nendif()\nif(USE_PLUGIN_INPUT_TILES)" + "add_subdirectory(osmflat)") + +# 3. Link its sources into libmapnik and define MAPNIK_STATIC_PLUGIN_OSMFLAT. +osmflat_patch_file(src/CMakeLists.txt + " $<$>,$>:input-tiles>\n" + " $<$>,$>:input-tiles>\n $<$>,$>:input-osmflat>\n" + "TARGET_EXISTS:input-osmflat>>:input-osmflat>") +osmflat_patch_file(src/CMakeLists.txt + " $<$>,$>:MAPNIK_STATIC_PLUGIN_TILES>\n" + " $<$>,$>:MAPNIK_STATIC_PLUGIN_TILES>\n $<$>,$>:MAPNIK_STATIC_PLUGIN_OSMFLAT>\n" + "MAPNIK_STATIC_PLUGIN_OSMFLAT") + +# 4. Accept "+proj=longlat ..." without PROJ, classified the way PROJ +# classifies it. +# +# Built with PROJ, `proj_get_type` reports a proj4 longlat string as +# PJ_TYPE_OTHER_CRS, so `is_geographic_` stays false and scale denominators +# come out in degrees -- which is what every style written against this +# plugin is tuned for (a MaxScaleDenominator of 0.1 ~ neighborhood). Without +# PROJ, Mapnik knows only "epsg:4326" (geographic, so ~111319x larger +# denominators) and throws on anything else. Matching PROJ's classification +# here keeps styles renderer-independent; other proj4 strings still throw. +osmflat_patch_file(src/projection.cpp +[==[#ifdef MAPNIK_USE_PROJ + init_proj(); +#else + throw std::runtime_error(std::string("Cannot initialize projection '") + params_ + + " ' without proj support (-DMAPNIK_USE_PROJ)"); +#endif]==] +[==[#ifdef MAPNIK_USE_PROJ + init_proj(); +#else + // osmflat: PROJ reports a proj4 longlat string as a non-geographic + // "other" CRS; mirror that so scale denominators stay in degrees. + if (params_.rfind("+proj=longlat", 0) == 0) + { + is_geographic_ = false; + } + else + { + throw std::runtime_error(std::string("Cannot initialize projection '") + params_ + + " ' without proj support (-DMAPNIK_USE_PROJ)"); + } +#endif]==] +[==[osmflat: PROJ reports a proj4 longlat string]==]) + +# 5. Register it in the static datasource table. +osmflat_patch_file(src/datasource_cache_static.cpp + "#if defined(MAPNIK_STATIC_PLUGIN_TOPOJSON)\n#include \"input/topojson/topojson_datasource.hpp\"\n#endif\n" + "#if defined(MAPNIK_STATIC_PLUGIN_TOPOJSON)\n#include \"input/topojson/topojson_datasource.hpp\"\n#endif\n#if defined(MAPNIK_STATIC_PLUGIN_OSMFLAT)\n#include \"input/osmflat/osmflat_datasource.hpp\"\n#endif\n" + "input/osmflat/osmflat_datasource.hpp") +osmflat_patch_file(src/datasource_cache_static.cpp + " REGISTER_STATIC_DATASOURCE_PLUGIN(topojson_datasource_plugin);\n#endif\n" + " REGISTER_STATIC_DATASOURCE_PLUGIN(topojson_datasource_plugin);\n#endif\n#if defined(MAPNIK_STATIC_PLUGIN_OSMFLAT)\n REGISTER_STATIC_DATASOURCE_PLUGIN(osmflat_datasource_plugin);\n#endif\n" + "REGISTER_STATIC_DATASOURCE_PLUGIN(osmflat_datasource_plugin)") diff --git a/cmake/static-mapnik/plugin/CMakeLists.txt b/cmake/static-mapnik/plugin/CMakeLists.txt new file mode 100644 index 0000000..38fa59e --- /dev/null +++ b/cmake/static-mapnik/plugin/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copied into /plugins/input/osmflat by patch-mapnik.cmake. +# +# With BUILD_SHARED_PLUGINS=OFF, add_plugin_target makes an INTERFACE library +# whose sources compile into libmapnik itself. OSMFLAT_SOURCE_DIR comes from the +# parent project (cmake/StaticRender.cmake). Paths are wrapped in +# $ so Mapnik's install(EXPORT) never sees source-tree paths. +add_plugin_target(input-osmflat "osmflat") + +target_sources(input-osmflat ${_plugin_visibility} + $ + $ + $ + $ +) +target_include_directories(input-osmflat ${_plugin_visibility} + $ + $ +) +target_link_libraries(input-osmflat ${_plugin_visibility} + mapnik::mapnik +) diff --git a/cmake/static-mapnik/plugin/osmflat_datasource.hpp.in b/cmake/static-mapnik/plugin/osmflat_datasource.hpp.in new file mode 100644 index 0000000..4d34689 --- /dev/null +++ b/cmake/static-mapnik/plugin/osmflat_datasource.hpp.in @@ -0,0 +1,13 @@ +// Generated into /plugins/input/osmflat by patch-mapnik.cmake. +// +// datasource_cache_static.cpp includes "input//_datasource.hpp" +// and expects it to declare `_datasource_plugin`. Our plugin class is +// declared in src/osmflat_datasource.cpp (for the dynamic build), so re-declare +// it here with the same macro; the definitions stay in that .cpp. +#pragma once + +#include "@OSMFLAT_SOURCE_DIR@/include/osmflat_datasource.hpp" + +#include + +DATASOURCE_PLUGIN_DEF(osmflat_datasource_plugin, osmflat); diff --git a/scripts/build-static-render.sh b/scripts/build-static-render.sh new file mode 100755 index 0000000..86393cd --- /dev/null +++ b/scripts/build-static-render.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# Build a fully static `render` (musl, Mapnik + osmflat compiled in) inside an +# Alpine container, run the smoke test, and package it with DejaVu fonts. +# +# Run from the repo root, in CI or locally: +# +# docker run --rm -v "$PWD:/src" -w /src alpine:3.22 sh scripts/build-static-render.sh +# podman run --rm -v "$PWD:/src" -w /src alpine:3.22 sh scripts/build-static-render.sh +# +# Output: dist/osmflat-render---linux-musl.tar.gz +set -eu + +BUILD_DIR="${BUILD_DIR:-build-static}" +DIST_DIR="${DIST_DIR:-dist}" + +apk add --no-cache \ + build-base cmake samurai git pkgconf curl ca-certificates linux-headers file \ + rust cargo \ + boost1.84-dev boost1.84-static \ + icu-dev icu-static \ + freetype-dev freetype-static \ + harfbuzz-dev harfbuzz-static graphite2-static glib-static pcre2-static \ + libpng-dev libpng-static zlib-dev zlib-static bzip2-static brotli-static expat-static \ + font-dejavu + +cmake -S . -B "$BUILD_DIR" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DOSMFLAT_STATIC_RENDER=ON \ + -DOSMFLAT_FULLY_STATIC=ON +cmake --build "$BUILD_DIR" --target render --parallel "$(nproc)" +strip "$BUILD_DIR/render" + +file "$BUILD_DIR/render" +if file "$BUILD_DIR/render" | grep -q 'dynamically linked'; then + echo "error: render is dynamically linked" >&2 + exit 1 +fi + +MAPNIK_FONT_DIR=/usr/share/fonts/dejavu ctest --test-dir "$BUILD_DIR" --output-on-failure + +version="$(sed -n 's/^project(osmflat-mapnik-plugin VERSION \([^ )]*\).*/\1/p' CMakeLists.txt)" +name="osmflat-render-${version}-$(uname -m)-linux-musl" +rm -rf "$DIST_DIR/$name" +mkdir -p "$DIST_DIR/$name/fonts" +cp "$BUILD_DIR/render" "$DIST_DIR/$name/" +cp /usr/share/fonts/dejavu/*.ttf "$DIST_DIR/$name/fonts/" +tar -C "$DIST_DIR" -czf "$DIST_DIR/$name.tar.gz" "$name" +ls -lh "$DIST_DIR/$name/render" "$DIST_DIR/$name.tar.gz" diff --git a/test/render.cpp b/test/render.cpp index 41b8f93..a1211f5 100644 --- a/test/render.cpp +++ b/test/render.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -32,6 +33,20 @@ static std::string slurp(const std::string& path) return ss.str(); } +#ifdef OSMFLAT_STATIC_RENDER +// Directory holding the running executable, following symlinks (e.g. a +// /usr/local/bin/render link into an unpacked release tarball). +static std::string executable_dir(const char* argv0) +{ + std::error_code ec; + std::filesystem::path exe = std::filesystem::read_symlink("/proc/self/exe", ec); + if (ec) { + exe = std::filesystem::canonical(argv0, ec); + } + return ec ? std::string(".") : exe.parent_path().string(); +} +#endif + int main(int argc, char** argv) { if (argc != 9 && argc != 11) { @@ -59,12 +74,24 @@ int main(int argc, char** argv) } mapnik::setup(); +#ifdef OSMFLAT_STATIC_RENDER + // osmflat is compiled into this binary's static libmapnik. Loading + // .input modules from disk would pull in a second, shared libmapnik, + // so plugin_dir is accepted (for a stable CLI) but ignored. + (void)plugin_dir; +#else mapnik::datasource_cache::instance().register_datasources(plugin_dir); +#endif // Register bundled DejaVu fonts so TextSymbolizer can resolve face-name. if (const char* fd = std::getenv("MAPNIK_FONT_DIR")) { mapnik::freetype_engine::register_fonts(fd, true); } else { +#ifdef OSMFLAT_STATIC_RENDER + // The release tarball ships DejaVu in fonts/ beside the binary. + mapnik::freetype_engine::register_fonts(executable_dir(argv[0]) + "/fonts", true); +#else mapnik::freetype_engine::register_fonts("/opt/homebrew/lib/mapnik/fonts", true); +#endif } std::string xml = slurp(style_xml); From 365c37eff0e5cde65852ea465ea78b2ed0c4c2e4 Mon Sep 17 00:00:00 2001 From: Boyd Johnson Date: Thu, 17 Sep 2026 13:13:39 -0500 Subject: [PATCH 2/8] Cache the static build tree in CI Mapnik is ~175 TUs and dominates the build (~16 min cold on x86_64). Put everything reusable under $BUILD_DIR -- mapnik's fetched source and objects, the cargo registry (CARGO_HOME), and downloaded apk packages -- so one actions/cache path covers it all. Cache keys are unique per run (GitHub caches are immutable) with restore-keys falling back to the newest cache for that arch; changed build plumbing or a new mapnik tag starts a fresh lineage, and ninja decides what to actually redo. Locally: 468M tree, and a re-run with it in place takes 1m11s (one relink) against ~5 min cold. Co-Authored-By: Claude Opus 5 --- .github/workflows/static-render.yml | 16 ++++++++++++++++ scripts/build-static-render.sh | 11 ++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/static-render.yml b/.github/workflows/static-render.yml index 0300c51..ffc6f7e 100644 --- a/.github/workflows/static-render.yml +++ b/.github/workflows/static-render.yml @@ -27,6 +27,22 @@ jobs: - 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.arch }}-${{ hashFiles('cmake/**', 'scripts/build-static-render.sh', 'CMakeLists.txt') }}-${{ github.run_id }} + restore-keys: | + static-render-${{ matrix.arch }}-${{ hashFiles('cmake/**', 'scripts/build-static-render.sh', 'CMakeLists.txt') }}- + static-render-${{ matrix.arch }}- + # 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 diff --git a/scripts/build-static-render.sh b/scripts/build-static-render.sh index 86393cd..8fba882 100755 --- a/scripts/build-static-render.sh +++ b/scripts/build-static-render.sh @@ -8,12 +8,21 @@ # podman run --rm -v "$PWD:/src" -w /src alpine:3.22 sh scripts/build-static-render.sh # # Output: dist/osmflat-render---linux-musl.tar.gz +# +# Everything reusable lives under $BUILD_DIR (mapnik's fetched source + objects, +# the cargo registry, downloaded apk packages) so CI can cache that one path and +# a second run only relinks what changed. set -eu BUILD_DIR="${BUILD_DIR:-build-static}" DIST_DIR="${DIST_DIR:-dist}" +# Absolute: cmake/cargo are invoked from various working directories. +CARGO_HOME="${CARGO_HOME:-$PWD/$BUILD_DIR/cargo-home}" +APK_CACHE="${APK_CACHE:-$PWD/$BUILD_DIR/apk-cache}" +export CARGO_HOME +mkdir -p "$APK_CACHE" -apk add --no-cache \ +apk add --cache-dir "$APK_CACHE" \ build-base cmake samurai git pkgconf curl ca-certificates linux-headers file \ rust cargo \ boost1.84-dev boost1.84-static \ From fb375eb8e78088ca3acbd5d925a8af72c449b173 Mon Sep 17 00:00:00 2001 From: Boyd Johnson Date: Thu, 17 Sep 2026 13:18:19 -0500 Subject: [PATCH 3/8] Add a release workflow for the static render tool A v* tag builds the static render for x86_64 and aarch64, smoke-tests each unpacked archive on a bare alpine image, and publishes both with a SHA256SUMS. workflow_dispatch builds the same matrix without publishing. Unlike CI, release builds skip the cache: a published binary should come from a clean build of the tagged tree. The tag must match the project VERSION, so archive names can't drift from the release. Archives now carry mapnik's LGPL-2.1 text plus a NOTICE naming every statically linked library and pointing at the sources and build script, which is what the relinking requirement asks for. Co-Authored-By: Claude Opus 5 --- .github/workflows/release.yml | 119 +++++++++++++++++++++++++++++++++ README.md | 10 ++- scripts/build-static-render.sh | 33 ++++++++- 3 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8bc8e84 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,119 @@ +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.arch }}-linux-musl + strategy: + # One arch failing shouldn't hide whether the other is healthy. + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-24.04 + - arch: aarch64 + runner: ubuntu-24.04-arm + 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 + 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 }} + + # 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. + - name: Smoke test the packaged archive + 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.arch }}-linux-musl + 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/* diff --git a/README.md b/README.md index 9df12e1..2c7ae92 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,15 @@ podman run --rm -v "$PWD:/src" -w /src alpine:3.22 sh scripts/build-static-rende ``` `.github/workflows/static-render.yml` runs that same script under `docker run` -for x86_64 and aarch64 and uploads both tarballs. +for x86_64 and aarch64 and uploads both tarballs, caching the build tree +(mapnik is ~16 min cold, ~1 min warm). `.github/workflows/release.yml` builds +the same two 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 diff --git a/scripts/build-static-render.sh b/scripts/build-static-render.sh index 8fba882..12b61a4 100755 --- a/scripts/build-static-render.sh +++ b/scripts/build-static-render.sh @@ -47,11 +47,42 @@ fi MAPNIK_FONT_DIR=/usr/share/fonts/dejavu ctest --test-dir "$BUILD_DIR" --output-on-failure -version="$(sed -n 's/^project(osmflat-mapnik-plugin VERSION \([^ )]*\).*/\1/p' CMakeLists.txt)" +# A tag build passes the tag's version so the archive name matches the release; +# otherwise take the project version. +version="${OSMFLAT_RENDER_VERSION:-$(sed -n 's/^project(osmflat-mapnik-plugin VERSION \([^ )]*\).*/\1/p' CMakeLists.txt)}" name="osmflat-render-${version}-$(uname -m)-linux-musl" rm -rf "$DIST_DIR/$name" mkdir -p "$DIST_DIR/$name/fonts" cp "$BUILD_DIR/render" "$DIST_DIR/$name/" cp /usr/share/fonts/dejavu/*.ttf "$DIST_DIR/$name/fonts/" + +# mapnik is LGPL-2.1 and linked into the binary, so ship its license text and +# say where the sources and relink instructions are. +cp "$BUILD_DIR/_deps/mapnik-src/COPYING" "$DIST_DIR/$name/LICENSE.mapnik" +mapnik_tag="$(git -C "$BUILD_DIR/_deps/mapnik-src" describe --tags --always 2>/dev/null || echo unknown)" +cat > "$DIST_DIR/$name/NOTICE" < Date: Thu, 17 Sep 2026 13:19:35 -0500 Subject: [PATCH 4/8] Dual license MIT / Apache-2.0 The repo had no license file at all. Adds both texts, a README License section, and ships them in the release archive. The static archives are the one wrinkle: Apache-2.0 is considered incompatible with LGPL-2.1, so a redistributed binary with mapnik linked in is the MIT arm of the dual license combined with LGPL mapnik. The NOTICE says so. Building the ordinary osmflat.input module against a system mapnik is dynamic linking and raises none of this. Co-Authored-By: Claude Opus 5 --- LICENSE-APACHE | 201 +++++++++++++++++++++++++++++++++ LICENSE-MIT | 25 ++++ README.md | 19 ++++ scripts/build-static-render.sh | 5 + 4 files changed, 250 insertions(+) create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..0c840ea --- /dev/null +++ b/LICENSE-MIT @@ -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. diff --git a/README.md b/README.md index 2c7ae92..feeb1a8 100644 --- a/README.md +++ b/README.md @@ -511,3 +511,22 @@ 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 + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or + ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or + ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. + +The statically linked `render` archives are a separate case -- see +"Static, self-contained `render`" above. diff --git a/scripts/build-static-render.sh b/scripts/build-static-render.sh index 12b61a4..9935625 100755 --- a/scripts/build-static-render.sh +++ b/scripts/build-static-render.sh @@ -55,6 +55,7 @@ rm -rf "$DIST_DIR/$name" mkdir -p "$DIST_DIR/$name/fonts" cp "$BUILD_DIR/render" "$DIST_DIR/$name/" cp /usr/share/fonts/dejavu/*.ttf "$DIST_DIR/$name/fonts/" +cp LICENSE-APACHE LICENSE-MIT README.md "$DIST_DIR/$name/" # mapnik is LGPL-2.1 and linked into the binary, so ship its license text and # say where the sources and relink instructions are. @@ -63,6 +64,10 @@ mapnik_tag="$(git -C "$BUILD_DIR/_deps/mapnik-src" describe --tags --always 2>/d cat > "$DIST_DIR/$name/NOTICE" < Date: Thu, 17 Sep 2026 13:21:55 -0500 Subject: [PATCH 5/8] License MIT only, not MIT / Apache-2.0 Apache-2.0 is considered incompatible with LGPL-2.1 for a combined work, and the static render links LGPL mapnik. Dual licensing worked (a redistributor would take the MIT arm), but MIT alone leaves nothing to explain. Co-Authored-By: Claude Opus 5 --- LICENSE-APACHE | 201 --------------------------------- README.md | 16 +-- scripts/build-static-render.sh | 6 +- 3 files changed, 8 insertions(+), 215 deletions(-) delete mode 100644 LICENSE-APACHE diff --git a/LICENSE-APACHE b/LICENSE-APACHE deleted file mode 100644 index 16fe87b..0000000 --- a/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/README.md b/README.md index feeb1a8..6d5f279 100644 --- a/README.md +++ b/README.md @@ -514,19 +514,15 @@ See `osmflat-ext`'s README for both build sides and `osmflat_ext::coastline` / ## License -Licensed under either of +MIT ([LICENSE-MIT](LICENSE-MIT) or ). -- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or - ) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or - ) - -at your option. +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, as defined in the Apache-2.0 -license, shall be dual licensed as above, without any additional terms or -conditions. +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. diff --git a/scripts/build-static-render.sh b/scripts/build-static-render.sh index 9935625..4ebbf68 100755 --- a/scripts/build-static-render.sh +++ b/scripts/build-static-render.sh @@ -55,7 +55,7 @@ rm -rf "$DIST_DIR/$name" mkdir -p "$DIST_DIR/$name/fonts" cp "$BUILD_DIR/render" "$DIST_DIR/$name/" cp /usr/share/fonts/dejavu/*.ttf "$DIST_DIR/$name/fonts/" -cp LICENSE-APACHE LICENSE-MIT README.md "$DIST_DIR/$name/" +cp LICENSE-MIT README.md "$DIST_DIR/$name/" # mapnik is LGPL-2.1 and linked into the binary, so ship its license text and # say where the sources and relink instructions are. @@ -64,9 +64,7 @@ mapnik_tag="$(git -C "$BUILD_DIR/_deps/mapnik-src" describe --tags --always 2>/d cat > "$DIST_DIR/$name/NOTICE" < Date: Thu, 17 Sep 2026 13:26:19 -0500 Subject: [PATCH 6/8] Cancel superseded CI runs per branch Four pushes in quick succession each started a full static-render build (~16 min cold on x86_64), all racing to build commits already replaced. A concurrency group per workflow+ref keeps one run per PR branch. main and tags share the group name but never cancel, so a release build can't be killed by a later push. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 7 +++++++ .github/workflows/static-render.yml | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a253667..2d666fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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) diff --git a/.github/workflows/static-render.yml b/.github/workflows/static-render.yml index ffc6f7e..602f2d8 100644 --- a/.github/workflows/static-render.yml +++ b/.github/workflows/static-render.yml @@ -10,6 +10,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: linux-musl: name: Static render (${{ matrix.arch }}-linux-musl) From 29cde859ac111b325212dbb5e4037892be811c9e Mon Sep 17 00:00:00 2001 From: Boyd Johnson Date: Thu, 17 Sep 2026 13:28:54 -0500 Subject: [PATCH 7/8] Fix the CI cache save failing on root-owned build files The build runs as root in the container (apk needs it), and cargo leaves files the runner user can't read, so the post-job cache save died on 'tar: ... Permission denied' -- the first cached run saved nothing. chown the tree back before the cache step. Co-Authored-By: Claude Opus 5 --- .github/workflows/static-render.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/static-render.yml b/.github/workflows/static-render.yml index 602f2d8..f1d0a7a 100644 --- a/.github/workflows/static-render.yml +++ b/.github/workflows/static-render.yml @@ -59,6 +59,12 @@ jobs: docker run --rm -v "$PWD:/src" -w /src alpine:3.22 \ sh scripts/build-static-render.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 + run: sudo chown -R "$(id -u):$(id -g)" build-static dist + - name: Upload uses: actions/upload-artifact@v4 with: From fc9bdd578c21c469858816838493c29d4a9c7f10 Mon Sep 17 00:00:00 2001 From: Boyd Johnson Date: Thu, 17 Sep 2026 14:01:10 -0500 Subject: [PATCH 8/8] Add an aarch64 macOS static render build macOS can't link libSystem statically, so this bundles mapnik and every third-party library and leaves only macOS' own dylibs/frameworks dynamic; the build script fails if otool -L shows anything outside /usr/lib or /System. Three macOS-specific problems: - Homebrew's libharfbuzz.a is built against graphite2, which ships no static archive, so harfbuzz is built from source with just freetype and CoreText (which also drops glib/pcre2/intl). - icu4c, zlib and bzip2 are keg-only, so their prefixes are passed explicitly or nothing finds the archives. - Homebrew's Boost::regex names icu libs bare, which wouldn't resolve and would prefer dylibs; mapnik's own workaround drops them. Packaging moves to scripts/package-render.sh, shared by both platforms. Verified locally on arm64: links only /usr/lib + /System, ctest passes, the unpacked archive renders with its bundled fonts, and output is byte-identical to both the dynamic macOS build and the linux/musl static build (md5 d48d0556925e17e872d0ac25229319d2). Co-Authored-By: Claude Opus 5 --- .github/workflows/release.yml | 26 +++++++-- .github/workflows/static-render.yml | 26 ++++++--- README.md | 15 ++++- cmake/StaticRender.cmake | 86 ++++++++++++++++++++++++++++ scripts/build-static-render-macos.sh | 55 ++++++++++++++++++ scripts/build-static-render.sh | 44 +------------- scripts/package-render.sh | 55 ++++++++++++++++++ test/render.cpp | 18 +++++- 8 files changed, 264 insertions(+), 61 deletions(-) create mode 100755 scripts/build-static-render-macos.sh create mode 100755 scripts/package-render.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8bc8e84..60487e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,16 +22,20 @@ permissions: jobs: build: - name: ${{ matrix.arch }}-linux-musl + name: ${{ matrix.platform }} strategy: - # One arch failing shouldn't hide whether the other is healthy. + # One platform failing shouldn't hide whether the others are healthy. fail-fast: false matrix: include: - - arch: x86_64 + - platform: x86_64-linux-musl runner: ubuntu-24.04 - - arch: aarch64 + - 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: @@ -58,16 +62,26 @@ jobs: # 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 + - 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 ' @@ -80,7 +94,7 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: osmflat-render-${{ matrix.arch }}-linux-musl + name: osmflat-render-${{ matrix.platform }} path: dist/*.tar.gz if-no-files-found: error diff --git a/.github/workflows/static-render.yml b/.github/workflows/static-render.yml index f1d0a7a..338abad 100644 --- a/.github/workflows/static-render.yml +++ b/.github/workflows/static-render.yml @@ -18,16 +18,18 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - linux-musl: - name: Static render (${{ matrix.arch }}-linux-musl) + build: + name: Static render (${{ matrix.platform }}) strategy: fail-fast: false matrix: include: - - arch: x86_64 + - platform: x86_64-linux-musl runner: ubuntu-24.04 - - arch: aarch64 + - platform: aarch64-linux-musl runner: ubuntu-24.04-arm + - platform: aarch64-apple-darwin + runner: macos-14 runs-on: ${{ matrix.runner }} steps: @@ -45,29 +47,35 @@ jobs: uses: actions/cache@v4 with: path: build-static - key: static-render-${{ matrix.arch }}-${{ hashFiles('cmake/**', 'scripts/build-static-render.sh', 'CMakeLists.txt') }}-${{ github.run_id }} + key: static-render-${{ matrix.platform }}-${{ hashFiles('cmake/**', 'scripts/*.sh', 'CMakeLists.txt') }}-${{ github.run_id }} restore-keys: | - static-render-${{ matrix.arch }}-${{ hashFiles('cmake/**', 'scripts/build-static-render.sh', 'CMakeLists.txt') }}- - static-render-${{ matrix.arch }}- + 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 + - 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.arch }}-linux-musl + name: osmflat-render-${{ matrix.platform }} path: dist/*.tar.gz if-no-files-found: error diff --git a/README.md b/README.md index 6d5f279..8006c34 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,15 @@ cmake -S . -B build-static -DCMAKE_BUILD_TYPE=Release \ cmake --build build-static --target render ``` -`OSMFLAT_FULLY_STATIC=ON` adds `-static` (musl only: libc goes in too). The +`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 `` argument stays in the CLI but is ignored — loading a `.input` module would pull in a second, shared mapnik. @@ -98,9 +106,10 @@ podman run --rm -v "$PWD:/src" -w /src alpine:3.22 sh scripts/build-static-rende ``` `.github/workflows/static-render.yml` runs that same script under `docker run` -for x86_64 and aarch64 and uploads both tarballs, caching the build tree +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 two archives from a clean tree on a `v*` tag — the tag has to match +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. diff --git a/cmake/StaticRender.cmake b/cmake/StaticRender.cmake index a0ca0b7..2ddf612 100644 --- a/cmake/StaticRender.cmake +++ b/cmake/StaticRender.cmake @@ -7,6 +7,11 @@ set(OSMFLAT_MAPNIK_GIT_TAG "v4.2.2" CACHE STRING "Mapnik tag to build statically") option(OSMFLAT_FULLY_STATIC "Link render with -static (Linux/musl: no shared libs at all)" OFF) +set(OSMFLAT_HARFBUZZ_GIT_TAG "11.2.1" CACHE STRING "harfbuzz tag built from source on macOS") + +if(APPLE AND OSMFLAT_FULLY_STATIC) + message(FATAL_ERROR "OSMFLAT_FULLY_STATIC needs musl; macOS always links libSystem dynamically") +endif() # --- Mapnik build options ---------------------------------------------------- # Only what `render` uses: the AGG renderer writing PNG, fonts via FreeType + @@ -65,6 +70,75 @@ endforeach() # Static dependency archives (libicuuc.a, libfreetype.a, ...) rather than .so. set(Boost_USE_STATIC_LIBS ON) set(ICU_USE_STATIC_LIBS ON) + +if(APPLE) + # Everything third-party goes in; only macOS' own dylibs stay dynamic. + set(CMAKE_FIND_LIBRARY_SUFFIXES .a) + # Homebrew's Boost::regex names icu libs as bare `icuuc`/`icudata`/... which + # the linker can't resolve (icu4c is keg-only) and which would anyway pick + # the dylib over the archive. Mapnik's own workaround drops them; it links + # ICU::uc/data/i18n itself, by full path to the .a. + set(USE_BOOST_REGEX_ICU_WORKAROUND ON CACHE BOOL "" FORCE) + # Homebrew keeps icu4c, zlib and bzip2 keg-only: their archives aren't + # symlinked into the prefix, so nothing finds libicuuc.a / libz.a / + # libbz2.a without being told where they live. + find_program(OSMFLAT_BREW brew) + if(OSMFLAT_BREW) + if(NOT ICU_ROOT AND NOT DEFINED ENV{ICU_ROOT}) + # Homebrew versions icu4c; take whichever of these is installed. + foreach(icu_formula icu4c@76 icu4c@77 icu4c) + if(NOT ICU_ROOT) + execute_process(COMMAND ${OSMFLAT_BREW} --prefix ${icu_formula} + OUTPUT_VARIABLE ICU_ROOT OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + endif() + endforeach() + endif() + foreach(keg zlib bzip2) + execute_process(COMMAND ${OSMFLAT_BREW} --prefix ${keg} + OUTPUT_VARIABLE keg_prefix OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if(keg_prefix) + list(APPEND CMAKE_PREFIX_PATH "${keg_prefix}") + endif() + endforeach() + endif() + + # Homebrew's libharfbuzz.a is built against glib and graphite2, and + # graphite2 has no static archive -- so a binary using it would still need + # Homebrew at runtime. Build harfbuzz ourselves instead, with just freetype + # and CoreText, which drops glib/graphite2/pcre2/intl entirely. + set(HB_HAVE_FREETYPE ON CACHE BOOL "" FORCE) + set(HB_HAVE_CORETEXT ON CACHE BOOL "" FORCE) + set(HB_HAVE_GLIB OFF CACHE BOOL "" FORCE) + set(HB_HAVE_GRAPHITE2 OFF CACHE BOOL "" FORCE) + set(HB_HAVE_ICU OFF CACHE BOOL "" FORCE) + set(HB_BUILD_SUBSET OFF CACHE BOOL "" FORCE) + set(HB_BUILD_UTILS OFF CACHE BOOL "" FORCE) + set(HB_BUILD_TESTS OFF CACHE BOOL "" FORCE) + FetchContent_Declare( + harfbuzz + GIT_REPOSITORY https://github.com/harfbuzz/harfbuzz.git + GIT_TAG ${OSMFLAT_HARFBUZZ_GIT_TAG} + GIT_SHALLOW TRUE + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(harfbuzz) + + # Mapnik looks for harfbuzz with find_package(CONFIG) and expects a + # harfbuzz::harfbuzz target. Point that lookup at a generated config that + # aliases the target we just built, so it never sees Homebrew's (which + # would also collide on the target name). + set(shim "${CMAKE_BINARY_DIR}/harfbuzz-config-shim") + file(WRITE "${shim}/harfbuzzConfig.cmake" +"set(harfbuzz_FOUND TRUE) +set(harfbuzz_VERSION \"${OSMFLAT_HARFBUZZ_GIT_TAG}\") +if(NOT TARGET harfbuzz::harfbuzz) + add_library(harfbuzz::harfbuzz ALIAS harfbuzz) +endif() +") + set(harfbuzz_DIR "${shim}" CACHE PATH "" FORCE) +endif() if(OSMFLAT_FULLY_STATIC) set(CMAKE_FIND_LIBRARY_SUFFIXES .a) # --static so pkg-config also reports Libs.private (harfbuzz needs glib, @@ -124,6 +198,18 @@ add_executable(render test/render.cpp) # mapnik::mapnik before osmflat_capi: libmapnik.a references the Rust symbols, # and single-pass linkers resolve left to right. target_link_libraries(render PRIVATE mapnik::mapnik osmflat_capi) +if(APPLE) + # Homebrew's libfreetype.a carries its bzip2/brotli font decompressors, but + # FindFreetype reports only the archive itself. + find_library(OSMFLAT_BZ2_LIBRARY NAMES bz2) + find_library(OSMFLAT_BROTLIDEC_LIBRARY NAMES brotlidec) + find_library(OSMFLAT_BROTLICOMMON_LIBRARY NAMES brotlicommon) + foreach(lib OSMFLAT_BZ2_LIBRARY OSMFLAT_BROTLIDEC_LIBRARY OSMFLAT_BROTLICOMMON_LIBRARY) + if(${lib}) + target_link_libraries(render PRIVATE ${${lib}}) + endif() + endforeach() +endif() target_compile_definitions(render PRIVATE OSMFLAT_STATIC_RENDER) if(OSMFLAT_FULLY_STATIC) target_link_options(render PRIVATE -static) diff --git a/scripts/build-static-render-macos.sh b/scripts/build-static-render-macos.sh new file mode 100755 index 0000000..a4e0ec3 --- /dev/null +++ b/scripts/build-static-render-macos.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Build a self-contained `render` on macOS: mapnik and every third-party +# library linked in, leaving only macOS' own dylibs/frameworks dynamic (libSystem +# can't be linked statically). Run from the repo root on an arm64 or x86_64 Mac: +# +# sh scripts/build-static-render-macos.sh +# +# Output: dist/osmflat-render---apple-darwin.tar.gz +set -euo pipefail + +BUILD_DIR="${BUILD_DIR:-build-static}" +DIST_DIR="${DIST_DIR:-dist}" +# Binaries built on a newer runner won't start on older macOS unless a +# deployment target is declared. Homebrew's bottles target the runner's OS, so +# this can't go below it. +MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-14.0}" +export MACOSX_DEPLOYMENT_TARGET + +# No auto-update / no upgrading of already-installed formulae: this script +# runs on developer machines too, and shouldn't reshuffle someone's Homebrew. +export HOMEBREW_NO_AUTO_UPDATE=1 +export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 +brew install --quiet boost icu4c@76 freetype libpng bzip2 zlib ninja cmake >/dev/null + +# DejaVu is what the styles ask for by face-name, and macOS doesn't ship it. +# Pinned rather than taken from a cask, so both platforms package the same +# font release. +fonts_dir="$BUILD_DIR/dejavu" +if [ ! -d "$fonts_dir" ]; then + mkdir -p "$fonts_dir" + curl -fsSL https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-fonts-ttf-2.37.tar.bz2 \ + | tar xj -C "$fonts_dir" --strip-components=2 'dejavu-fonts-ttf-2.37/ttf/*.ttf' +fi + +cmake -S . -B "$BUILD_DIR" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DOSMFLAT_STATIC_RENDER=ON \ + -DCMAKE_OSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" +cmake --build "$BUILD_DIR" --target render --parallel "$(sysctl -n hw.ncpu)" +strip -S -x "$BUILD_DIR/render" + +# Nothing outside /usr/lib and /System may be linked, or the binary only runs +# on a machine with Homebrew in the same place. +otool -L "$BUILD_DIR/render" +if otool -L "$BUILD_DIR/render" | tail -n +2 | grep -vE '^\s+(/usr/lib/|/System/)'; then + echo "error: render links a non-system library (see above)" >&2 + exit 1 +fi + +MAPNIK_FONT_DIR="$fonts_dir" ctest --test-dir "$BUILD_DIR" --output-on-failure + +# uname says arm64; name the archive the way the toolchain triple does. +arch="$(uname -m)" +[ "$arch" = arm64 ] && arch=aarch64 +sh scripts/package-render.sh "$BUILD_DIR" "$DIST_DIR" "${arch}-apple-darwin" "$fonts_dir" diff --git a/scripts/build-static-render.sh b/scripts/build-static-render.sh index 4ebbf68..489cddb 100755 --- a/scripts/build-static-render.sh +++ b/scripts/build-static-render.sh @@ -8,6 +8,7 @@ # podman run --rm -v "$PWD:/src" -w /src alpine:3.22 sh scripts/build-static-render.sh # # Output: dist/osmflat-render---linux-musl.tar.gz +# (packaging itself lives in scripts/package-render.sh, shared with macOS) # # Everything reusable lives under $BUILD_DIR (mapnik's fetched source + objects, # the cargo registry, downloaded apk packages) so CI can cache that one path and @@ -47,45 +48,4 @@ fi MAPNIK_FONT_DIR=/usr/share/fonts/dejavu ctest --test-dir "$BUILD_DIR" --output-on-failure -# A tag build passes the tag's version so the archive name matches the release; -# otherwise take the project version. -version="${OSMFLAT_RENDER_VERSION:-$(sed -n 's/^project(osmflat-mapnik-plugin VERSION \([^ )]*\).*/\1/p' CMakeLists.txt)}" -name="osmflat-render-${version}-$(uname -m)-linux-musl" -rm -rf "$DIST_DIR/$name" -mkdir -p "$DIST_DIR/$name/fonts" -cp "$BUILD_DIR/render" "$DIST_DIR/$name/" -cp /usr/share/fonts/dejavu/*.ttf "$DIST_DIR/$name/fonts/" -cp LICENSE-MIT README.md "$DIST_DIR/$name/" - -# mapnik is LGPL-2.1 and linked into the binary, so ship its license text and -# say where the sources and relink instructions are. -cp "$BUILD_DIR/_deps/mapnik-src/COPYING" "$DIST_DIR/$name/LICENSE.mapnik" -mapnik_tag="$(git -C "$BUILD_DIR/_deps/mapnik-src" describe --tags --always 2>/dev/null || echo unknown)" -cat > "$DIST_DIR/$name/NOTICE" <-.tar.gz +# with the fonts it falls back to and the licenses it has to carry. +# +# package-render.sh +# +# A tag build passes OSMFLAT_RENDER_VERSION so the archive name matches the +# release; otherwise the project version is used. +set -eu + +BUILD_DIR="$1" +DIST_DIR="$2" +PLATFORM="$3" +FONTS_DIR="$4" + +version="${OSMFLAT_RENDER_VERSION:-$(sed -n 's/^project(osmflat-mapnik-plugin VERSION \([^ )]*\).*/\1/p' CMakeLists.txt)}" +name="osmflat-render-${version}-${PLATFORM}" +rm -rf "$DIST_DIR/$name" +mkdir -p "$DIST_DIR/$name/fonts" +cp "$BUILD_DIR/render" "$DIST_DIR/$name/" +cp "$FONTS_DIR"/*.ttf "$DIST_DIR/$name/fonts/" +cp LICENSE-MIT README.md "$DIST_DIR/$name/" + +# mapnik is LGPL-2.1 and linked into the binary, so ship its license text and +# say where the sources and relink instructions are. +cp "$BUILD_DIR/_deps/mapnik-src/COPYING" "$DIST_DIR/$name/LICENSE.mapnik" +mapnik_tag="$(git -C "$BUILD_DIR/_deps/mapnik-src" describe --tags --always 2>/dev/null || echo unknown)" +cat > "$DIST_DIR/$name/NOTICE" < #include +#ifdef __APPLE__ +#include +#include +#endif #include #include #include @@ -39,8 +43,20 @@ static std::string slurp(const std::string& path) static std::string executable_dir(const char* argv0) { std::error_code ec; - std::filesystem::path exe = std::filesystem::read_symlink("/proc/self/exe", ec); + std::filesystem::path exe; +#ifdef __APPLE__ + char buf[PATH_MAX]; + uint32_t size = sizeof(buf); + if (_NSGetExecutablePath(buf, &size) == 0) { + exe = std::filesystem::canonical(buf, ec); + } else { + ec = std::make_error_code(std::errc::filename_too_long); + } +#else + exe = std::filesystem::read_symlink("/proc/self/exe", ec); +#endif if (ec) { + ec.clear(); exe = std::filesystem::canonical(argv0, ec); } return ec ? std::string(".") : exe.parent_path().string();