From 7458ee1f75df82e401de824cd890342ff8e6f6c8 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Thu, 16 Apr 2026 23:17:33 +0200 Subject: [PATCH 01/50] linux works --- .gitignore | 5 + .pre-commit-config.yaml | 17 + CMakeLists.txt | 5 + docs/.vitepress/config.js | 10 +- docs/index.md | 4 + docs/python/api.md | 243 +++++ docs/python/development.md | 261 ++++++ pyproject.toml | 103 +++ src/python/CMakeLists.txt | 98 ++ src/python/README.md | 68 ++ src/python/include/python/engineconfig_nb.hpp | 19 + .../python/parameters/baseparameter_nb.hpp | 28 + .../python/parameters/matchparameter_nb.hpp | 23 + .../python/parameters/nearestparameter_nb.hpp | 8 + .../python/parameters/routeparameter_nb.hpp | 43 + .../python/parameters/tableparameter_nb.hpp | 31 + .../python/parameters/tileparameter_nb.hpp | 8 + .../python/parameters/tripparameter_nb.hpp | 29 + .../include/python/types/approach_nb.hpp | 20 + .../include/python/types/bearing_nb.hpp | 8 + .../include/python/types/coordinate_nb.hpp | 8 + .../include/python/types/jsoncontainer_nb.hpp | 120 +++ .../include/python/types/optional_nb.hpp | 8 + .../include/python/utility/osrm_utility.hpp | 19 + .../include/python/utility/param_utility.hpp | 101 +++ src/python/osrm/__init__.py | 15 + src/python/osrm/__main__.py | 72 ++ src/python/osrm/osrm_ext.pyi | 855 ++++++++++++++++++ src/python/src/engineconfig_nb.cpp | 68 ++ src/python/src/osrm_nb.cpp | 260 ++++++ .../src/parameters/baseparameter_nb.cpp | 124 +++ .../src/parameters/matchparameter_nb.cpp | 130 +++ .../src/parameters/nearestparameter_nb.cpp | 71 ++ .../src/parameters/routeparameter_nb.cpp | 195 ++++ .../src/parameters/tableparameter_nb.cpp | 175 ++++ .../src/parameters/tileparameter_nb.cpp | 57 ++ .../src/parameters/tripparameter_nb.cpp | 147 +++ src/python/src/types/approach_nb.cpp | 27 + src/python/src/types/bearing_nb.cpp | 25 + src/python/src/types/coordinate_nb.cpp | 55 ++ src/python/src/types/jsoncontainer_nb.cpp | 65 ++ src/python/src/types/optional_nb.cpp | 8 + src/python/src/utility/osrm_utility.cpp | 142 +++ src/python/src/utility/param_utility.cpp | 107 +++ test/data/windows-build-test-data.bat | 44 + test/python/constants.py | 18 + test/python/test_index.py | 118 +++ test/python/test_match.py | 244 +++++ test/python/test_nearest.py | 16 + test/python/test_route.py | 287 ++++++ test/python/test_table.py | 141 +++ test/python/test_tile.py | 26 + test/python/test_trip.py | 110 +++ 53 files changed, 4887 insertions(+), 2 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 docs/python/api.md create mode 100644 docs/python/development.md create mode 100755 pyproject.toml create mode 100644 src/python/CMakeLists.txt create mode 100644 src/python/README.md create mode 100644 src/python/include/python/engineconfig_nb.hpp create mode 100644 src/python/include/python/parameters/baseparameter_nb.hpp create mode 100644 src/python/include/python/parameters/matchparameter_nb.hpp create mode 100644 src/python/include/python/parameters/nearestparameter_nb.hpp create mode 100644 src/python/include/python/parameters/routeparameter_nb.hpp create mode 100644 src/python/include/python/parameters/tableparameter_nb.hpp create mode 100644 src/python/include/python/parameters/tileparameter_nb.hpp create mode 100644 src/python/include/python/parameters/tripparameter_nb.hpp create mode 100644 src/python/include/python/types/approach_nb.hpp create mode 100644 src/python/include/python/types/bearing_nb.hpp create mode 100644 src/python/include/python/types/coordinate_nb.hpp create mode 100644 src/python/include/python/types/jsoncontainer_nb.hpp create mode 100644 src/python/include/python/types/optional_nb.hpp create mode 100644 src/python/include/python/utility/osrm_utility.hpp create mode 100644 src/python/include/python/utility/param_utility.hpp create mode 100755 src/python/osrm/__init__.py create mode 100644 src/python/osrm/__main__.py create mode 100644 src/python/osrm/osrm_ext.pyi create mode 100644 src/python/src/engineconfig_nb.cpp create mode 100644 src/python/src/osrm_nb.cpp create mode 100644 src/python/src/parameters/baseparameter_nb.cpp create mode 100644 src/python/src/parameters/matchparameter_nb.cpp create mode 100644 src/python/src/parameters/nearestparameter_nb.cpp create mode 100644 src/python/src/parameters/routeparameter_nb.cpp create mode 100644 src/python/src/parameters/tableparameter_nb.cpp create mode 100644 src/python/src/parameters/tileparameter_nb.cpp create mode 100644 src/python/src/parameters/tripparameter_nb.cpp create mode 100644 src/python/src/types/approach_nb.cpp create mode 100644 src/python/src/types/bearing_nb.cpp create mode 100644 src/python/src/types/coordinate_nb.cpp create mode 100644 src/python/src/types/jsoncontainer_nb.cpp create mode 100644 src/python/src/types/optional_nb.cpp create mode 100644 src/python/src/utility/osrm_utility.cpp create mode 100644 src/python/src/utility/param_utility.cpp create mode 100644 test/data/windows-build-test-data.bat create mode 100755 test/python/constants.py create mode 100755 test/python/test_index.py create mode 100755 test/python/test_match.py create mode 100755 test/python/test_nearest.py create mode 100755 test/python/test_route.py create mode 100755 test/python/test_table.py create mode 100755 test/python/test_tile.py create mode 100755 test/python/test_trip.py diff --git a/.gitignore b/.gitignore index f72889eae4..680ceab6ca 100644 --- a/.gitignore +++ b/.gitignore @@ -111,6 +111,11 @@ debug.lua ########################## lib/binding_napi_v8 +# Python +########################## +__pycache__/ +*.whl + # documentation related files ########################## .vitepress/cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..0bd24ab61a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/pre-commit/mirrors-clang-format + # matches (more or less) the current clang-format on OSRM CI + # TODO(nils): we should change to pypi's clang tools for reproducibility + rev: v18.1.8 + hooks: + - id: clang-format + types_or: [c, c++] + files: ^src/python/ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.5 + hooks: + - id: ruff + args: [--fix] + files: ^(src/python/|test/python/) + - id: ruff-format + files: ^(src/python/|test/python/) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d48c24da6..b51be76f37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ option(ENABLE_DEBUG_LOGGING "Use debug logging in release mode" OFF) option(ENABLE_FUZZING "Fuzz testing using LLVM's libFuzzer" OFF) option(ENABLE_LTO "Use Link Time Optimisation" ON) option(ENABLE_NODE_BINDINGS "Build NodeJs bindings" OFF) +option(ENABLE_PYTHON_BINDINGS "Build Python bindings" OFF) option(ENABLE_SANITIZER "Use memory sanitizer for Debug build" OFF) if (ENABLE_CONAN) @@ -719,6 +720,10 @@ if (ENABLE_NODE_BINDINGS) add_subdirectory(src/nodejs) endif() +if (ENABLE_PYTHON_BINDINGS) + add_subdirectory(src/python) +endif() + if (ENABLE_FUZZING) # Requires libosrm being built with sanitizers; make configurable and default to ubsan set(FUZZ_SANITIZER "undefined" CACHE STRING "Sanitizer to be used for Fuzz testing") diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 1c2a310a5f..ad96720fc2 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -14,7 +14,11 @@ export default defineConfig({ nav: [ { text: 'Home', link: '/' }, { text: 'HTTP API', link: '/http' }, - { text: 'Node.js API', link: '/nodejs/api' } + { text: 'Node.js API', link: '/nodejs/api' }, + { text: 'Python', items: [ + { text: 'API', link: '/python/api' }, + { text: 'Development', link: '/python/development' } + ]} ], sidebar: [ @@ -24,7 +28,8 @@ export default defineConfig({ { text: 'Tool options', link: '/tools'}, { text: 'Developing', link: '/developing' }, { text: 'Testing', link: '/testing' }, - { text: 'Releasing', link: '/releasing' } + { text: 'Releasing', link: '/releasing' }, + { text: 'Python Development', link: '/python/development' } ] }, { @@ -33,6 +38,7 @@ export default defineConfig({ { text: 'HTTP API', link: '/http' }, { text: 'Node.js API', link: '/nodejs/api' }, { text: 'libosrm C++ API', link: '/libosrm' }, + { text: 'Python API', link: '/python/api' }, ] }, { diff --git a/docs/index.md b/docs/index.md index f615efabd8..b7a029b28e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,6 +12,9 @@ hero: - theme: alt text: Node.js API link: /nodejs/api + - theme: alt + text: Python API + link: /python/api - theme: alt text: View on GitHub link: https://github.com/Project-OSRM/osrm-backend @@ -46,6 +49,7 @@ OSRM provides powerful routing services through both HTTP and Node.js APIs: - **[HTTP API](./http.md)** - RESTful API for routing services - **[Node.js API](./nodejs/api.md)** - Native Node.js bindings for embedded use +- **[Python API](./python/api.md)** - Python bindings via nanobind ([Development Guide](./python/development.md)) ## Documentation diff --git a/docs/python/api.md b/docs/python/api.md new file mode 100644 index 0000000000..0346c51c5f --- /dev/null +++ b/docs/python/api.md @@ -0,0 +1,243 @@ +# Python API + +The Python bindings provide access to OSRM's routing services through the `osrm` package. Install with `pip install osrm-bindings`. + +## OSRM + +The `OSRM` class is the main entry point. It requires a `.osrm.*` dataset prepared by the OSRM toolchain. + +```python +import osrm + +# From file +engine = osrm.OSRM("path/to/data.osrm") + +# With keyword arguments +engine = osrm.OSRM( + storage_config="path/to/data.osrm", + algorithm="CH", # or "MLD" + use_shared_memory=False, + max_locations_trip=3, + max_locations_viaroute=3, + max_locations_distance_table=3, + max_locations_map_matching=3, + max_results_nearest=1, + max_alternatives=1, + default_radius="unlimited", +) + +# Using shared memory (requires osrm-datastore) +engine = osrm.OSRM(use_shared_memory=True) +``` + +### Parameters + +- **`storage_config`** `str` - Path to the `.osrm` dataset. +- **`algorithm`** `str` - Routing algorithm: `"CH"` or `"MLD"`. Default: `"CH"`. +- **`use_shared_memory`** `bool` - Connect to shared memory datastore. Default: `True`. +- **`dataset_name`** `str` - Named shared memory dataset (requires `osrm-datastore --dataset_name`). +- **`memory_file`** `str` - **Deprecated.** Equivalent to `use_mmap=True`. +- **`use_mmap`** `bool` - Memory-map files instead of loading into RAM. +- **`max_locations_trip`** `int` - Max locations in trip queries. +- **`max_locations_viaroute`** `int` - Max locations in route queries. +- **`max_locations_distance_table`** `int` - Max locations in table queries. +- **`max_locations_map_matching`** `int` - Max locations in match queries. +- **`max_results_nearest`** `int` - Max results in nearest queries. +- **`max_alternatives`** `int` - Max alternative routes. +- **`default_radius`** `float | "unlimited"` - Default search radius in meters. + +### Services + +All service methods take a parameters object and return a dict-like `Object`: + +```python +result = engine.Route(route_params) +print(result["routes"]) +print(result["waypoints"]) +``` + +## Route + +Finds the fastest route between two or more coordinates. + +```python +params = osrm.RouteParameters( + coordinates=[(7.41337, 43.72956), (7.41546, 43.73077)], + steps=True, + alternatives=2, + annotations=["speed", "duration"], + geometries="geojson", + overview="full", +) +result = engine.Route(params) +``` + +### RouteParameters + +Inherits all [BaseParameters](#baseparameters). + +- **`steps`** `bool` - Return route steps for each leg. Default: `False`. +- **`alternatives`** `int` - Number of alternative routes to search for. Default: `0`. +- **`annotations`** `list[str]` - Additional metadata: `"none"`, `"duration"`, `"nodes"`, `"distance"`, `"weight"`, `"datasources"`, `"speed"`, `"all"`. Default: `[]`. +- **`geometries`** `str` - Geometry format: `"polyline"`, `"polyline6"`, `"geojson"`. Default: `"polyline"`. +- **`overview`** `str` - Overview geometry: `"simplified"`, `"full"`, `"false"`. Default: `"simplified"`. +- **`continue_straight`** `bool | None` - Force route to continue straight at waypoints. +- **`waypoints`** `list[int]` - Indices of coordinates to treat as waypoints. Must include first and last. + +## Table + +Computes duration/distance matrices between coordinates. + +```python +params = osrm.TableParameters( + coordinates=[(7.41337, 43.72956), (7.41546, 43.73077), (7.41862, 43.73216)], + sources=[0], + destinations=[1, 2], + annotations=["duration", "distance"], +) +result = engine.Table(params) +``` + +### TableParameters + +Inherits all [BaseParameters](#baseparameters). + +- **`sources`** `list[int]` - Indices of source coordinates. Default: all. +- **`destinations`** `list[int]` - Indices of destination coordinates. Default: all. +- **`annotations`** `list[str]` - `"duration"`, `"distance"`, `"all"`. Default: `["duration"]`. +- **`fallback_speed`** `float` - Speed for crow-flies fallback when no route found. +- **`fallback_coordinate_type`** `str` - `"input"` or `"snapped"`. +- **`scale_factor`** `float` - Scales duration values. Default: `1.0`. + +## Nearest + +Finds the nearest street segment for a coordinate. + +```python +params = osrm.NearestParameters( + coordinates=[(7.41337, 43.72956)], + number_of_results=3, +) +result = engine.Nearest(params) +``` + +### NearestParameters + +Inherits all [BaseParameters](#baseparameters). + +- **`number_of_results`** `int` - Number of nearest segments to return. Default: `1`. + +## Match + +Snaps noisy GPS traces to the road network. + +```python +params = osrm.MatchParameters( + coordinates=[(7.41337, 43.72956), (7.41546, 43.73077), (7.41862, 43.73216)], + timestamps=[1424684612, 1424684616, 1424684620], + radiuses=[5.0, 5.0, 5.0], + annotations=["speed"], + geometries="geojson", +) +result = engine.Match(params) +``` + +### MatchParameters + +Inherits all [RouteParameters](#routeparameters) and [BaseParameters](#baseparameters). + +- **`timestamps`** `list[int]` - UNIX timestamps for each coordinate. +- **`gaps`** `str` - Gap handling: `"split"` or `"ignore"`. Default: `"split"`. +- **`tidy`** `bool` - Remove duplicates. Default: `False`. +- **`waypoints`** `list[int]` - Indices of coordinates to treat as waypoints. + +## Trip + +Solves the Traveling Salesman Problem for the given coordinates. + +```python +params = osrm.TripParameters( + coordinates=[(7.41337, 43.72956), (7.41546, 43.73077), (7.41862, 43.73216)], + source="first", + destination="last", + roundtrip=True, + annotations=["duration"], + geometries="geojson", +) +result = engine.Trip(params) +``` + +### TripParameters + +Inherits all [RouteParameters](#routeparameters) and [BaseParameters](#baseparameters). + +- **`source`** `str` - `"any"` or `"first"`. Default: `"any"`. +- **`destination`** `str` - `"any"` or `"last"`. Default: `"any"`. +- **`roundtrip`** `bool` - Return to first location. Default: `True`. + +## Tile + +Generates vector tiles with internal routing graph data. + +```python +params = osrm.TileParameters(x=17059, y=11948, z=15) +result = engine.Tile(params) # returns bytes +``` + +### TileParameters + +- **`x`** `int` - Tile x coordinate. +- **`y`** `int` - Tile y coordinate. +- **`z`** `int` - Tile zoom level. + +## BaseParameters + +Shared parameters inherited by Nearest, Table, Route, Match, and Trip. + +- **`coordinates`** `list[tuple[float, float]]` - List of `(longitude, latitude)` pairs. +- **`hints`** `list[str | None]` - Base64-encoded hints from previous requests. +- **`radiuses`** `list[float | None]` - Search radius per coordinate in meters. `None` for unlimited. +- **`bearings`** `list[tuple[int, int] | None]` - `(bearing, range)` pairs in degrees. `None` for unrestricted. +- **`approaches`** `list[str | None]` - `"curb"`, `"unrestricted"`, or `None`. +- **`generate_hints`** `bool` - Include hints in response. Default: `True`. +- **`exclude`** `list[str]` - Road classes to avoid (e.g. `["motorway"]`). +- **`snapping`** `str` - `"default"` or `"any"`. Default: `"default"`. + +## Types + +### Coordinate + +```python +coord = osrm.Coordinate((7.41337, 43.72956)) +print(coord.lon, coord.lat) +``` + +### Bearing + +```python +bearing = osrm.Bearing((200, 180)) +print(bearing.bearing, bearing.range) +``` + +### Object / Array + +Service results are returned as `Object` (dict-like) and `Array` (list-like) wrappers around OSRM's internal JSON types. They support `[]`, `len()`, `in`, and iteration. + +```python +result = engine.Route(params) +for route in result["routes"]: + print(route["distance"], route["duration"]) +``` + +## CLI + +The package also installs OSRM command-line tools, accessible via `python -m osrm`: + +```bash +python -m osrm extract data.osm.pbf -p profiles/car.lua +python -m osrm contract data.osrm +python -m osrm partition data.osrm +python -m osrm customize data.osrm +python -m osrm datastore data.osrm +python -m osrm routed data.osrm +``` diff --git a/docs/python/development.md b/docs/python/development.md new file mode 100644 index 0000000000..00226534a7 --- /dev/null +++ b/docs/python/development.md @@ -0,0 +1,261 @@ +# Python Bindings Development Guide + +## Installing for production + +Pre-built wheels are published to PyPI for Linux (x86\_64, aarch64), macOS (arm64), and Windows (amd64), requiring Python 3.12+: + +```bash +pip install osrm-bindings +``` + +To build from source (e.g. unsupported platform): + +```bash +pip install osrm-bindings --no-binary osrm-bindings +``` + +Source builds compile the full OSRM C++ library — this takes a long time. +See [platform-specific notes](#platform-specific-build-requirements) for prerequisites. + +## Installing for development + +Clone the repo and install in editable mode with dev dependencies: + +```bash +git clone https://github.com/Project-OSRM/osrm-backend +cd osrm-backend +pip install -e ".[dev]" +``` + +Install pre-commit hooks: + +```bash +pre-commit install +``` + +## Platform-specific build requirements + +### Linux + +No extra steps needed. Wheels are built inside a custom manylinux image +that has all OSRM dependencies baked in. A regular source install will pull +OSRM's dependencies via the system package manager or build them from source. + +### macOS + +Install OSRM's C++ dependencies via Homebrew: + +```bash +brew install lua tbb boost@1.90 +brew link boost@1.90 +``` + +### Windows + +Windows uses [Conan](https://conan.io/) for OSRM's C++ dependencies. Install +it and generate a default profile before building: + +```bash +pip install conan==2.27.0 +conan profile detect --force +``` + +Pass `ENABLE_CONAN=ON` to CMake at build time (see below). + +## Building locally + +### Editable install (recommended for development) + +A standard `pip install -e .` works, but by default pip uses PEP 517 isolated +builds — each invocation creates a temporary directory, compiles everything, +then discards it. This means OSRM is recompiled from scratch every time. + +Use `--no-build-isolation` to make scikit-build-core reuse the persistent +build directory (`build/{wheel_tag}/`) across runs: + +```bash +# Linux / macOS +pip install -e . --no-build-isolation + +# Windows +pip install -e . --no-build-isolation -C cmake.define.ENABLE_CONAN=ON +``` + +The first run is slow (full OSRM compile). Subsequent runs only recompile +changed binding files. + +::: warning Keep config flags identical across runs +scikit-build-core hashes its configuration to detect changes. If the flags +differ between runs, it wipes the build directory and starts from scratch. +::: + +::: warning Generator mismatch +CMake records the generator in `CMakeCache.txt`. If you ever see +`Does not match the generator used previously`, delete the build directory +and rebuild from scratch: +```powershell +Remove-Item -Recurse -Force build/cp312-abi3-win_amd64 +``` +::: + +### Building a wheel + +After the editable install has compiled everything, produce a wheel without +recompiling: + +```bash +# Linux / macOS +pip wheel . --no-build-isolation -w dist + +# Windows +pip wheel . --no-build-isolation -C cmake.define.ENABLE_CONAN=ON -w dist +``` + +CMake finds the existing artifacts in the build directory and skips +recompilation. The wheel lands in `dist/`. + +### Wheel repair + +Locally built wheels link against system shared libraries and are tagged +as `linux_x86_64` (not `manylinux`). To make them portable or to inspect +their dependencies, use the platform-specific repair tools: + +**Linux** — [auditwheel](https://github.com/pypa/auditwheel): + +```bash +pip install auditwheel +auditwheel show dist/*.whl # inspect shared library dependencies +auditwheel repair -w dist dist/*.whl # bundle libs and retag as manylinux +``` + +**macOS** — [delocate](https://github.com/matthew-brett/delocate): + +```bash +pip install delocate +delocate-listdeps dist/*.whl # inspect dependencies +delocate-wheel -w dist dist/*.whl # bundle dylibs +``` + +**Windows** — [delvewheel](https://github.com/adang1345/delvewheel): + +```bash +pip install delvewheel +delvewheel show dist/*.whl # inspect dependencies +delvewheel repair -w dist dist/*.whl +``` + +On Windows, Conan's shared DLLs (tbb, hwloc) must be on PATH for delvewheel +to find them. Activate the `conanrun.bat` generated in the build tree first. + +::: tip +cibuildwheel runs wheel repair automatically in CI. You only need these +commands when building wheels locally for distribution. +::: + +### Compiler cache + +On Linux and macOS, ccache is used automatically (pre-installed in the +manylinux image; installed via Homebrew for macOS CI). + +On Windows, scikit-build-core defaults to the **Visual Studio generator**, +which does not support `CMAKE_CXX_COMPILER_LAUNCHER`. The build dir reuse +from `--no-build-isolation` is the main speed optimisation for local Windows +development. + +## Running tests + +Build the test data (requires the package to be installed so the `osrm` +executables are available): + +```bash +# Linux / macOS +cd test/data && make + +# Windows +cd test\data && windows-build-test-data.bat +``` + +Load the shared memory datastore: + +```bash +python -m osrm datastore test/data/ch/monaco +``` + +Run the test suite: + +```bash +pytest test/python/ +``` + +## Running cibuildwheel locally + +[cibuildwheel](https://cibuildwheel.pypa.io/) builds wheels inside isolated +environments that closely match CI. Install it with: + +```bash +pip install cibuildwheel +``` + +Build for the current platform: + +```bash +cibuildwheel --platform linux # requires Docker on non-Linux hosts +cibuildwheel --platform macos +cibuildwheel --platform windows +``` + +Wheels land in `wheelhouse/`. + +**Windows note:** cibuildwheel's `config-settings` in pyproject.toml are +*replaced* (not merged) by `CIBW_CONFIG_SETTINGS_WINDOWS` if that env var is +set. Always include `ENABLE_CONAN=ON` explicitly when overriding via the env +var. + +**Linux note:** The manylinux container mounts the host ccache directory via a +Docker volume. Set `CCACHE_DIR` on the host so the mount path matches what CI +uses: + +```bash +CIBW_CONTAINER_ENGINE="docker; create_args: --volume /tmp/ccache:/ccache" \ +CIBW_ENVIRONMENT_LINUX="CCACHE_DIR=/ccache" \ +cibuildwheel --platform linux +``` + +## Type stubs + +`src/python/osrm/osrm_ext.pyi` is auto-generated by `nanobind_add_stub()` at +build time and committed to the repository so documentation tools can work +without compiling the extension. + +After changing C++ bindings, rebuild and commit the updated stub: + +```bash +pip install -e . --no-build-isolation # regenerates the .pyi +git add src/python/osrm/osrm_ext.pyi +``` + +To regenerate manually without a full rebuild: + +```bash +pip install nanobind ruff +python -m nanobind.stubgen -m osrm.osrm_ext -o src/python/osrm/osrm_ext.pyi +ruff format src/python/osrm/osrm_ext.pyi +``` + +## Releasing + +Releases are driven by git tags. `setuptools-scm` reads the tag to set the +package version — no manual version bumps needed. + +1. Ensure CI is green on `main`. +2. Create and push an annotated tag: + ```bash + git tag -a v1.2.3 -m "v1.2.3" + git push origin v1.2.3 + ``` +3. The publish workflow triggers on tag push, builds wheels for all platforms, + and uploads to PyPI via trusted publisher. +4. Verify the release at [pypi.org/project/osrm-bindings](https://pypi.org/project/osrm-bindings/). + +**Test release without tagging:** trigger the publish workflow manually via +`workflow_dispatch` with the `upload` input set to `true`. diff --git a/pyproject.toml b/pyproject.toml new file mode 100755 index 0000000000..babe77aa95 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,103 @@ +[build-system] +requires = [ + "scikit-build-core >=0.12.0", + "nanobind >=2.12.0", + "setuptools-scm >=10", + "cmake", + "ninja", + "conan==2.27.0; sys_platform == 'win32'", +] +build-backend = "scikit_build_core.build" + +[project] +name = "osrm-bindings" +dynamic = ["version"] +description = "Python bindings for the osrm-backend project" +readme = "src/python/README.md" +requires-python = ">=3.10" +authors = [{name = "Doo Woong Chung"}, { name = "Nils Nolde" }] +license = { file = "LICENSE.TXT" } + +[project.optional-dependencies] +dev = ["pytest", "pre-commit", "ruff"] + +[project.urls] +repository = "https://github.com/Project-OSRM/osrm-backend" + +[tool.ruff] +exclude = [".venv", "build"] +extend-exclude = [ + "dist", + "wheelhouse", + "*build*", + "*.egg-info", +] +line-length = 105 +lint.ignore = ["E731", "F811", "E741"] + +[tool.scikit-build.metadata] +version.provider = "scikit_build_core.metadata.setuptools_scm" + +[tool.setuptools_scm] +version_scheme = "no-guess-dev" +local_scheme = "no-local-version" + +[tool.scikit-build] +minimum-version = "0.4" +build-dir = "build/{wheel_tag}" +cmake.build-type = "Release" +wheel.py-api = "cp312" +wheel.packages = ["src/python/osrm"] +wheel.exclude = ["include/**", "lib/**", "bin/**", "share/**"] +sdist.include = ["pyproject.toml"] +sdist.exclude = ["test", "dist", "wheelhouse"] + +[tool.scikit-build.cmake.define] +ENABLE_PYTHON_BINDINGS = "ON" +CMAKE_BUILD_TYPE = "Release" +CMAKE_CXX_SCAN_FOR_MODULES = "OFF" + +[tool.cibuildwheel] +archs = ["native"] +build = "cp312-*" +build-verbosity = 1 +skip = "*musllinux*" +# TODO(nils): should we leave it here or push to osrm org? +manylinux-x86_64-image = "ghcr.io/nilsnolde/manylinux:2_28_osrm_python" +manylinux-aarch64-image = "ghcr.io/nilsnolde/manylinux:2_28_osrm_python" +test-requires = "pytest" +test-command = [ + "cd {project}/test/data && python -m osrm extract -p {project}/profiles/car.lua monaco.osm.pbf", + "mkdir -p {project}/test/data/ch && cp {project}/test/data/monaco.osrm* {project}/test/data/ch/", + "python -m osrm contract {project}/test/data/ch/monaco.osrm", + "mkdir -p {project}/test/data/mld && cp {project}/test/data/monaco.osrm* {project}/test/data/mld/", + "python -m osrm partition {project}/test/data/mld/monaco.osrm", + "python -m osrm customize {project}/test/data/mld/monaco.osrm", + "python -m osrm datastore {project}/test/data/ch/monaco", + "pytest {project}/test/python" +] + +[tool.cibuildwheel.linux] +environment = "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH}" +before-build = "ccache -s && ccache -M 500M" +repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" + +[tool.cibuildwheel.macos] +environment = "MACOSX_DEPLOYMENT_TARGET=15 CCACHE_DIR=/tmp/ccache" +before-build = "ccache -s && ccache -M 500M" +before-all = """ + export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 + brew install lua tbb boost@1.90 ccache + brew link boost@1.90 +""" + +[tool.cibuildwheel.windows] +before-build = "pip install conan==2.27.0 delvewheel && conan profile detect --force" +config-settings = "cmake.define.ENABLE_CONAN=ON" +repair-wheel-command = 'call build\cp312-abi3-win_amd64\_deps\libosrm-build\conanrun.bat && delvewheel repair --analyze-existing-exes --add-dll hwloc.dll --no-mangle tbb12.dll --no-mangle hwloc.dll -w {dest_dir} {wheel}' +test-command = [ + "cd /d {project}/test/data", + "windows-build-test-data.bat", + "python -m osrm datastore {project}/test/data/ch/monaco", + "pytest {project}/test/python" +] diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt new file mode 100644 index 0000000000..9fafaa8e48 --- /dev/null +++ b/src/python/CMakeLists.txt @@ -0,0 +1,98 @@ +message(STATUS "Building Python bindings") + +set(EXT_NAME "osrm_ext") + +find_package(Python + REQUIRED COMPONENTS Interpreter Development.Module + OPTIONAL_COMPONENTS Development.SABIModule +) + +# Find nanobind's cmake dir via the Python module +execute_process( + COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE NB_DIR +) +list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}") +find_package(nanobind CONFIG REQUIRED) + +set(SRCS + src/osrm_nb.cpp + src/engineconfig_nb.cpp + src/utility/osrm_utility.cpp + src/utility/param_utility.cpp + + src/parameters/baseparameter_nb.cpp + src/parameters/routeparameter_nb.cpp + src/parameters/matchparameter_nb.cpp + src/parameters/nearestparameter_nb.cpp + src/parameters/tableparameter_nb.cpp + src/parameters/tileparameter_nb.cpp + src/parameters/tripparameter_nb.cpp + + src/types/optional_nb.cpp + src/types/coordinate_nb.cpp + src/types/jsoncontainer_nb.cpp + src/types/approach_nb.cpp + src/types/bearing_nb.cpp +) +nanobind_add_module( + ${EXT_NAME} + STABLE_ABI + NB_STATIC + ${SRCS} +) + +# OSRM's root CMakeLists.txt enables CMAKE_INTERPROCEDURAL_OPTIMIZATION globally, +# which conflicts with nanobind's NB_MAKE_OPAQUE macros (ODR violations across TUs +# flagged as errors during LTO). Disable IPO for the extension module — nanobind's +# own LTO flag (passed to nanobind_add_module above) handles LTO for the binding. +set_target_properties(${EXT_NAME} PROPERTIES INTERPROCEDURAL_OPTIMIZATION FALSE) +# Also suppress LTO warnings-as-errors at link time, since the osrm library +# is compiled with fat LTO objects and GCC's LTO linker plugin sees ODR +# differences between nanobind's type_caster and NB_MAKE_OPAQUE macros. +target_link_options(${EXT_NAME} PRIVATE -Wno-error) + +# nanobind's internal sources trigger warnings under OSRM's strict -Werror flags. +# Suppress -Werror for the nanobind static library target. +if(TARGET nanobind-static-abi3) + target_compile_options(nanobind-static-abi3 PRIVATE -Wno-error) +elseif(TARGET nanobind-static) + target_compile_options(nanobind-static PRIVATE -Wno-error) +endif() + +# Binding headers live in src/python/include/python/ +target_include_directories(${EXT_NAME} + PRIVATE + ${PROJECT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +# Link against the osrm library built by the parent project +target_link_libraries(${EXT_NAME} + PRIVATE + osrm + Python::SABIModule +) + +if (MSVC) + target_compile_definitions(${EXT_NAME} PRIVATE BOOST_ALL_NO_LIB) +endif() + +# Install the extension module into the osrm Python package +install(TARGETS ${EXT_NAME} LIBRARY DESTINATION osrm) + +# Install OSRM executables and profiles into the wheel +set(OSRM_EXECUTABLES osrm-extract osrm-contract osrm-customize osrm-partition osrm-datastore osrm-routed osrm-components) +install(TARGETS ${OSRM_EXECUTABLES} DESTINATION osrm/bin) +install(DIRECTORY ${PROJECT_SOURCE_DIR}/profiles DESTINATION osrm/share) + +# Generate Python type stubs (.pyi) from the compiled extension module. +# Stubs are written to the source tree so they can be committed and used by +# documentation tools without compiling the extension. +nanobind_add_stub( + ${EXT_NAME}_stub + MODULE osrm_ext + OUTPUT ${PROJECT_SOURCE_DIR}/src/python/osrm/osrm_ext.pyi + PYTHON_PATH $ + DEPENDS ${EXT_NAME} +) diff --git a/src/python/README.md b/src/python/README.md new file mode 100644 index 0000000000..58a20f75c9 --- /dev/null +++ b/src/python/README.md @@ -0,0 +1,68 @@ +# osrm-bindings + +**Python bindings for [osrm-backend](https://github.com/Project-OSRM/osrm-backend) using [nanobind](https://github.com/wjakob/nanobind).** + +## PyPI + +``` +pip install osrm-bindings +``` + +> [!NOTE] +> On PyPI we only distribute `abi3` wheels for each platform, i.e. one needs at least Python 3.12 to install the wheels. Of course it'll fall back to attempt an installation from source. + +Platform | Arch +---|--- +Linux | x86_64 +Linux | aarch64 +MacOS | arm64 +Windows | x86_64 + +## From Source + +osrm-bindings requires **CPython 3.10+** and can be installed from source by running the following command in the repository root: + +``` +pip install . +``` + +The Python bindings are built alongside the OSRM C++ libraries. The version of the bindings matches the version of osrm-backend. + +## Example + +The following example will showcase the process of calculating routes between two coordinates. + +First, import the `osrm` library, and instantiate an instance of OSRM: +```python +import osrm + +# Instantiate osrm_py instance +osrm_py = osrm.OSRM("./test/data/ch/monaco.osrm") +``` + +Then, declare `RouteParameters`, and then pass it into the `osrm_py` instance: +```python +# Declare Route Parameters +route_params = osrm.RouteParameters( + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)] +) + +# Pass it into the osrm_py instance +res = osrm_py.Route(route_params) + +# Print out result output +print(res["waypoints"]) +print(res["routes"]) +``` + +## Type Stubs + +The file `src/python/osrm/osrm_ext.pyi` contains auto-generated type stubs for the C++ extension module. These are used by IDEs for autocompletion and by documentation tools without compiling the extension. + +After changing C++ bindings, rebuild the project to regenerate the stubs: + +``` +pip install -e . +``` + +Then commit the updated `.pyi` file. diff --git a/src/python/include/python/engineconfig_nb.hpp b/src/python/include/python/engineconfig_nb.hpp new file mode 100644 index 0000000000..1f57d271c2 --- /dev/null +++ b/src/python/include/python/engineconfig_nb.hpp @@ -0,0 +1,19 @@ +#ifndef OSRM_NB_ENGINECONFIG_H +#define OSRM_NB_ENGINECONFIG_H + +#include "engine/engine_config.hpp" + +#include + +#include + +using osrm::engine::EngineConfig; + +void init_EngineConfig(nanobind::module_ &m); + +static const std::unordered_map algorithm_map{ + {"CH", EngineConfig::Algorithm::CH}, + {std::string(), EngineConfig::Algorithm::CH}, + {"MLD", EngineConfig::Algorithm::MLD}}; + +#endif // OSRM_NB_ENGINECONFIG_H diff --git a/src/python/include/python/parameters/baseparameter_nb.hpp b/src/python/include/python/parameters/baseparameter_nb.hpp new file mode 100644 index 0000000000..999b59203c --- /dev/null +++ b/src/python/include/python/parameters/baseparameter_nb.hpp @@ -0,0 +1,28 @@ +#ifndef OSRM_NB_BASEPARAMETER_H +#define OSRM_NB_BASEPARAMETER_H + +#include "engine/api/base_parameters.hpp" + +#include + +#include + +using osrm::engine::api::BaseParameters; + +// Must be visible in every TU that converts these enum types to/from Python. +NB_MAKE_OPAQUE(osrm::engine::api::BaseParameters::SnappingType) +NB_MAKE_OPAQUE(osrm::engine::api::BaseParameters::OutputFormatType) + +void init_BaseParameters(nanobind::module_ &m); + +static const std::unordered_map snapping_map{ + {"default", BaseParameters::SnappingType::Default}, + {std::string(), BaseParameters::SnappingType::Default}, + {"any", BaseParameters::SnappingType::Any}}; + +static const std::unordered_map output_map{ + {"json", BaseParameters::OutputFormatType::JSON}, + {std::string(), BaseParameters::OutputFormatType::JSON}, + {"flatbuffers", BaseParameters::OutputFormatType::FLATBUFFERS}}; + +#endif // OSRM_NB_BASEPARAMETER_H diff --git a/src/python/include/python/parameters/matchparameter_nb.hpp b/src/python/include/python/parameters/matchparameter_nb.hpp new file mode 100644 index 0000000000..cf3a397d08 --- /dev/null +++ b/src/python/include/python/parameters/matchparameter_nb.hpp @@ -0,0 +1,23 @@ +#ifndef OSRM_NB_MATCHPARAMETER_H +#define OSRM_NB_MATCHPARAMETER_H + +#include "python/parameters/routeparameter_nb.hpp" +#include "engine/api/match_parameters.hpp" + +#include + +#include + +using osrm::engine::api::MatchParameters; + +// Must be visible in every TU that converts these enum types to/from Python. +NB_MAKE_OPAQUE(osrm::engine::api::MatchParameters::GapsType) + +void init_MatchParameters(nanobind::module_ &m); + +static const std::unordered_map gaps_map{ + {"split", MatchParameters::GapsType::Split}, + {std::string(), MatchParameters::GapsType::Split}, + {"ignore", MatchParameters::GapsType::Ignore}}; + +#endif // OSRM_NB_MATCHPARAMETER_H diff --git a/src/python/include/python/parameters/nearestparameter_nb.hpp b/src/python/include/python/parameters/nearestparameter_nb.hpp new file mode 100644 index 0000000000..b8b8b704da --- /dev/null +++ b/src/python/include/python/parameters/nearestparameter_nb.hpp @@ -0,0 +1,8 @@ +#ifndef OSRM_NB_NEARESTPARAMETER_H +#define OSRM_NB_NEARESTPARAMETER_H + +#include + +void init_NearestParameters(nanobind::module_ &m); + +#endif // OSRM_NB_NEARESTPARAMETER_H diff --git a/src/python/include/python/parameters/routeparameter_nb.hpp b/src/python/include/python/parameters/routeparameter_nb.hpp new file mode 100644 index 0000000000..b1254ac528 --- /dev/null +++ b/src/python/include/python/parameters/routeparameter_nb.hpp @@ -0,0 +1,43 @@ +#ifndef OSRM_NB_ROUTEPARAMETER_H +#define OSRM_NB_ROUTEPARAMETER_H + +#include "python/parameters/baseparameter_nb.hpp" +#include "engine/api/route_parameters.hpp" + +#include + +#include + +using osrm::engine::api::RouteParameters; + +// Must be visible in every TU that converts these enum types to/from Python. +NB_MAKE_OPAQUE(osrm::engine::api::RouteParameters::GeometriesType) +NB_MAKE_OPAQUE(osrm::engine::api::RouteParameters::OverviewType) +NB_MAKE_OPAQUE(osrm::engine::api::RouteParameters::AnnotationsType) + +void init_RouteParameters(nanobind::module_ &m); + +static const std::unordered_map geometries_map{ + {"polyline", RouteParameters::GeometriesType::Polyline}, + {std::string(), RouteParameters::GeometriesType::Polyline}, + {"polyline6", RouteParameters::GeometriesType::Polyline6}, + {"geojson", RouteParameters::GeometriesType::GeoJSON}}; + +static const std::unordered_map overview_map{ + {"simplified", RouteParameters::OverviewType::Simplified}, + {std::string(), RouteParameters::OverviewType::Simplified}, + {"full", RouteParameters::OverviewType::Full}, + {"false", RouteParameters::OverviewType::False}}; + +static const std::unordered_map + route_annotations_map{{"none", RouteParameters::AnnotationsType::None}, + {std::string(), RouteParameters::AnnotationsType::None}, + {"duration", RouteParameters::AnnotationsType::Duration}, + {"nodes", RouteParameters::AnnotationsType::Nodes}, + {"distance", RouteParameters::AnnotationsType::Distance}, + {"weight", RouteParameters::AnnotationsType::Weight}, + {"datasources", RouteParameters::AnnotationsType::Datasources}, + {"speed", RouteParameters::AnnotationsType::Speed}, + {"all", RouteParameters::AnnotationsType::All}}; + +#endif // OSRM_NB_ROUTEPARAMETER_H diff --git a/src/python/include/python/parameters/tableparameter_nb.hpp b/src/python/include/python/parameters/tableparameter_nb.hpp new file mode 100644 index 0000000000..9f78534cf5 --- /dev/null +++ b/src/python/include/python/parameters/tableparameter_nb.hpp @@ -0,0 +1,31 @@ +#ifndef OSRM_NB_TABLEPARAMETER_H +#define OSRM_NB_TABLEPARAMETER_H + +#include "python/parameters/baseparameter_nb.hpp" +#include "engine/api/table_parameters.hpp" + +#include + +#include + +using osrm::engine::api::TableParameters; + +// Must be visible in every TU that converts these enum types to/from Python. +NB_MAKE_OPAQUE(osrm::engine::api::TableParameters::FallbackCoordinateType) +NB_MAKE_OPAQUE(osrm::engine::api::TableParameters::AnnotationsType) + +void init_TableParameters(nanobind::module_ &m); + +static const std::unordered_map fallback_map{ + {"input", TableParameters::FallbackCoordinateType::Input}, + {std::string(), TableParameters::FallbackCoordinateType::Input}, + {"snapped", TableParameters::FallbackCoordinateType::Snapped}}; + +static const std::unordered_map + table_annotations_map{{"none", TableParameters::AnnotationsType::None}, + {std::string(), TableParameters::AnnotationsType::None}, + {"duration", TableParameters::AnnotationsType::Duration}, + {"distance", TableParameters::AnnotationsType::Distance}, + {"all", TableParameters::AnnotationsType::All}}; + +#endif // OSRM_NB_TABLEPARAMETER_H diff --git a/src/python/include/python/parameters/tileparameter_nb.hpp b/src/python/include/python/parameters/tileparameter_nb.hpp new file mode 100644 index 0000000000..b02742e111 --- /dev/null +++ b/src/python/include/python/parameters/tileparameter_nb.hpp @@ -0,0 +1,8 @@ +#ifndef OSRM_NB_TILEPARAMETER_H +#define OSRM_NB_TILEPARAMETER_H + +#include + +void init_TileParameters(nanobind::module_ &m); + +#endif // OSRM_NB_TILEPARAMETER_H diff --git a/src/python/include/python/parameters/tripparameter_nb.hpp b/src/python/include/python/parameters/tripparameter_nb.hpp new file mode 100644 index 0000000000..ad6bccb596 --- /dev/null +++ b/src/python/include/python/parameters/tripparameter_nb.hpp @@ -0,0 +1,29 @@ +#ifndef OSRM_NB_TRIPPARAMETER_H +#define OSRM_NB_TRIPPARAMETER_H + +#include "python/parameters/routeparameter_nb.hpp" +#include "engine/api/trip_parameters.hpp" + +#include + +#include + +using osrm::engine::api::TripParameters; + +// Must be visible in every TU that converts these enum types to/from Python. +NB_MAKE_OPAQUE(osrm::engine::api::TripParameters::SourceType) +NB_MAKE_OPAQUE(osrm::engine::api::TripParameters::DestinationType) + +void init_TripParameters(nanobind::module_ &m); + +static const std::unordered_map source_map{ + {"any", TripParameters::SourceType::Any}, + {std::string(), TripParameters::SourceType::Any}, + {"first", TripParameters::SourceType::First}}; + +static const std::unordered_map dest_map{ + {"any", TripParameters::DestinationType::Any}, + {std::string(), TripParameters::DestinationType::Any}, + {"last", TripParameters::DestinationType::Last}}; + +#endif // OSRM_NB_TRIPPARAMETER_H diff --git a/src/python/include/python/types/approach_nb.hpp b/src/python/include/python/types/approach_nb.hpp new file mode 100644 index 0000000000..8299e5d854 --- /dev/null +++ b/src/python/include/python/types/approach_nb.hpp @@ -0,0 +1,20 @@ +#ifndef OSRM_NB_APPROACH_H +#define OSRM_NB_APPROACH_H + +#include "engine/approach.hpp" + +#include + +#include +#include + +using osrm::engine::Approach; + +void init_Approach(nanobind::module_ &m); + +static const std::unordered_map approach_map{ + {"curb", Approach::CURB}, + {std::string(), Approach::CURB}, + {"unrestricted", Approach::UNRESTRICTED}}; + +#endif // OSRM_NB_APPROACH_H diff --git a/src/python/include/python/types/bearing_nb.hpp b/src/python/include/python/types/bearing_nb.hpp new file mode 100644 index 0000000000..f4e5396331 --- /dev/null +++ b/src/python/include/python/types/bearing_nb.hpp @@ -0,0 +1,8 @@ +#ifndef OSRM_NB_BEARING_H +#define OSRM_NB_BEARING_H + +#include + +void init_Bearing(nanobind::module_ &m); + +#endif // OSRM_NB_BEARING_H diff --git a/src/python/include/python/types/coordinate_nb.hpp b/src/python/include/python/types/coordinate_nb.hpp new file mode 100644 index 0000000000..ebfa3deac3 --- /dev/null +++ b/src/python/include/python/types/coordinate_nb.hpp @@ -0,0 +1,8 @@ +#ifndef OSRM_NB_COORDINATE_H +#define OSRM_NB_COORDINATE_H + +#include + +void init_Coordinate(nanobind::module_ &m); + +#endif // OSRM_NB_COORDINATE_H diff --git a/src/python/include/python/types/jsoncontainer_nb.hpp b/src/python/include/python/types/jsoncontainer_nb.hpp new file mode 100644 index 0000000000..8e67768f85 --- /dev/null +++ b/src/python/include/python/types/jsoncontainer_nb.hpp @@ -0,0 +1,120 @@ +#ifndef OSRM_NB_JSONCONTAINER_H +#define OSRM_NB_JSONCONTAINER_H + +#include "util/json_container.hpp" + +#include + +#include +#include + +void init_JSONContainer(nanobind::module_ &m); + +namespace json = osrm::util::json; +using JSONValue = json::Value; + +// Custom Type Casters +namespace nanobind::detail +{ + +template <> struct type_caster +{ + static constexpr auto Name = const_name("JSONValue"); + + template using Caster = make_caster>; + + template + static handle from_cpp(T &&val, rv_policy policy, cleanup_list *cleanup) noexcept + { + return std::visit( + [&](auto &&v) { + return Caster::from_cpp(std::forward(v), policy, cleanup); + }, + std::forward(val)); + } +}; + +template <> struct type_caster : type_caster_base +{ + static handle from_cpp(const json::String &val, rv_policy, cleanup_list *) noexcept + { + return PyUnicode_FromStringAndSize(val.value.c_str(), val.value.size()); + } +}; +template <> struct type_caster : type_caster_base +{ + static handle from_cpp(const json::Number &val, rv_policy, cleanup_list *) noexcept + { + return PyFloat_FromDouble((double)val.value); + } +}; + +template <> struct type_caster : type_caster_base +{ + static handle from_cpp(const json::True &, rv_policy, cleanup_list *) noexcept + { + return handle(Py_True).inc_ref(); + } +}; +template <> struct type_caster : type_caster_base +{ + static handle from_cpp(const json::False &, rv_policy, cleanup_list *) noexcept + { + return handle(Py_False).inc_ref(); + } +}; +template <> struct type_caster : type_caster_base +{ + static handle from_cpp(const json::Null &, rv_policy, cleanup_list *) noexcept + { + return none().release(); + } +}; + +} // namespace nanobind::detail + +struct ValueStringifyVisitor +{ + std::string operator()(const json::String &str) { return "'" + str.value + "'"; } + std::string operator()(const json::Number &num) { return std::to_string(num.value); } + std::string operator()(const json::True &) { return "True"; } + std::string operator()(const json::False &) { return "False"; } + std::string operator()(const json::Null &) { return "None"; } + + std::string visitarray(const json::Array &arr) + { + std::string output = "["; + for (size_t i = 0; i < arr.values.size(); ++i) + { + if (i != 0) + { + output += ", "; + } + output += std::visit(*this, arr.values[i]); + } + return output + "]"; + } + std::string operator()(const json::Array &arr) { return visitarray(arr); } + + std::string visitobject(const json::Object &obj) + { + std::string output = "{"; + bool first = true; + for (const auto &itr : obj.values) + { + if (!first) + { + output += ", "; + } + output += '\''; + output += itr.first; + output += "': "; + output += std::visit(*this, itr.second); + first = false; + } + return output + "}"; + } + std::string operator()(const json::Object &obj) { return visitobject(obj); } +}; + +#endif // OSRM_NB_JSONCONTAINER_H diff --git a/src/python/include/python/types/optional_nb.hpp b/src/python/include/python/types/optional_nb.hpp new file mode 100644 index 0000000000..eed5b535cc --- /dev/null +++ b/src/python/include/python/types/optional_nb.hpp @@ -0,0 +1,8 @@ +#ifndef OSRM_NB_OPTIONAL_H +#define OSRM_NB_OPTIONAL_H + +#include + +void init_Optional(nanobind::module_ &m); + +#endif // OSRM_NB_OPTIONAL_H diff --git a/src/python/include/python/utility/osrm_utility.hpp b/src/python/include/python/utility/osrm_utility.hpp new file mode 100644 index 0000000000..1b59d9cfe0 --- /dev/null +++ b/src/python/include/python/utility/osrm_utility.hpp @@ -0,0 +1,19 @@ +#ifndef OSRM_NB_OSRM_UTIL_H +#define OSRM_NB_OSRM_UTIL_H + +#include "engine/engine_config.hpp" +#include "engine/status.hpp" +#include "util/json_container.hpp" + +#include + +namespace osrm_nb_util +{ + +void check_status(osrm::engine::Status status, osrm::util::json::Object &res); + +void populate_cfg_from_kwargs(const nanobind::kwargs &kwargs, osrm::engine::EngineConfig &config); + +} // namespace osrm_nb_util + +#endif // OSRM_NB_OSRM_UTIL_H diff --git a/src/python/include/python/utility/param_utility.hpp b/src/python/include/python/utility/param_utility.hpp new file mode 100644 index 0000000000..226968e496 --- /dev/null +++ b/src/python/include/python/utility/param_utility.hpp @@ -0,0 +1,101 @@ +#ifndef OSRM_NB_PARAM_UTIL_H +#define OSRM_NB_PARAM_UTIL_H + +#include "engine/api/base_parameters.hpp" +#include "engine/api/match_parameters.hpp" +#include "engine/api/route_parameters.hpp" +#include "engine/api/table_parameters.hpp" +#include "engine/api/trip_parameters.hpp" +#include "engine/approach.hpp" + +#include +#include +#include + +using osrm::engine::Approach; +using osrm::engine::api::BaseParameters; +using osrm::engine::api::MatchParameters; +using osrm::engine::api::RouteParameters; +using osrm::engine::api::TableParameters; +using osrm::engine::api::TripParameters; + +namespace osrm_nb_util +{ + +template +T str_to_enum(const std::string &str, + const std::string &type_name, + const std::unordered_map &enum_map) +{ + auto itr = enum_map.find(str); + + if (itr != enum_map.end()) + { + return itr->second; + } + + std::string valid_strs = "(Valid Options: "; + bool first = true; + + for (auto itr : enum_map) + { + if (itr.first.empty()) + { + continue; + } + if (!first) + { + valid_strs += ", "; + } + valid_strs += "'" + itr.first + "'"; + first = false; + } + valid_strs += ")"; + + throw std::invalid_argument("Invalid " + type_name + ": '" + str + "' " + valid_strs); +} + +template +std::string enum_to_str(T enum_type, + const std::string &type_name, + const std::unordered_map &enum_map) +{ + for (auto itr : enum_map) + { + if (itr.second == enum_type) + { + return itr.first; + } + } + + throw std::invalid_argument("Undefined " + type_name + " Enum"); +} + +void assign_baseparameters(BaseParameters *params, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping); + +void assign_routeparameters(RouteParameters *params, + const bool steps, + int number_of_alternatives, + const std::vector &annotations, + RouteParameters::GeometriesType geometries, + RouteParameters::OverviewType overview, + const std::optional continue_straight, + std::vector waypoints); + +RouteParameters::AnnotationsType +calculate_routeannotations_type(const std::vector &annotations); + +TableParameters::AnnotationsType +calculate_tableannotations_type(const std::vector &annotations); + +} // namespace osrm_nb_util + +#endif // OSRM_NB_PARAM_UTIL_H diff --git a/src/python/osrm/__init__.py b/src/python/osrm/__init__.py new file mode 100755 index 0000000000..28f43ef022 --- /dev/null +++ b/src/python/osrm/__init__.py @@ -0,0 +1,15 @@ +# ruff: noqa: F401 +from .osrm_ext import ( + OSRM, + EngineConfig, + Bearing, + Coordinate, + RouteParameters, + NearestParameters, + TableParameters, + TileParameters, + TripParameters, + MatchParameters, + Array, + Object, +) diff --git a/src/python/osrm/__main__.py b/src/python/osrm/__main__.py new file mode 100644 index 0000000000..493ca47e08 --- /dev/null +++ b/src/python/osrm/__main__.py @@ -0,0 +1,72 @@ +import os +import platform +import shutil +import subprocess +import sys +from pathlib import Path + +IS_WIN = platform.system().lower() == "windows" + +# Executables are installed under osrm/bin/ inside the package directory. +# For editable/dev installs, fall back to the CMake build directory or PATH. +_BIN_DIR = Path(__file__).parent / "bin" + +# delvewheel bundles shared DLLs (tbb12, hwloc) into osrm_bindings.libs/. +# Subprocess-launched executables can't benefit from the .pyd DLL path +# patching, so we pass PATH explicitly on Windows. +_LIBS_DIR = Path(__file__).parent.parent / "osrm_bindings.libs" + +COMMANDS = { + "components": "osrm-components", + "contract": "osrm-contract", + "customize": "osrm-customize", + "datastore": "osrm-datastore", + "extract": "osrm-extract", + "partition": "osrm-partition", + "routed": "osrm-routed", +} + +if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS: + print("Usage: python -m osrm [args...]") + print(f"Commands: {', '.join(COMMANDS)}") + sys.exit(1) + +exe_name = COMMANDS[sys.argv[1]] +if IS_WIN: + exe_name += ".exe" + + +def _find_executable(exe_name): + # 1. Installed wheel layout: osrm/bin/ + candidate = _BIN_DIR / exe_name + if candidate.is_file(): + return candidate + + # 2. Editable install: look in CMake build directories + project_root = Path(__file__).parent.parent.parent.parent + for build_dir in sorted(project_root.glob("build/*/"), reverse=True): + candidate = build_dir / exe_name + if candidate.is_file(): + return candidate + + # 3. System PATH + found = shutil.which(exe_name) + if found: + return Path(found) + + return None + + +executable = _find_executable(exe_name) +if not executable: + print(f"OSRM executable not found: {exe_name}") + sys.exit(1) + +cmd = [str(executable)] + sys.argv[2:] + +env = None +if IS_WIN and _LIBS_DIR.is_dir(): + env = {**os.environ, "PATH": str(_LIBS_DIR) + os.pathsep + os.environ.get("PATH", "")} + +proc = subprocess.run(cmd, env=env) +sys.exit(proc.returncode) diff --git a/src/python/osrm/osrm_ext.pyi b/src/python/osrm/osrm_ext.pyi new file mode 100644 index 0000000000..7809db9d38 --- /dev/null +++ b/src/python/osrm/osrm_ext.pyi @@ -0,0 +1,855 @@ +from collections.abc import Iterator, Sequence +from typing import overload + +class EngineConfig: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, **kwargs) -> None: ... + def IsValid(self) -> bool: ... + def SetStorageConfig(self, arg: str, /) -> None: ... + @property + def max_locations_trip(self) -> int: ... + @max_locations_trip.setter + def max_locations_trip(self, arg: int, /) -> None: ... + @property + def max_locations_viaroute(self) -> int: ... + @max_locations_viaroute.setter + def max_locations_viaroute(self, arg: int, /) -> None: ... + @property + def max_locations_distance_table(self) -> int: ... + @max_locations_distance_table.setter + def max_locations_distance_table(self, arg: int, /) -> None: ... + @property + def max_locations_map_matching(self) -> int: ... + @max_locations_map_matching.setter + def max_locations_map_matching(self, arg: int, /) -> None: ... + @property + def max_radius_map_matching(self) -> float: ... + @max_radius_map_matching.setter + def max_radius_map_matching(self, arg: float, /) -> None: ... + @property + def max_results_nearest(self) -> int: ... + @max_results_nearest.setter + def max_results_nearest(self, arg: int, /) -> None: ... + @property + def default_radius(self) -> float: ... + @default_radius.setter + def default_radius(self, arg: float, /) -> None: ... + @property + def max_alternatives(self) -> int: ... + @max_alternatives.setter + def max_alternatives(self, arg: int, /) -> None: ... + @property + def use_shared_memory(self) -> bool: ... + @use_shared_memory.setter + def use_shared_memory(self, arg: bool, /) -> None: ... + @property + def memory_file(self) -> str: ... + @memory_file.setter + def memory_file(self, arg: str, /) -> None: ... + @property + def use_mmap(self) -> bool: ... + @use_mmap.setter + def use_mmap(self, arg: bool, /) -> None: ... + @property + def algorithm(self) -> Algorithm: ... + @algorithm.setter + def algorithm(self, arg: Algorithm, /) -> None: ... + @property + def verbosity(self) -> str: ... + @verbosity.setter + def verbosity(self, arg: str, /) -> None: ... + @property + def dataset_name(self) -> str: ... + @dataset_name.setter + def dataset_name(self, arg: str, /) -> None: ... + +class Algorithm: + def __init__(self, arg: str, /) -> None: ... + def __repr__(self) -> str: ... + +class Approach: + def __init__(self, arg: str, /) -> None: ... + def __repr__(self) -> str: ... + +class Bearing: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, arg: tuple[int, int], /) -> None: ... + @property + def bearing(self) -> int: ... + @bearing.setter + def bearing(self, arg: int, /) -> None: ... + @property + def range(self) -> int: ... + @range.setter + def range(self, arg: int, /) -> None: ... + def IsValid(self) -> bool: ... + def __eq__(self, arg: Bearing, /) -> bool: ... + def __ne__(self, arg: Bearing, /) -> bool: ... + +class Coordinate: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, coordinate: Coordinate) -> None: ... + @overload + def __init__(self, arg: tuple[float, float], /) -> None: ... + @property + def lon(self) -> float: ... + @lon.setter + def lon(self, arg: float, /) -> None: ... + @property + def lat(self) -> float: ... + @lat.setter + def lat(self, arg: float, /) -> None: ... + def IsValid(self) -> bool: ... + def __repr__(self) -> str: ... + def __eq__(self, arg: Coordinate, /) -> bool: ... + def __ne__(self, arg: Coordinate, /) -> bool: ... + +class Object: + def __init__(self) -> None: ... + def __len__(self) -> int: ... + def __bool__(self) -> bool: ... + def __repr__(self) -> str: ... + def __getitem__(self, arg: str, /) -> object: ... + def __contains__(self, arg: str, /) -> bool: ... + def __iter__(self) -> Iterator[str]: ... + +class Array: + def __init__(self) -> None: ... + def __len__(self) -> int: ... + def __bool__(self) -> bool: ... + def __repr__(self) -> str: ... + def __getitem__(self, arg: int, /) -> object: ... + def __iter__(self) -> Iterator: ... + +class String: + def __init__(self, arg: str, /) -> None: ... + +class Number: + def __init__(self, arg: float, /) -> None: ... + +class BaseParameters: + def __init__(self) -> None: + r""" + Instantiates an instance of BaseParameters. + + Note: + This is the parent class to many parameter classes, and not intended to be used on its own. + + Args: + coordinates (list of floats pairs): Pairs of Longitude and Latitude Coordinates. (default []) + hints (list): Hint from previous request to derive position in street network. (default []) + radiuses (list of floats): Limits the search to given radius in meters. (default []) + bearings (list of int pairs): Limits the search to segments with given bearing in degrees towards true north in clockwise direction. (default []) + approaches (list): Keep waypoints on curb side. (default []) + generate_hints (bool): Adds a hint to the response which can be used in subsequent requests. (default True) + exclude (list of strings): Additive list of classes to avoid. (default []) + snapping (string 'default' | 'any'): 'default' snapping avoids is_startpoint edges, 'any' will snap to any edge in the graph. (default \'\') + + Returns: + __init__ (osrm.osrm_ext.BaseParameters): A BaseParameter object, that is the parent object to many other Parameter objects. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + coordinates (list of floats pairs): Pairs of longitude & latitude coordinates. + hints (list): Hint from previous request to derive position in street network. + radiuses (list of floats): Limits the search to given radius in meters. + bearings (list of int pairs): Limits the search to segments with given bearing in degrees towards true north in clockwise direction. + approaches (list): Keep waypoints on curb side. + exclude (list of strings): Additive list of classes to avoid, order does not matter. + format (string): Specifies response type - currently only 'json' is supported. + generate_hints (bool): Adds a hint to the response which can be used in subsequent requests. + skip_waypoints (list): Removes waypoints from the response. + snapping (string): 'default' snapping avoids is_startpoint edges, 'any' will snap to any edge in the graph. + """ + + @property + def coordinates(self) -> list[Coordinate]: ... + @coordinates.setter + def coordinates(self, arg: Sequence[Coordinate], /) -> None: ... + @property + def hints(self) -> list: ... + @hints.setter + def hints(self, arg: list, /) -> None: ... + @property + def radiuses(self) -> list[float | None]: ... + @radiuses.setter + def radiuses(self, arg: Sequence[float | None], /) -> None: ... + @property + def bearings(self) -> list[Bearing | None]: ... + @bearings.setter + def bearings(self, arg: Sequence[Bearing | None], /) -> None: ... + @property + def approaches(self) -> list[Approach | None]: ... + @approaches.setter + def approaches(self, arg: Sequence[Approach | None], /) -> None: ... + @property + def exclude(self) -> list[str]: ... + @exclude.setter + def exclude(self, arg: Sequence[str], /) -> None: ... + @property + def format(self) -> OutputFormatType | None: ... + @format.setter + def format(self, arg: OutputFormatType | None) -> None: ... + @property + def generate_hints(self) -> bool: ... + @generate_hints.setter + def generate_hints(self, arg: bool, /) -> None: ... + @property + def skip_waypoints(self) -> bool: ... + @skip_waypoints.setter + def skip_waypoints(self, arg: bool, /) -> None: ... + @property + def snapping(self) -> SnappingType: ... + @snapping.setter + def snapping(self, arg: SnappingType, /) -> None: ... + def IsValid(self) -> bool: ... + +class SnappingType: + def __init__(self, arg: str, /) -> None: + """Instantiates a SnappingType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on SnappingType value.""" + +class OutputFormatType: + def __init__(self, arg: str, /) -> None: + """Instantiates a OutputFormatType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on OutputFormatType value.""" + +class NearestParameters(BaseParameters): + @overload + def __init__(self) -> None: + """ + Instantiates an instance of NearestParameters. + + Examples: + >>> nearest_params = osrm.NearestParameters( + coordinates = [(7.41337, 43.72956)], + exclude = ['motorway'] + ) + >>> nearest_params.IsValid() + True + + Args: + BaseParameters (osrm.osrm_ext.BaseParameters): Keyword arguments from parent class. + + Returns: + __init__ (osrm.NearestParameters): A NearestParameters object, for usage in osrm.OSRM.Nearest. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + number_of_results (unsigned int): Number of nearest segments that should be returned. + BaseParameters (osrm.osrm_ext.BaseParameters): Attributes from parent class. + """ + + @overload + def __init__( + self, + coordinates: Sequence[Coordinate] = [], + hints: Sequence[str | None] = [], + radiuses: Sequence[float | None] = [], + bearings: Sequence[Bearing | None] = [], + approaches: Sequence[Approach | None] = [], + generate_hints: bool = True, + exclude: Sequence[str] = [], + snapping: SnappingType = "", + ) -> None: ... + @property + def number_of_results(self) -> int: ... + @number_of_results.setter + def number_of_results(self, arg: int, /) -> None: ... + def IsValid(self) -> bool: ... + +class TableParameters(BaseParameters): + @overload + def __init__(self) -> None: + r""" + Instantiates an instance of TableParameters. + + Examples: + >>> table_params = osrm.TableParameters( + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)], + sources = [0], + destinations = [1], + annotations = ['duration'], + fallback_speed = 1, + fallback_coordinate_type = 'input', + scale_factor = 0.9 + ) + >>> table_params.IsValid() + True + + Args: + sources (list of int): Use location with given index as source. (default []) + destinations (list of int): Use location with given index as destination. (default []) + annotations (list of 'none' | 'duration' | 'distance' | 'all'): Returns additional metadata for each coordinate along the route geometry. (default []) + fallback_speed (float): If no route found between a source/destination pair, calculate the as-the-crow-flies distance, then use this speed to estimate duration. (default INVALID_FALLBACK_SPEED) + fallback_coordinate_type (string 'input' | 'snapped'): When using a fallback_speed, use the user-supplied coordinate (input), or the snapped location (snapped) for calculating distances. (default \'\') + scale_factor: Scales the table duration values by this number (use in conjunction with annotations=durations). (default 1.0) + BaseParameters (osrm.osrm_ext.BaseParameters): Keyword arguments from parent class. + + Returns: + __init__ (osrm.TableParameters): A TableParameters object, for usage in Table. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + sources (list of int): Use location with given index as source. + destinations (list of int): Use location with given index as destination. + annotations (string): Returns additional metadata for each coordinate along the route geometry. + fallback_speed (float): If no route found between a source/destination pair, calculate the as-the-crow-flies distance, then use this speed to estimate duration. + fallback_coordinate_type (string): When using a fallback_speed, use the user-supplied coordinate (input), or the snapped location (snapped) for calculating distances. + scale_factor: Scales the table duration values by this number (use in conjunction with annotations=durations). + BaseParameters (osrm.osrm_ext.BaseParameters): Attributes from parent class. + """ + + @overload + def __init__( + self, + sources: Sequence[int] = [], + destinations: Sequence[int] = [], + annotations: Sequence[TableAnnotationsType] = [], + fallback_speed: float = 3.4028234663852886e38, + fallback_coordinate_type: TableFallbackCoordinateType = "", + scale_factor: float = 1.0, + coordinates: Sequence[Coordinate] = [], + hints: Sequence[str | None] = [], + radiuses: Sequence[float | None] = [], + bearings: Sequence[Bearing | None] = [], + approaches: Sequence[Approach | None] = [], + generate_hints: bool = True, + exclude: Sequence[str] = [], + snapping: SnappingType = "", + ) -> None: ... + @property + def sources(self) -> list[int]: ... + @sources.setter + def sources(self, arg: Sequence[int], /) -> None: ... + @property + def destinations(self) -> list[int]: ... + @destinations.setter + def destinations(self, arg: Sequence[int], /) -> None: ... + @property + def fallback_speed(self) -> float: ... + @fallback_speed.setter + def fallback_speed(self, arg: float, /) -> None: ... + @property + def fallback_coordinate_type(self) -> TableFallbackCoordinateType: ... + @fallback_coordinate_type.setter + def fallback_coordinate_type(self, arg: TableFallbackCoordinateType, /) -> None: ... + @property + def annotations(self) -> TableAnnotationsType: ... + @annotations.setter + def annotations(self, arg: TableAnnotationsType, /) -> None: ... + @property + def scale_factor(self) -> float: ... + @scale_factor.setter + def scale_factor(self, arg: float, /) -> None: ... + def IsValid(self) -> bool: ... + +class TableFallbackCoordinateType: + def __init__(self, arg: str, /) -> None: + """Instantiates a FallbackCoordinateType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on FallbackCoordinateType value.""" + +class TableAnnotationsType: + def __init__(self, arg: str, /) -> None: + """Instantiates a AnnotationsType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on AnnotationsType value.""" + + def __and__(self, arg: TableAnnotationsType, /) -> bool: + """Return the bitwise AND result of two AnnotationsTypes.""" + + def __or__(self, arg: TableAnnotationsType, /) -> TableAnnotationsType: + """Return the bitwise OR result of two AnnotationsTypes.""" + + def __ior__(self, arg: TableAnnotationsType, /) -> TableAnnotationsType: + """Add the bitwise OR value of another AnnotationsType.""" + +class RouteParameters(BaseParameters): + @overload + def __init__(self) -> None: + r""" + Instantiates an instance of RouteParameters. + + Examples: + >>> route_params = osrm.RouteParameters( + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)], + steps = True, + number_of_alternatives = 3, + annotations = ['speed'], + geometries = 'polyline', + overview = 'simplified', + continue_straight = False, + waypoints = [0, 1], + radiuses = [4.07, 4.07], + bearings = [(200, 180), (250, 180)], + # approaches = ['unrestricted', 'unrestricted'], + generate_hints = False, + exclude = ['motorway'], + snapping = 'any' + ) + >>> route_params.IsValid() + True + + Args: + steps (bool): Return route steps for each route leg. (default False) + number_of_alternatives (int): Search for n alternative routes. (default 0) + annotations (list of 'none' | 'duration' | 'nodes' | 'distance' | 'weight' | 'datasources' | 'speed' | 'all'): Returns additional metadata for each coordinate along the route geometry. (default []) + geometries (string 'polyline' | 'polyline6' | 'geojson'): Returned route geometry format - influences overview and per step. (default ) + overview (string 'simplified' | 'full' | 'false'): Add overview geometry either full, simplified. (default \'\') + continue_straight (bool): Forces the route to keep going straight at waypoints, constraining u-turns. (default {}) + waypoints (list of int): Treats input coordinates indicated by given indices as waypoints in returned Match object. (default []) + BaseParameters (osrm.osrm_ext.BaseParameters): Keyword arguments from parent class. + + Returns: + __init__ (osrm.RouteParameters): A RouteParameters object, for usage in Route. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + steps (bool): Return route steps for each route leg. + alternatives (bool): Search for alternative routes. + number_of_alternatives (int): Search for n alternative routes. + annotations_type (string): Returns additional metadata for each coordinate along the route geometry. + geometries (string): Returned route geometry format - influences overview and per step. + overview (string): Add overview geometry either full, simplified. + continue_straight (bool): Forces the route to keep going straight at waypoints, constraining u-turns. + BaseParameters (osrm.osrm_ext.BaseParameters): Attributes from parent class. + """ + + @overload + def __init__( + self, + steps: bool = False, + number_of_alternatives: int = 0, + annotations: Sequence[RouteAnnotationsType] = [], + geometries: RouteGeometriesType = "", + overview: RouteOverviewType = "", + continue_straight: bool | None = None, + waypoints: Sequence[int] = [], + coordinates: Sequence[Coordinate] = [], + hints: Sequence[str | None] = [], + radiuses: Sequence[float | None] = [], + bearings: Sequence[Bearing | None] = [], + approaches: Sequence[Approach | None] = [], + generate_hints: bool = True, + exclude: Sequence[str] = [], + snapping: SnappingType = "", + ) -> None: ... + @property + def steps(self) -> bool: ... + @steps.setter + def steps(self, arg: bool, /) -> None: ... + @property + def alternatives(self) -> bool: ... + @alternatives.setter + def alternatives(self, arg: bool, /) -> None: ... + @property + def number_of_alternatives(self) -> int: ... + @number_of_alternatives.setter + def number_of_alternatives(self, arg: int, /) -> None: ... + @property + def annotations_type(self) -> RouteAnnotationsType: ... + @annotations_type.setter + def annotations_type(self, arg: RouteAnnotationsType, /) -> None: ... + @property + def geometries(self) -> RouteGeometriesType: ... + @geometries.setter + def geometries(self, arg: RouteGeometriesType, /) -> None: ... + @property + def overview(self) -> RouteOverviewType: ... + @overview.setter + def overview(self, arg: RouteOverviewType, /) -> None: ... + @property + def continue_straight(self) -> bool | None: ... + @continue_straight.setter + def continue_straight(self, arg: bool | None) -> None: ... + def IsValid(self) -> bool: ... + +class RouteGeometriesType: + def __init__(self, arg: str, /) -> None: + """Instantiates a GeometriesType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on GeometriesType value.""" + +class RouteOverviewType: + def __init__(self, arg: str, /) -> None: + """Instantiates a OverviewType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on OverviewType value.""" + +class RouteAnnotationsType: + def __init__(self, arg: str, /) -> None: + """Instantiates a AnnotationsType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on AnnotationsType value.""" + + def __and__(self, arg: RouteAnnotationsType, /) -> bool: + """Return the bitwise AND result of two AnnotationsTypes.""" + + def __or__(self, arg: RouteAnnotationsType, /) -> RouteAnnotationsType: + """Return the bitwise OR result of two AnnotationsTypes.""" + + def __ior__(self, arg: RouteAnnotationsType, /) -> RouteAnnotationsType: + """Add the bitwise OR value of another AnnotationsType.""" + +class MatchParameters(RouteParameters): + @overload + def __init__(self) -> None: + """ + Instantiates an instance of MatchParameters. + + Examples: + >>> match_params = osrm.MatchParameters( + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077), (7.41862, 43.73216)], + timestamps = [1424684612, 1424684616, 1424684620], + gaps = 'split', + tidy = True + ) + >>> match_params.IsValid() + True + + Args: + timestamps (list of unsigned int): Timestamps for the input locations in seconds since UNIX epoch. (default []) + gaps (list of 'split' | 'ignore'): Allows the input track splitting based on huge timestamp gaps between points. (default []) + tidy (bool): Allows the input track modification to obtain better matching quality for noisy tracks. (default False) + RouteParameters (osrm.RouteParameters): Keyword arguments from parent class. + + Returns: + __init__ (osrm.MatchParameters): A MatchParameters object, for usage in Match. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + timestamps (list of unsigned int): Timestamps for the input locations in seconds since UNIX epoch. + gaps (string): Allows the input track splitting based on huge timestamp gaps between points. + tidy (bool): Allows the input track modification to obtain better matching quality for noisy tracks. + RouteParameters (osrm.RouteParameters): Attributes from parent class. + """ + + @overload + def __init__( + self, + timestamps: Sequence[int] = [], + gaps: MatchGapsType = "", + tidy: bool = False, + steps: bool = False, + number_of_alternatives: int = 0, + annotations: Sequence[RouteAnnotationsType] = [], + geometries: RouteGeometriesType = "", + overview: RouteOverviewType = "", + continue_straight: bool | None = None, + waypoints: Sequence[int] = [], + coordinates: Sequence[Coordinate] = [], + hints: Sequence[str | None] = [], + radiuses: Sequence[float | None] = [], + bearings: Sequence[Bearing | None] = [], + approaches: Sequence[Approach | None] = [], + generate_hints: bool = True, + exclude: Sequence[str] = [], + snapping: SnappingType = "", + ) -> None: ... + @property + def timestamps(self) -> list[int]: ... + @timestamps.setter + def timestamps(self, arg: Sequence[int], /) -> None: ... + @property + def gaps(self) -> MatchGapsType: ... + @gaps.setter + def gaps(self, arg: MatchGapsType, /) -> None: ... + @property + def tidy(self) -> bool: ... + @tidy.setter + def tidy(self, arg: bool, /) -> None: ... + def IsValid(self) -> bool: ... + +class MatchGapsType: + def __init__(self, arg: str, /) -> None: + """Instantiates a GapsType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on GapsType value.""" + +class TripParameters(RouteParameters): + @overload + def __init__(self) -> None: + r""" + Instantiates an instance of TripParameters. + + Examples: + >>> trip_params = osrm.TripParameters( + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)], + source = 'any', + destination = 'last', + roundtrip = False + ) + >>> trip_params.IsValid() + True + + Args: + source (string 'any' | 'first'): Returned route starts at 'any' or 'first' coordinate. (default \'\') + destination (string 'any' | 'last'): Returned route ends at 'any' or 'last' coordinate. (default \'\') + roundtrip (bool): Returned route is a roundtrip (route returns to first location). (default True) + RouteParameters (osrm.RouteParameters): Keyword arguments from parent class. + + Returns: + __init__ (osrm.TripParameters): A TripParameters object, for usage in Trip. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + source (string): Returned route starts at 'any' or 'first' coordinate. + destination (string): Returned route ends at 'any' or 'last' coordinate. + roundtrip (bool): Returned route is a roundtrip (route returns to first location). + RouteParameters (osrm.RouteParameters): Attributes from parent class. + """ + + @overload + def __init__( + self, + source: TripSourceType = "", + destination: TripDestinationType = "", + roundtrip: bool = True, + steps: bool = False, + alternatives: int = 0, + annotations: Sequence[RouteAnnotationsType] = [], + geometries: RouteGeometriesType = "", + overview: RouteOverviewType = "", + continue_straight: bool | None = None, + waypoints: Sequence[int] = [], + coordinates: Sequence[Coordinate] = [], + hints: Sequence[str | None] = [], + radiuses: Sequence[float | None] = [], + bearings: Sequence[Bearing | None] = [], + approaches: Sequence[Approach | None] = [], + generate_hints: bool = True, + exclude: Sequence[str] = [], + snapping: SnappingType = "", + ) -> None: ... + @property + def source(self) -> TripSourceType: ... + @source.setter + def source(self, arg: TripSourceType, /) -> None: ... + @property + def destination(self) -> TripDestinationType: ... + @destination.setter + def destination(self, arg: TripDestinationType, /) -> None: ... + @property + def roundtrip(self) -> bool: ... + @roundtrip.setter + def roundtrip(self, arg: bool, /) -> None: ... + def IsValid(self) -> bool: ... + +class TripSourceType: + def __init__(self, arg: str, /) -> None: + """Instantiates a SourceType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on SourceType value.""" + +class TripDestinationType: + def __init__(self, arg: str, /) -> None: + """Instantiates a DestinationType based on provided String value.""" + + def __repr__(self) -> str: + """Return a String based on DestinationType value.""" + +class TileParameters: + @overload + def __init__(self) -> None: + """ + Instantiates an instance of TileParameters. + + Examples: + >>> tile_params = osrm.TileParameters([17059, 11948, 15]) + >>> tile_params = osrm.TileParameters( + x = 17059, + y = 11948, + z = 15 + ) + >>> tile_params.IsValid() + True + + Args: + list (list of int): Instantiates an instance of TileParameters using an array [x, y, z]. + x (int): x value. + y (int): y value. + z (int): z value. + + Returns: + __init__ (osrm.TileParameters): A TileParameters object, for usage in Tile. + IsValid (bool): A bool value denoting validity of parameter values. + + Attributes: + x (int): x value. + y (int): y value. + z (int): z value. + """ + + @overload + def __init__(self, arg0: int, arg1: int, arg2: int, /) -> None: ... + @overload + def __init__(self, arg: Sequence[int], /) -> None: ... + @property + def x(self) -> int: ... + @x.setter + def x(self, arg: int, /) -> None: ... + @property + def y(self) -> int: ... + @y.setter + def y(self, arg: int, /) -> None: ... + @property + def z(self) -> int: ... + @z.setter + def z(self, arg: int, /) -> None: ... + def IsValid(self) -> bool: ... + +class OSRM: + @overload + def __init__(self, arg: EngineConfig, /) -> None: + """ + Instantiates an instance of OSRM. + + Examples: + >>> import osrm + >>> osrm_py = osrm.OSRM('.tests/test_data/ch/monaco.osrm') + >>> osrm_py = osrm.OSRM( + algorithm = 'CH', + storage_config = '.tests/test_data/ch/monaco.osrm', + max_locations_trip = 3, + max_locations_viaroute = 3, + max_locations_distance_table = 3, + max_locations_map_matching = 3, + max_results_nearest = 1, + max_alternatives = 1, + default_radius = 'unlimited' + ) + + Args: + storage_config (string): File path string to storage config. + EngineConfig (osrm.osrm_ext.EngineConfig): Keyword arguments from the EngineConfig class. + + Returns: + __init__ (osrm.OSRM): A OSRM object. + + Raises: + RuntimeError: On invalid OSRM EngineConfig parameters. + """ + + @overload + def __init__(self, arg: str, /) -> None: ... + @overload + def __init__(self, **kwargs) -> None: ... + def Match(self, arg: MatchParameters, /) -> Object: + """ + Matches/snaps given GPS points to the road network in the most plausible way. + + Examples: + >>> res = osrm_py.Match(match_params) + + Args: + match_params (osrm.MatchParameters): MatchParameters Object. + + Returns: + (json): [A Match JSON Response](https://project-osrm.org/docs/v5.24.0/api/#match-service). + + Raises: + RuntimeError: On invalid MatchParameters. + """ + + def Nearest(self, arg: NearestParameters, /) -> Object: + """ + Snaps a coordinate to the street network and returns the nearest matches. + + Examples: + >>> res = osrm_py.Nearest(nearest_params) + + Args: + nearest_params (osrm.NearestParameters): NearestParameters Object. + + Returns: + (json): [A Nearest JSON Response](https://project-osrm.org/docs/v5.24.0/api/#nearest-service). + + Raises: + RuntimeError: On invalid NearestParameters. + """ + + def Route(self, arg: RouteParameters, /) -> Object: + """ + Finds the fastest route between coordinates in the supplied order. + + Examples: + >>> res = osrm_py.Route(route_params) + + Args: + route_params (osrm.RouteParameters): RouteParameters Object. + + Returns: + (json): [A Route JSON Response](https://project-osrm.org/docs/v5.24.0/api/#route-service). + + Raises: + RuntimeError: On invalid RouteParameters. + """ + + def Table(self, arg: TableParameters, /) -> Object: + """ + Computes the duration of the fastest route between all pairs of supplied coordinates. + + Examples: + >>> res = osrm_py.Table(table_params) + + Args: + table_params (osrm.TableParameters): TableParameters Object. + + Returns: + (json): [A Table JSON Response](https://project-osrm.org/docs/v5.24.0/api/#table-service). + + Raises: + RuntimeError: On invalid TableParameters. + """ + + def Tile(self, arg: TileParameters, /) -> object: + """ + Computes the duration of the fastest route between all pairs of supplied coordinates. + + Examples: + >>> res = osrm_py.Tile(tile_params) + + Args: + tile_params (osrm.TileParameters): TileParameters Object. + + Returns: + (json): [A Tile JSON Response](https://project-osrm.org/docs/v5.24.0/api/#tile-service). + + Raises: + RuntimeError: On invalid TileParameters. + """ + + def Trip(self, arg: TripParameters, /) -> Object: + """ + Solves the Traveling Salesman Problem using a greedy heuristic (farthest-insertion algorithm). + + Examples: + >>> res = osrm_py.Trip(trip_params) + + Args: + trip_params (osrm.TripParameters): TripParameters Object. + + Returns: + (json): [A Trip JSON Response](https://project-osrm.org/docs/v5.24.0/api/#trip-service). + + Raises: + RuntimeError: On invalid TripParameters. + """ diff --git a/src/python/src/engineconfig_nb.cpp b/src/python/src/engineconfig_nb.cpp new file mode 100644 index 0000000000..8ca1776b81 --- /dev/null +++ b/src/python/src/engineconfig_nb.cpp @@ -0,0 +1,68 @@ +#include "python/engineconfig_nb.hpp" +#include "python/utility/osrm_utility.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/engine_config.hpp" + +#include +#include + +#include + +NB_MAKE_OPAQUE(osrm::engine::EngineConfig::Algorithm) + +namespace nb = nanobind; + +void init_EngineConfig(nb::module_ &m) +{ + using osrm::engine::EngineConfig; + + nb::class_(m, "EngineConfig", nb::is_final()) + .def(nb::init<>()) + .def("__init__", + [](EngineConfig *t, const nb::kwargs &kwargs) + { + new (t) EngineConfig(); + + osrm_nb_util::populate_cfg_from_kwargs(kwargs, *t); + + if (!t->IsValid()) + { + throw std::runtime_error("Config Parameters are Invalid"); + } + }) + .def("IsValid", &EngineConfig::IsValid) + .def("SetStorageConfig", + [](EngineConfig &self, const std::string &path) + { self.storage_config = osrm::storage::StorageConfig(path); }) + .def_rw("max_locations_trip", &EngineConfig::max_locations_trip) + .def_rw("max_locations_viaroute", &EngineConfig::max_locations_viaroute) + .def_rw("max_locations_distance_table", &EngineConfig::max_locations_distance_table) + .def_rw("max_locations_map_matching", &EngineConfig::max_locations_map_matching) + .def_rw("max_radius_map_matching", &EngineConfig::max_radius_map_matching) + .def_rw("max_results_nearest", &EngineConfig::max_results_nearest) + .def_rw("default_radius", &EngineConfig::default_radius) + .def_rw("max_alternatives", &EngineConfig::max_alternatives) + .def_rw("use_shared_memory", &EngineConfig::use_shared_memory) + .def_prop_rw( + "memory_file", + [](const EngineConfig &c) { return c.memory_file.string(); }, + [](EngineConfig &c, const std::string &val) + { c.memory_file = std::filesystem::path(val); }) + .def_rw("use_mmap", &EngineConfig::use_mmap) + .def_rw("algorithm", &EngineConfig::algorithm) + .def_rw("verbosity", &EngineConfig::verbosity) + .def_rw("dataset_name", &EngineConfig::dataset_name); + + nb::class_(m, "Algorithm") + .def("__init__", + [](EngineConfig::Algorithm *t, const std::string &str) + { + EngineConfig::Algorithm algorithm = + osrm_nb_util::str_to_enum(str, "Algorithm", algorithm_map); + new (t) EngineConfig::Algorithm(algorithm); + }) + .def("__repr__", + [](EngineConfig::Algorithm type) + { return osrm_nb_util::enum_to_str(type, "Algorithm", algorithm_map); }); + nb::implicitly_convertible(); +} diff --git a/src/python/src/osrm_nb.cpp b/src/python/src/osrm_nb.cpp new file mode 100644 index 0000000000..687af0480f --- /dev/null +++ b/src/python/src/osrm_nb.cpp @@ -0,0 +1,260 @@ +#include "python/engineconfig_nb.hpp" +#include "python/parameters/baseparameter_nb.hpp" +#include "python/parameters/matchparameter_nb.hpp" +#include "python/parameters/nearestparameter_nb.hpp" +#include "python/parameters/routeparameter_nb.hpp" +#include "python/parameters/tableparameter_nb.hpp" +#include "python/parameters/tileparameter_nb.hpp" +#include "python/parameters/tripparameter_nb.hpp" +#include "python/types/approach_nb.hpp" +#include "python/types/bearing_nb.hpp" +#include "python/types/coordinate_nb.hpp" +#include "python/types/jsoncontainer_nb.hpp" +#include "python/types/optional_nb.hpp" +#include "python/utility/osrm_utility.hpp" +#include "engine/api/match_parameters.hpp" +#include "engine/api/nearest_parameters.hpp" +#include "engine/api/route_parameters.hpp" +#include "engine/api/table_parameters.hpp" +#include "engine/api/tile_parameters.hpp" +#include "engine/api/trip_parameters.hpp" +#include "engine/engine_config.hpp" +#include "engine/status.hpp" +#include "osrm/osrm.hpp" + +#include +#include + +#include + +namespace nb = nanobind; + +NB_MODULE(osrm_ext, m) +{ + namespace api = osrm::engine::api; + namespace json = osrm::util::json; + + using osrm::OSRM; + using osrm::engine::EngineConfig; + using osrm::engine::api::MatchParameters; + using osrm::engine::api::NearestParameters; + using osrm::engine::api::RouteParameters; + using osrm::engine::api::TableParameters; + using osrm::engine::api::TileParameters; + using osrm::engine::api::TripParameters; + + init_EngineConfig(m); + + init_Approach(m); + init_Bearing(m); + init_Coordinate(m); + init_JSONContainer(m); + init_Optional(m); + + init_BaseParameters(m); + init_NearestParameters(m); + init_TableParameters(m); + init_RouteParameters(m); + init_MatchParameters(m); + init_TripParameters(m); + init_TileParameters(m); + + nb::class_(m, "OSRM", nb::is_final()) + .def(nb::init(), + "Instantiates an instance of OSRM.\n\n" + "Examples:\n\ + >>> import osrm\n\ + >>> osrm_py = osrm.OSRM('.tests/test_data/ch/monaco.osrm')\n\ + >>> osrm_py = osrm.OSRM(\n\ + algorithm = 'CH',\n\ + storage_config = '.tests/test_data/ch/monaco.osrm',\n\ + max_locations_trip = 3,\n\ + max_locations_viaroute = 3,\n\ + max_locations_distance_table = 3,\n\ + max_locations_map_matching = 3,\n\ + max_results_nearest = 1,\n\ + max_alternatives = 1,\n\ + default_radius = 'unlimited'\n\ + )\n\n" + "Args:\n\ + storage_config (string): File path string to storage config.\n\ + EngineConfig (osrm.osrm_ext.EngineConfig): Keyword arguments from the EngineConfig class.\n\n" + "Returns:\n\ + __init__ (osrm.OSRM): A OSRM object.\n\n" + "Raises:\n\ + RuntimeError: On invalid OSRM EngineConfig parameters.") + .def("__init__", + [](OSRM *t, const std::string &storage_path) + { + EngineConfig config; + config.storage_config = osrm::storage::StorageConfig(storage_path); + config.use_shared_memory = false; + + if (!config.IsValid()) + { + throw std::runtime_error("Required files are missing"); + } + + new (t) OSRM(config); + }) + .def("__init__", + [](OSRM *t, const nb::kwargs &kwargs) + { + EngineConfig config; + osrm_nb_util::populate_cfg_from_kwargs(kwargs, config); + + if (!config.IsValid()) + { + throw std::runtime_error("Config Parameters are Invalid"); + } + + new (t) OSRM(config); + }) + .def( + "Match", + [](OSRM *t, const MatchParameters ¶ms) + { + if (!params.IsValid()) + { + throw std::runtime_error("Invalid Match Parameters"); + } + + json::Object result; + osrm::engine::Status status = t->Match(params, result); + osrm_nb_util::check_status(status, result); + + return result; + }, + "Matches/snaps given GPS points to the road network in the most plausible way.\n\n" + "Examples:\n\ + >>> res = osrm_py.Match(match_params)\n\n" + "Args:\n\ + match_params (osrm.MatchParameters): MatchParameters Object.\n\n" + "Returns:\n\ + (json): [A Match JSON Response](https://project-osrm.org/docs/v5.24.0/api/#match-service).\n\n" + "Raises:\n\ + RuntimeError: On invalid MatchParameters.") + .def( + "Nearest", + [](OSRM *t, const NearestParameters ¶ms) + { + if (!params.IsValid()) + { + throw std::runtime_error("Invalid Nearest Parameters"); + } + + json::Object result; + osrm::engine::Status status = t->Nearest(params, result); + osrm_nb_util::check_status(status, result); + + return result; + }, + "Snaps a coordinate to the street network and returns the nearest matches.\n\n" + "Examples:\n\ + >>> res = osrm_py.Nearest(nearest_params)\n\n" + "Args:\n\ + nearest_params (osrm.NearestParameters): NearestParameters Object.\n\n" + "Returns:\n\ + (json): [A Nearest JSON Response](https://project-osrm.org/docs/v5.24.0/api/#nearest-service).\n\n" + "Raises:\n\ + RuntimeError: On invalid NearestParameters.") + .def( + "Route", + [](OSRM *t, const RouteParameters ¶ms) + { + if (!params.IsValid()) + { + throw std::runtime_error("Invalid Route Parameters"); + } + + json::Object result; + osrm::engine::Status status = t->Route(params, result); + osrm_nb_util::check_status(status, result); + + return result; + }, + "Finds the fastest route between coordinates in the supplied order.\n\n" + "Examples:\n\ + >>> res = osrm_py.Route(route_params)\n\n" + "Args:\n\ + route_params (osrm.RouteParameters): RouteParameters Object.\n\n" + "Returns:\n\ + (json): [A Route JSON Response](https://project-osrm.org/docs/v5.24.0/api/#route-service).\n\n" + "Raises:\n\ + RuntimeError: On invalid RouteParameters.") + .def( + "Table", + [](OSRM *t, const TableParameters ¶ms) + { + if (!params.IsValid()) + { + throw std::runtime_error("Invalid Table Parameters"); + } + + json::Object result; + osrm::engine::Status status = t->Table(params, result); + osrm_nb_util::check_status(status, result); + + return result; + }, + "Computes the duration of the fastest route between all pairs of supplied " + "coordinates.\n\n" + "Examples:\n\ + >>> res = osrm_py.Table(table_params)\n\n" + "Args:\n\ + table_params (osrm.TableParameters): TableParameters Object.\n\n" + "Returns:\n\ + (json): [A Table JSON Response](https://project-osrm.org/docs/v5.24.0/api/#table-service).\n\n" + "Raises:\n\ + RuntimeError: On invalid TableParameters.") + .def( + "Tile", + [](OSRM *t, const TileParameters ¶ms) + { + if (!params.IsValid()) + { + throw std::runtime_error("Invalid Tile Parameters"); + } + + std::string result; + t->Tile(params, result); + nb::object obj = nb::bytes(result.c_str(), result.size()); + + return obj; + }, + "Computes the duration of the fastest route between all pairs of supplied " + "coordinates.\n\n" + "Examples:\n\ + >>> res = osrm_py.Tile(tile_params)\n\n" + "Args:\n\ + tile_params (osrm.TileParameters): TileParameters Object.\n\n" + "Returns:\n\ + (json): [A Tile JSON Response](https://project-osrm.org/docs/v5.24.0/api/#tile-service).\n\n" + "Raises:\n\ + RuntimeError: On invalid TileParameters.") + .def( + "Trip", + [](OSRM *t, const TripParameters ¶ms) + { + if (!params.IsValid()) + { + throw std::runtime_error("Invalid Trip Parameters"); + } + + json::Object result; + osrm::engine::Status status = t->Trip(params, result); + osrm_nb_util::check_status(status, result); + + return result; + }, + "Solves the Traveling Salesman Problem using a greedy heuristic (farthest-insertion " + "algorithm).\n\n" + "Examples:\n\ + >>> res = osrm_py.Trip(trip_params)\n\n" + "Args:\n\ + trip_params (osrm.TripParameters): TripParameters Object.\n\n" + "Returns:\n\ + (json): [A Trip JSON Response](https://project-osrm.org/docs/v5.24.0/api/#trip-service).\n\n" + "Raises:\n\ + RuntimeError: On invalid TripParameters."); +} diff --git a/src/python/src/parameters/baseparameter_nb.cpp b/src/python/src/parameters/baseparameter_nb.cpp new file mode 100644 index 0000000000..ce717ea444 --- /dev/null +++ b/src/python/src/parameters/baseparameter_nb.cpp @@ -0,0 +1,124 @@ +#include "python/parameters/baseparameter_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/api/base_parameters.hpp" +#include "engine/hint.hpp" + +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_BaseParameters(nb::module_ &m) +{ + using osrm::engine::api::BaseParameters; + + nb::class_(m, "BaseParameters") + .def(nb::init<>(), + "Instantiates an instance of BaseParameters.\n\n" + "Note:\n\ + This is the parent class to many parameter classes, and not intended to be used on its own.\n\n" + "Args:\n\ + coordinates (list of floats pairs): Pairs of Longitude and Latitude Coordinates. (default [])\n\ + hints (list): Hint from previous request to derive position in street network. (default [])\n\ + radiuses (list of floats): Limits the search to given radius in meters. (default [])\n\ + bearings (list of int pairs): Limits the search to segments with given bearing in degrees towards true north in clockwise direction. (default [])\n\ + approaches (list): Keep waypoints on curb side. (default [])\n\ + generate_hints (bool): Adds a hint to the response which can be used in subsequent requests. (default True)\n\ + exclude (list of strings): Additive list of classes to avoid. (default [])\n\ + snapping (string 'default' | 'any'): 'default' snapping avoids is_startpoint edges, 'any' will snap to any edge in the graph. (default '')\n\n" + "Returns:\n\ + __init__ (osrm.osrm_ext.BaseParameters): A BaseParameter object, that is the parent object to many other Parameter objects.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + coordinates (list of floats pairs): Pairs of longitude & latitude coordinates.\n\ + hints (list): Hint from previous request to derive position in street network.\n\ + radiuses (list of floats): Limits the search to given radius in meters.\n\ + bearings (list of int pairs): Limits the search to segments with given bearing in degrees towards true north in clockwise direction.\n\ + approaches (list): Keep waypoints on curb side.\n\ + exclude (list of strings): Additive list of classes to avoid, order does not matter.\n\ + format (string): Specifies response type - currently only 'json' is supported.\n\ + generate_hints (bool): Adds a hint to the response which can be used in subsequent requests.\n\ + skip_waypoints (list): Removes waypoints from the response.\n\ + snapping (string): 'default' snapping avoids is_startpoint edges, 'any' will snap to any edge in the graph.") + .def_rw("coordinates", &BaseParameters::coordinates) + .def_prop_rw( + "hints", + [](const BaseParameters &p) + { + nb::list result; + for (const auto &h : p.hints) + { + if (h) + { + result.append(h->ToBase64()); + } + else + { + result.append(nb::none()); + } + } + return result; + }, + [](BaseParameters &p, const nb::list &hints) + { + p.hints.clear(); + for (auto item : hints) + { + if (item.is_none()) + { + p.hints.push_back(std::nullopt); + } + else + { + p.hints.push_back( + osrm::engine::Hint::FromBase64(nb::cast(item))); + } + } + }) + .def_rw("radiuses", &BaseParameters::radiuses) + .def_rw("bearings", &BaseParameters::bearings) + .def_rw("approaches", &BaseParameters::approaches) + .def_rw("exclude", &BaseParameters::exclude) + .def_rw("format", &BaseParameters::format) + .def_rw("generate_hints", &BaseParameters::generate_hints) + .def_rw("skip_waypoints", &BaseParameters::skip_waypoints) + .def_rw("snapping", &BaseParameters::snapping) + .def("IsValid", &BaseParameters::IsValid); + + nb::class_(m, "SnappingType") + .def( + "__init__", + [](BaseParameters::SnappingType *t, const std::string &str) + { + BaseParameters::SnappingType snapping = + osrm_nb_util::str_to_enum(str, "SnappingType", snapping_map); + new (t) BaseParameters::SnappingType(snapping); + }, + "Instantiates a SnappingType based on provided String value.") + .def( + "__repr__", + [](BaseParameters::SnappingType type) + { return osrm_nb_util::enum_to_str(type, "SnappingType", snapping_map); }, + "Return a String based on SnappingType value."); + nb::implicitly_convertible(); + + nb::class_(m, "OutputFormatType") + .def( + "__init__", + [](BaseParameters::OutputFormatType *t, const std::string &str) + { + BaseParameters::OutputFormatType output = + osrm_nb_util::str_to_enum(str, "OutputFormatType", output_map); + new (t) BaseParameters::OutputFormatType(output); + }, + "Instantiates a OutputFormatType based on provided String value.") + .def( + "__repr__", + [](BaseParameters::OutputFormatType type) + { return osrm_nb_util::enum_to_str(type, "OutputFormatType", output_map); }, + "Return a String based on OutputFormatType value."); + nb::implicitly_convertible(); +} diff --git a/src/python/src/parameters/matchparameter_nb.cpp b/src/python/src/parameters/matchparameter_nb.cpp new file mode 100644 index 0000000000..7da22c7b3a --- /dev/null +++ b/src/python/src/parameters/matchparameter_nb.cpp @@ -0,0 +1,130 @@ +#include "python/parameters/matchparameter_nb.hpp" +#include "python/parameters/routeparameter_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/api/match_parameters.hpp" + +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_MatchParameters(nb::module_ &m) +{ + using osrm::engine::api::MatchParameters; + using osrm::engine::api::RouteParameters; + + nb::class_(m, "MatchParameters") + .def(nb::init<>(), + "Instantiates an instance of MatchParameters.\n\n" + "Examples:\n\ + >>> match_params = osrm.MatchParameters(\n\ + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077), (7.41862, 43.73216)],\n\ + timestamps = [1424684612, 1424684616, 1424684620],\n\ + gaps = 'split',\n\ + tidy = True\n\ + )\n\ + >>> match_params.IsValid()\n\ + True\n\n" + "Args:\n\ + timestamps (list of unsigned int): Timestamps for the input locations in seconds since UNIX epoch. (default [])\n\ + gaps (list of 'split' | 'ignore'): Allows the input track splitting based on huge timestamp gaps between points. (default [])\n\ + tidy (bool): Allows the input track modification to obtain better matching quality for noisy tracks. (default False)\n\ + RouteParameters (osrm.RouteParameters): Keyword arguments from parent class.\n\n" + "Returns:\n\ + __init__ (osrm.MatchParameters): A MatchParameters object, for usage in Match.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + timestamps (list of unsigned int): Timestamps for the input locations in seconds since UNIX epoch.\n\ + gaps (string): Allows the input track splitting based on huge timestamp gaps between points.\n\ + tidy (bool): Allows the input track modification to obtain better matching quality for noisy tracks.\n\ + RouteParameters (osrm.RouteParameters): Attributes from parent class.") + .def( + "__init__", + [](MatchParameters *t, + std::vector timestamps, + MatchParameters::GapsType gaps_type, + bool tidy, + const bool steps, + int number_of_alternatives, + const std::vector &annotations, + RouteParameters::GeometriesType geometries, + RouteParameters::OverviewType overview, + const std::optional continue_straight, + std::vector waypoints, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping) + { + new (t) MatchParameters(); + + t->timestamps = std::move(timestamps); + t->gaps = gaps_type; + t->tidy = tidy; + + osrm_nb_util::assign_routeparameters(t, + steps, + number_of_alternatives, + annotations, + geometries, + overview, + continue_straight, + waypoints); + + osrm_nb_util::assign_baseparameters(t, + coordinates, + hints, + radiuses, + bearings, + approaches, + generate_hints, + exclude, + snapping); + }, + "timestamps"_a = std::vector(), + "gaps"_a = std::string(), + "tidy"_a = false, + "steps"_a = false, + "number_of_alternatives"_a = 0, + "annotations"_a = std::vector(), + "geometries"_a = std::string(), + "overview"_a = std::string(), + "continue_straight"_a = std::optional(), + "waypoints"_a = std::vector(), + "coordinates"_a = std::vector(), + "hints"_a = std::vector>(), + "radiuses"_a = std::vector>(), + "bearings"_a = std::vector>(), + "approaches"_a = std::vector(), + "generate_hints"_a = true, + "exclude"_a = std::vector(), + "snapping"_a = std::string()) + .def_rw("timestamps", &MatchParameters::timestamps) + .def_rw("gaps", &MatchParameters::gaps) + .def_rw("tidy", &MatchParameters::tidy) + .def("IsValid", &MatchParameters::IsValid); + + nb::class_(m, "MatchGapsType") + .def( + "__init__", + [](MatchParameters::GapsType *t, const std::string &str) + { + MatchParameters::GapsType gaps = + osrm_nb_util::str_to_enum(str, "MatchGapsType", gaps_map); + new (t) MatchParameters::GapsType(gaps); + }, + "Instantiates a GapsType based on provided String value.") + .def( + "__repr__", + [](MatchParameters::GapsType type) + { return osrm_nb_util::enum_to_str(type, "MatchGapsType", gaps_map); }, + "Return a String based on GapsType value."); + nb::implicitly_convertible(); +} diff --git a/src/python/src/parameters/nearestparameter_nb.cpp b/src/python/src/parameters/nearestparameter_nb.cpp new file mode 100644 index 0000000000..ff61ac98c4 --- /dev/null +++ b/src/python/src/parameters/nearestparameter_nb.cpp @@ -0,0 +1,71 @@ +#include "python/parameters/nearestparameter_nb.hpp" +#include "python/parameters/baseparameter_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/api/nearest_parameters.hpp" + +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_NearestParameters(nb::module_ &m) +{ + using osrm::engine::api::BaseParameters; + using osrm::engine::api::NearestParameters; + + nb::class_(m, "NearestParameters") + .def(nb::init<>(), + "Instantiates an instance of NearestParameters.\n\n" + "Examples:\n\ + >>> nearest_params = osrm.NearestParameters(\n\ + coordinates = [(7.41337, 43.72956)],\n\ + exclude = ['motorway']\n\ + )\n\ + >>> nearest_params.IsValid()\n\ + True\n\n" + "Args:\n\ + BaseParameters (osrm.osrm_ext.BaseParameters): Keyword arguments from parent class.\n\n" + "Returns:\n\ + __init__ (osrm.NearestParameters): A NearestParameters object, for usage in osrm.OSRM.Nearest.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + number_of_results (unsigned int): Number of nearest segments that should be returned.\n\ + BaseParameters (osrm.osrm_ext.BaseParameters): Attributes from parent class.") + .def( + "__init__", + [](NearestParameters *t, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping) + { + new (t) NearestParameters(); + + osrm_nb_util::assign_baseparameters(t, + coordinates, + hints, + radiuses, + bearings, + approaches, + generate_hints, + exclude, + snapping); + }, + "coordinates"_a = std::vector(), + "hints"_a = std::vector>(), + "radiuses"_a = std::vector>(), + "bearings"_a = std::vector>(), + "approaches"_a = std::vector(), + "generate_hints"_a = true, + "exclude"_a = std::vector(), + "snapping"_a = std::string()) + .def_rw("number_of_results", &NearestParameters::number_of_results) + .def("IsValid", &NearestParameters::IsValid); +} diff --git a/src/python/src/parameters/routeparameter_nb.cpp b/src/python/src/parameters/routeparameter_nb.cpp new file mode 100644 index 0000000000..c0aef476d1 --- /dev/null +++ b/src/python/src/parameters/routeparameter_nb.cpp @@ -0,0 +1,195 @@ +#include "python/parameters/routeparameter_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/api/route_parameters.hpp" + +#include +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_RouteParameters(nb::module_ &m) +{ + using osrm::engine::api::BaseParameters; + using osrm::engine::api::RouteParameters; + + nb::class_(m, "RouteParameters") + .def(nb::init<>(), + "Instantiates an instance of RouteParameters.\n\n" + "Examples:\n\ + >>> route_params = osrm.RouteParameters(\n\ + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)],\n\ + steps = True,\n\ + number_of_alternatives = 3,\n\ + annotations = ['speed'],\n\ + geometries = 'polyline',\n\ + overview = 'simplified',\n\ + continue_straight = False,\n\ + waypoints = [0, 1],\n\ + radiuses = [4.07, 4.07],\n\ + bearings = [(200, 180), (250, 180)],\n\ + # approaches = ['unrestricted', 'unrestricted'],\n\ + generate_hints = False,\n\ + exclude = ['motorway'],\n\ + snapping = 'any'\n\ + )\n\ + >>> route_params.IsValid()\n\ + True\n\n" + "Args:\n\ + steps (bool): Return route steps for each route leg. (default False)\n\ + number_of_alternatives (int): Search for n alternative routes. (default 0)\n\ + annotations (list of 'none' | 'duration' | 'nodes' | 'distance' | 'weight' | 'datasources' \ + | 'speed' | 'all'): Returns additional metadata for each coordinate along the route geometry. (default [])\n\ + geometries (string 'polyline' | 'polyline6' | 'geojson'): Returned route geometry format - influences overview and per step. (default " + ")\n\ + overview (string 'simplified' | 'full' | 'false'): Add overview geometry either full, simplified. (default '')\n\ + continue_straight (bool): Forces the route to keep going straight at waypoints, constraining u-turns. (default {})\n\ + waypoints (list of int): Treats input coordinates indicated by given indices as waypoints in returned Match object. (default [])\n\ + BaseParameters (osrm.osrm_ext.BaseParameters): Keyword arguments from parent class.\n\n" + "Returns:\n\ + __init__ (osrm.RouteParameters): A RouteParameters object, for usage in Route.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + steps (bool): Return route steps for each route leg.\n\ + alternatives (bool): Search for alternative routes.\n\ + number_of_alternatives (int): Search for n alternative routes.\n\ + annotations_type (string): Returns additional metadata for each coordinate along the route geometry.\n\ + geometries (string): Returned route geometry format - influences overview and per step.\n\ + overview (string): Add overview geometry either full, simplified.\n\ + continue_straight (bool): Forces the route to keep going straight at waypoints, constraining u-turns.\n\ + BaseParameters (osrm.osrm_ext.BaseParameters): Attributes from parent class.") + .def( + "__init__", + [](RouteParameters *t, + const bool steps, + int number_of_alternatives, + const std::vector &annotations, + RouteParameters::GeometriesType geometries, + RouteParameters::OverviewType overview, + const std::optional continue_straight, + std::vector waypoints, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping) + { + new (t) RouteParameters(); + + osrm_nb_util::assign_routeparameters(t, + steps, + number_of_alternatives, + annotations, + geometries, + overview, + continue_straight, + waypoints); + + osrm_nb_util::assign_baseparameters(t, + coordinates, + hints, + radiuses, + bearings, + approaches, + generate_hints, + exclude, + snapping); + }, + "steps"_a = false, + "number_of_alternatives"_a = 0, + "annotations"_a = std::vector(), + "geometries"_a = std::string(), + "overview"_a = std::string(), + "continue_straight"_a = std::optional(), + "waypoints"_a = std::vector(), + "coordinates"_a = std::vector(), + "hints"_a = std::vector>(), + "radiuses"_a = std::vector>(), + "bearings"_a = std::vector>(), + "approaches"_a = std::vector(), + "generate_hints"_a = true, + "exclude"_a = std::vector(), + "snapping"_a = std::string()) + .def_rw("steps", &RouteParameters::steps) + .def_rw("alternatives", &RouteParameters::alternatives) + .def_rw("number_of_alternatives", &RouteParameters::number_of_alternatives) + .def_rw("annotations_type", &RouteParameters::annotations_type) + .def_rw("geometries", &RouteParameters::geometries) + .def_rw("overview", &RouteParameters::overview) + .def_rw("continue_straight", &RouteParameters::continue_straight) + .def("IsValid", &RouteParameters::IsValid); + + nb::class_(m, "RouteGeometriesType") + .def( + "__init__", + [](RouteParameters::GeometriesType *t, const std::string &str) + { + RouteParameters::GeometriesType geometries = + osrm_nb_util::str_to_enum(str, "RouteGeometriesType", geometries_map); + new (t) RouteParameters::GeometriesType(geometries); + }, + "Instantiates a GeometriesType based on provided String value.") + .def( + "__repr__", + [](RouteParameters::GeometriesType type) + { return osrm_nb_util::enum_to_str(type, "RouteGeometriesType", geometries_map); }, + "Return a String based on GeometriesType value."); + nb::implicitly_convertible(); + + nb::class_(m, "RouteOverviewType") + .def( + "__init__", + [](RouteParameters::OverviewType *t, const std::string &str) + { + RouteParameters::OverviewType overview = + osrm_nb_util::str_to_enum(str, "RouteOverviewType", overview_map); + new (t) RouteParameters::OverviewType(overview); + }, + "Instantiates a OverviewType based on provided String value.") + .def( + "__repr__", + [](RouteParameters::OverviewType type) + { return osrm_nb_util::enum_to_str(type, "RouteOverviewType", overview_map); }, + "Return a String based on OverviewType value."); + nb::implicitly_convertible(); + + nb::class_(m, "RouteAnnotationsType") + .def( + "__init__", + [](RouteParameters::AnnotationsType *t, const std::string &str) + { + RouteParameters::AnnotationsType annotation = + osrm_nb_util::str_to_enum(str, "RouteAnnotationsType", route_annotations_map); + new (t) RouteParameters::AnnotationsType(annotation); + }, + "Instantiates a AnnotationsType based on provided String value.") + .def( + "__repr__", + [](RouteParameters::AnnotationsType type) { return std::to_string((int)type); }, + "Return a String based on AnnotationsType value.") + .def( + "__and__", + [](RouteParameters::AnnotationsType lhs, RouteParameters::AnnotationsType rhs) + { return lhs & rhs; }, + nb::is_operator(), + "Return the bitwise AND result of two AnnotationsTypes.") + .def( + "__or__", + [](RouteParameters::AnnotationsType lhs, RouteParameters::AnnotationsType rhs) + { return lhs | rhs; }, + nb::is_operator(), + "Return the bitwise OR result of two AnnotationsTypes.") + .def( + "__ior__", + [](RouteParameters::AnnotationsType &lhs, RouteParameters::AnnotationsType rhs) + { return lhs = lhs | rhs; }, + nb::is_operator(), + "Add the bitwise OR value of another AnnotationsType."); + nb::implicitly_convertible(); +} diff --git a/src/python/src/parameters/tableparameter_nb.cpp b/src/python/src/parameters/tableparameter_nb.cpp new file mode 100644 index 0000000000..665e36cf57 --- /dev/null +++ b/src/python/src/parameters/tableparameter_nb.cpp @@ -0,0 +1,175 @@ +#include "python/parameters/tableparameter_nb.hpp" +#include "python/parameters/baseparameter_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/api/table_parameters.hpp" + +#include +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_TableParameters(nb::module_ &m) +{ + using osrm::engine::api::BaseParameters; + using osrm::engine::api::TableParameters; + static const std::unordered_map + table_annotations_map{{"none", TableParameters::AnnotationsType::None}, + {std::string(), TableParameters::AnnotationsType::None}, + {"duration", TableParameters::AnnotationsType::Duration}, + {"distance", TableParameters::AnnotationsType::Distance}, + {"all", TableParameters::AnnotationsType::All}}; + + nb::class_(m, "TableParameters") + .def(nb::init<>(), + "Instantiates an instance of TableParameters.\n\n" + "Examples:\n\ + >>> table_params = osrm.TableParameters(\n\ + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)],\n\ + sources = [0],\n\ + destinations = [1],\n\ + annotations = ['duration'],\n\ + fallback_speed = 1,\n\ + fallback_coordinate_type = 'input',\n\ + scale_factor = 0.9\n\ + )\n\ + >>> table_params.IsValid()\n\ + True\n\n" + "Args:\n\ + sources (list of int): Use location with given index as source. (default [])\n\ + destinations (list of int): Use location with given index as destination. (default [])\n\ + annotations (list of 'none' | 'duration' | 'distance' | 'all'): \ + Returns additional metadata for each coordinate along the route geometry. (default [])\n\ + fallback_speed (float): If no route found between a source/destination pair, calculate the as-the-crow-flies distance, \ + then use this speed to estimate duration. (default INVALID_FALLBACK_SPEED)\n\ + fallback_coordinate_type (string 'input' | 'snapped'): When using a fallback_speed, use the user-supplied coordinate (input), \ + or the snapped location (snapped) for calculating distances. (default '')\n\ + scale_factor: Scales the table duration values by this number (use in conjunction with annotations=durations). (default 1.0)\n\ + BaseParameters (osrm.osrm_ext.BaseParameters): Keyword arguments from parent class.\n\n" + "Returns:\n\ + __init__ (osrm.TableParameters): A TableParameters object, for usage in Table.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + sources (list of int): Use location with given index as source.\n\ + destinations (list of int): Use location with given index as destination.\n\ + annotations (string): Returns additional metadata for each coordinate along the route geometry.\n\ + fallback_speed (float): If no route found between a source/destination pair, calculate the as-the-crow-flies distance, \ + then use this speed to estimate duration.\n\ + fallback_coordinate_type (string): When using a fallback_speed, use the user-supplied coordinate (input), \ + or the snapped location (snapped) for calculating distances.\n\ + scale_factor: Scales the table duration values by this number (use in conjunction with annotations=durations).\n\ + BaseParameters (osrm.osrm_ext.BaseParameters): Attributes from parent class.") + .def( + "__init__", + [](TableParameters *t, + std::vector sources, + std::vector destinations, + const std::vector &annotations, + double fallback_speed, + TableParameters::FallbackCoordinateType fallback_coordinate_type, + double scale_factor, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping) + { + new (t) TableParameters(); + + t->sources = std::move(sources); + t->destinations = std::move(destinations); + t->annotations = osrm_nb_util::calculate_tableannotations_type(annotations); + t->fallback_speed = fallback_speed; + t->fallback_coordinate_type = fallback_coordinate_type; + t->scale_factor = scale_factor; + + osrm_nb_util::assign_baseparameters(t, + coordinates, + hints, + radiuses, + bearings, + approaches, + generate_hints, + exclude, + snapping); + }, + "sources"_a = std::vector(), + "destinations"_a = std::vector(), + "annotations"_a = std::vector(), + "fallback_speed"_a = osrm::from_alias(INVALID_FALLBACK_SPEED), + "fallback_coordinate_type"_a = std::string(), + "scale_factor"_a = 1.0, + "coordinates"_a = std::vector(), + "hints"_a = std::vector>(), + "radiuses"_a = std::vector>(), + "bearings"_a = std::vector>(), + "approaches"_a = std::vector(), + "generate_hints"_a = true, + "exclude"_a = std::vector(), + "snapping"_a = std::string()) + .def_rw("sources", &TableParameters::sources) + .def_rw("destinations", &TableParameters::destinations) + .def_rw("fallback_speed", &TableParameters::fallback_speed) + .def_rw("fallback_coordinate_type", &TableParameters::fallback_coordinate_type) + .def_rw("annotations", &TableParameters::annotations) + .def_rw("scale_factor", &TableParameters::scale_factor) + .def("IsValid", &TableParameters::IsValid); + + nb::class_(m, "TableFallbackCoordinateType") + .def( + "__init__", + [](TableParameters::FallbackCoordinateType *t, const std::string &str) + { + TableParameters::FallbackCoordinateType fallback = + osrm_nb_util::str_to_enum(str, "TableFallbackCoordinateType", fallback_map); + new (t) TableParameters::FallbackCoordinateType(fallback); + }, + "Instantiates a FallbackCoordinateType based on provided String value.") + .def( + "__repr__", + [](TableParameters::FallbackCoordinateType type) { + return osrm_nb_util::enum_to_str(type, "TableFallbackCoordinateType", fallback_map); + }, + "Return a String based on FallbackCoordinateType value."); + nb::implicitly_convertible(); + + nb::class_(m, "TableAnnotationsType") + .def( + "__init__", + [](TableParameters::AnnotationsType *t, const std::string &str) + { + TableParameters::AnnotationsType annotation = + osrm_nb_util::str_to_enum(str, "TableAnnotationsType", table_annotations_map); + new (t) TableParameters::AnnotationsType(annotation); + }, + "Instantiates a AnnotationsType based on provided String value.") + .def( + "__repr__", + [](TableParameters::AnnotationsType type) { return std::to_string((int)type); }, + "Return a String based on AnnotationsType value.") + .def( + "__and__", + [](TableParameters::AnnotationsType lhs, TableParameters::AnnotationsType rhs) + { return lhs & rhs; }, + nb::is_operator(), + "Return the bitwise AND result of two AnnotationsTypes.") + .def( + "__or__", + [](TableParameters::AnnotationsType lhs, TableParameters::AnnotationsType rhs) + { return lhs | rhs; }, + nb::is_operator(), + "Return the bitwise OR result of two AnnotationsTypes.") + .def( + "__ior__", + [](TableParameters::AnnotationsType &lhs, TableParameters::AnnotationsType rhs) + { return lhs = lhs | rhs; }, + nb::is_operator(), + "Add the bitwise OR value of another AnnotationsType."); + nb::implicitly_convertible(); +} diff --git a/src/python/src/parameters/tileparameter_nb.cpp b/src/python/src/parameters/tileparameter_nb.cpp new file mode 100644 index 0000000000..ec7127b20e --- /dev/null +++ b/src/python/src/parameters/tileparameter_nb.cpp @@ -0,0 +1,57 @@ +#include "python/parameters/tileparameter_nb.hpp" +#include "engine/api/tile_parameters.hpp" + +#include +#include +#include + +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_TileParameters(nb::module_ &m) +{ + using osrm::engine::api::TileParameters; + + nb::class_(m, "TileParameters", nb::is_final()) + .def(nb::init<>(), + "Instantiates an instance of TileParameters.\n\n" + "Examples:\n\ + >>> tile_params = osrm.TileParameters([17059, 11948, 15])\n\ + >>> tile_params = osrm.TileParameters(\n\ + x = 17059,\n\ + y = 11948,\n\ + z = 15\n\ + )\n\ + >>> tile_params.IsValid()\n\ + True\n\n" + "Args:\n\ + list (list of int): Instantiates an instance of TileParameters using an array [x, y, z].\n\ + x (int): x value.\n\ + y (int): y value.\n\ + z (int): z value.\n\n" + "Returns:\n\ + __init__ (osrm.TileParameters): A TileParameters object, for usage in Tile.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + x (int): x value.\n\ + y (int): y value.\n\ + z (int): z value.") + .def(nb::init()) + .def("__init__", + [](TileParameters *t, const std::vector &coord) + { + if (coord.size() != 3) + { + throw std::runtime_error("Parameter must be an array [x, y, z]"); + } + + new (t) TileParameters{coord[0], coord[1], coord[2]}; + }) + .def_rw("x", &TileParameters::x) + .def_rw("y", &TileParameters::y) + .def_rw("z", &TileParameters::z) + .def("IsValid", &TileParameters::IsValid); + nb::implicitly_convertible, TileParameters>(); +} diff --git a/src/python/src/parameters/tripparameter_nb.cpp b/src/python/src/parameters/tripparameter_nb.cpp new file mode 100644 index 0000000000..962d84c777 --- /dev/null +++ b/src/python/src/parameters/tripparameter_nb.cpp @@ -0,0 +1,147 @@ +#include "python/parameters/tripparameter_nb.hpp" +#include "python/parameters/routeparameter_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/api/trip_parameters.hpp" + +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_TripParameters(nb::module_ &m) +{ + using osrm::engine::api::RouteParameters; + using osrm::engine::api::TripParameters; + + nb::class_(m, "TripParameters") + .def(nb::init<>(), + "Instantiates an instance of TripParameters.\n\n" + "Examples:\n\ + >>> trip_params = osrm.TripParameters(\n\ + coordinates = [(7.41337, 43.72956), (7.41546, 43.73077)],\n\ + source = 'any',\n\ + destination = 'last',\n\ + roundtrip = False\n\ + )\n\ + >>> trip_params.IsValid()\n\ + True\n\n" + "Args:\n\ + source (string 'any' | 'first'): Returned route starts at 'any' or 'first' coordinate. (default '')\n\ + destination (string 'any' | 'last'): Returned route ends at 'any' or 'last' coordinate. (default '')\n\ + roundtrip (bool): Returned route is a roundtrip (route returns to first location). (default True)\n\ + RouteParameters (osrm.RouteParameters): Keyword arguments from parent class.\n\n" + "Returns:\n\ + __init__ (osrm.TripParameters): A TripParameters object, for usage in Trip.\n\ + IsValid (bool): A bool value denoting validity of parameter values.\n\n" + "Attributes:\n\ + source (string): Returned route starts at 'any' or 'first' coordinate.\n\ + destination (string): Returned route ends at 'any' or 'last' coordinate.\n\ + roundtrip (bool): Returned route is a roundtrip (route returns to first location).\n\ + RouteParameters (osrm.RouteParameters): Attributes from parent class.") + .def( + "__init__", + [](TripParameters *t, + TripParameters::SourceType source, + TripParameters::DestinationType destination, + bool roundtrip, + const bool steps, + int number_of_alternatives, + const std::vector &annotations, + RouteParameters::GeometriesType geometries, + RouteParameters::OverviewType overview, + const std::optional continue_straight, + std::vector waypoints, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping) + { + new (t) TripParameters(); + + t->source = source; + t->destination = destination; + t->roundtrip = roundtrip; + + osrm_nb_util::assign_routeparameters(t, + steps, + number_of_alternatives, + annotations, + geometries, + overview, + continue_straight, + waypoints); + + osrm_nb_util::assign_baseparameters(t, + coordinates, + hints, + radiuses, + bearings, + approaches, + generate_hints, + exclude, + snapping); + }, + "source"_a = std::string(), + "destination"_a = std::string(), + "roundtrip"_a = true, + "steps"_a = false, + "alternatives"_a = 0, + "annotations"_a = std::vector(), + "geometries"_a = std::string(), + "overview"_a = std::string(), + "continue_straight"_a = std::optional(), + "waypoints"_a = std::vector(), + "coordinates"_a = std::vector(), + "hints"_a = std::vector>(), + "radiuses"_a = std::vector>(), + "bearings"_a = std::vector>(), + "approaches"_a = std::vector(), + "generate_hints"_a = true, + "exclude"_a = std::vector(), + "snapping"_a = std::string()) + .def_rw("source", &TripParameters::source) + .def_rw("destination", &TripParameters::destination) + .def_rw("roundtrip", &TripParameters::roundtrip) + .def("IsValid", &TripParameters::IsValid); + + nb::class_(m, "TripSourceType") + .def( + "__init__", + [](TripParameters::SourceType *t, const std::string &str) + { + TripParameters::SourceType source = + osrm_nb_util::str_to_enum(str, "TripSourceType", source_map); + new (t) TripParameters::SourceType(source); + }, + "Instantiates a SourceType based on provided String value.") + .def( + "__repr__", + [](TripParameters::SourceType type) + { return osrm_nb_util::enum_to_str(type, "TripSourceType", source_map); }, + "Return a String based on SourceType value."); + nb::implicitly_convertible(); + + nb::class_(m, "TripDestinationType") + .def( + "__init__", + [](TripParameters::DestinationType *t, const std::string &str) + { + TripParameters::DestinationType dest = + osrm_nb_util::str_to_enum(str, "TripDestinationType", dest_map); + new (t) TripParameters::DestinationType(dest); + }, + "Instantiates a DestinationType based on provided String value.") + .def( + "__repr__", + [](TripParameters::DestinationType type) + { return osrm_nb_util::enum_to_str(type, "TripDestinationType", dest_map); }, + "Return a String based on DestinationType value."); + nb::implicitly_convertible(); +} diff --git a/src/python/src/types/approach_nb.cpp b/src/python/src/types/approach_nb.cpp new file mode 100644 index 0000000000..bfed97684d --- /dev/null +++ b/src/python/src/types/approach_nb.cpp @@ -0,0 +1,27 @@ +#include "python/types/approach_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/approach.hpp" + +#include +#include + +NB_MAKE_OPAQUE(osrm::engine::Approach) + +namespace nb = nanobind; + +void init_Approach(nb::module_ &m) +{ + using osrm::engine::Approach; + + nb::class_(m, "Approach") + .def("__init__", + [](Approach *t, const std::string &str) + { + Approach approach = osrm_nb_util::str_to_enum(str, "Approach", approach_map); + new (t) Approach(approach); + }) + .def("__repr__", + [](Approach type) + { return osrm_nb_util::enum_to_str(type, "Approach", approach_map); }); + nb::implicitly_convertible(); +} diff --git a/src/python/src/types/bearing_nb.cpp b/src/python/src/types/bearing_nb.cpp new file mode 100644 index 0000000000..5474673011 --- /dev/null +++ b/src/python/src/types/bearing_nb.cpp @@ -0,0 +1,25 @@ +#include "python/types/bearing_nb.hpp" +#include "engine/bearing.hpp" + +#include +#include +#include + +namespace nb = nanobind; + +void init_Bearing(nb::module_ &m) +{ + using osrm::engine::Bearing; + + nb::class_(m, "Bearing") + .def(nb::init<>()) + .def("__init__", + [](Bearing *t, std::pair pair) + { new (t) Bearing{pair.first, pair.second}; }) + .def_rw("bearing", &Bearing::bearing) + .def_rw("range", &Bearing::range) + .def("IsValid", &Bearing::IsValid) + .def(nb::self == nb::self) + .def(nb::self != nb::self); + nb::implicitly_convertible, Bearing>(); +} diff --git a/src/python/src/types/coordinate_nb.cpp b/src/python/src/types/coordinate_nb.cpp new file mode 100644 index 0000000000..36d5e45af3 --- /dev/null +++ b/src/python/src/types/coordinate_nb.cpp @@ -0,0 +1,55 @@ +#include "python/types/coordinate_nb.hpp" +#include "util/coordinate.hpp" + +#include +#include +#include +#include + +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void init_Coordinate(nb::module_ &m) +{ + namespace tag = osrm::util::tag; + using FloatLongitude = osrm::Alias; + using FloatLatitude = osrm::Alias; + + using osrm::util::Coordinate; + + nb::class_(m, "Coordinate") + .def(nb::init<>()) + .def(nb::init(), "coordinate"_a) + .def("__init__", + [](Coordinate *t, std::pair coords) + { + const FloatLongitude lon_ = FloatLongitude{coords.first}; + const FloatLatitude lat_ = FloatLatitude{coords.second}; + + new (t) Coordinate(lon_, lat_); + }) + .def_prop_rw( + "lon", + [](const Coordinate &c) + { return static_cast(static_cast(c.lon)) / osrm::COORDINATE_PRECISION; }, + [](Coordinate &c, double val) { c.lon = osrm::util::toFixed(FloatLongitude{val}); }) + .def_prop_rw( + "lat", + [](const Coordinate &c) + { return static_cast(static_cast(c.lat)) / osrm::COORDINATE_PRECISION; }, + [](Coordinate &c, double val) { c.lat = osrm::util::toFixed(FloatLatitude{val}); }) + .def("IsValid", &Coordinate::IsValid) + .def("__repr__", + [](const Coordinate &coord) + { + int lon = static_cast(coord.lon); + int lat = static_cast(coord.lat); + + return '(' + std::to_string(lon) + ',' + std::to_string(lat) + ')'; + }) + .def(nb::self == nb::self) + .def(nb::self != nb::self); + nb::implicitly_convertible, Coordinate>(); +} diff --git a/src/python/src/types/jsoncontainer_nb.cpp b/src/python/src/types/jsoncontainer_nb.cpp new file mode 100644 index 0000000000..a61aa8c1a8 --- /dev/null +++ b/src/python/src/types/jsoncontainer_nb.cpp @@ -0,0 +1,65 @@ +#include "python/types/jsoncontainer_nb.hpp" +#include "util/json_container.hpp" + +#include +#include +#include +#include + +namespace nb = nanobind; +namespace json = osrm::util::json; + +void init_JSONContainer(nb::module_ &m) +{ + nb::class_(m, "Object") + .def(nb::init<>()) + .def("__len__", [](const json::Object &obj) { return obj.values.size(); }) + .def("__bool__", [](const json::Object &obj) { return !obj.values.empty(); }) + .def("__repr__", + [](const json::Object &obj) + { + ValueStringifyVisitor visitor; + return visitor.visitobject(obj); + }) + .def("__getitem__", + [](json::Object &obj, const std::string &key) -> nb::object + { return nb::cast(obj.values.at(key)); }) + .def("__contains__", + [](const json::Object &obj, const std::string &key) + { return obj.values.count(key) > 0; }) + .def( + "__iter__", + [m](const json::Object &obj) { + return nb::make_key_iterator( + m, "key_iterator", obj.values.begin(), obj.values.end()); + }, + nb::keep_alive<0, 1>()); + + nb::class_(m, "Array") + .def(nb::init<>()) + .def("__len__", [](const json::Array &arr) { return arr.values.size(); }) + .def("__bool__", [](const json::Array &arr) { return !arr.values.empty(); }) + .def("__repr__", + [](const json::Array &arr) + { + ValueStringifyVisitor visitor; + return visitor.visitarray(arr); + }) + .def("__getitem__", + [](json::Array &arr, int i) -> nb::object { return nb::cast(arr.values[i]); }) + .def("__iter__", + [](const json::Array &arr) + { + nb::list items; + for (const auto &v : arr.values) + { + items.append(nb::cast(v)); + } + return nb::iter(items); + }); + + nb::class_(m, "String").def(nb::init()); + nb::class_(m, "Number").def(nb::init()); + + // Not exposed: json::True, json::False, json::Null — they shadow Python builtins +} diff --git a/src/python/src/types/optional_nb.cpp b/src/python/src/types/optional_nb.cpp new file mode 100644 index 0000000000..f0f5f40b6d --- /dev/null +++ b/src/python/src/types/optional_nb.cpp @@ -0,0 +1,8 @@ +#include "python/types/optional_nb.hpp" + +#include +#include + +namespace nb = nanobind; + +void init_Optional(nb::module_ &) {} diff --git a/src/python/src/utility/osrm_utility.cpp b/src/python/src/utility/osrm_utility.cpp new file mode 100644 index 0000000000..9bdbb2555d --- /dev/null +++ b/src/python/src/utility/osrm_utility.cpp @@ -0,0 +1,142 @@ +#include "python/utility/osrm_utility.hpp" +#include "python/engineconfig_nb.hpp" +#include "python/utility/param_utility.hpp" +#include "engine/engine_config.hpp" +#include "engine/status.hpp" +#include "osrm/osrm.hpp" + +#include +#include + +#include +#include + +#define UNLIMITED -1 + +namespace nb = nanobind; + +using osrm::engine::EngineConfig; + +template void assign_val(T &to_assign, const std::pair &val) +{ + try + { + to_assign = nb::cast(val.second); + } + catch (const nb::cast_error &ex) + { + throw std::runtime_error("Invalid type passed for argument: " + + nb::cast(val.first)); + } +} + +namespace osrm_nb_util +{ + +void check_status(osrm::engine::Status status, osrm::util::json::Object &res) +{ + if (status == osrm::engine::Status::Ok) + { + return; + } + + const std::string code = std::get(res.values.at("code")).value; + const std::string msg = std::get(res.values.at("message")).value; + + throw std::runtime_error(code + " - " + msg); +} + +void populate_cfg_from_kwargs(const nb::kwargs &kwargs, EngineConfig &config) +{ + std::unordered_map &)>> + assign_map{{"storage_config", + [&config](const std::pair &val) + { + std::string str; + assign_val(str, val); + config.storage_config = osrm::storage::StorageConfig(str); + }}, + {"max_locations_trip", + [&config](const std::pair &val) + { assign_val(config.max_locations_trip, val); }}, + {"max_locations_viaroute", + [&config](const std::pair &val) + { assign_val(config.max_locations_viaroute, val); }}, + {"max_locations_distance_table", + [&config](const std::pair &val) + { assign_val(config.max_locations_distance_table, val); }}, + {"max_locations_map_matching", + [&config](const std::pair &val) + { assign_val(config.max_locations_map_matching, val); }}, + {"max_radius_map_matching", + [&config](const std::pair &val) + { assign_val(config.max_radius_map_matching, val); }}, + {"max_results_nearest", + [&config](const std::pair &val) + { assign_val(config.max_results_nearest, val); }}, + {"default_radius", + [&config](const std::pair &val) + { + try + { + const std::string rad_val = nb::cast(val.second); + + if (!(rad_val == "unlimited" || rad_val == "UNLIMITED")) + { + throw std::runtime_error( + "default_radius must be a float value or 'unlimited'"); + } + + config.default_radius = UNLIMITED; + } + catch (const nb::cast_error &) + { + assign_val(config.default_radius, val); + } + }}, + {"max_alternatives", + [&config](const std::pair &val) + { assign_val(config.max_alternatives, val); }}, + {"use_shared_memory", + [&config](const std::pair &val) + { assign_val(config.use_shared_memory, val); }}, + {"memory_file", + [&config](const std::pair &val) + { + std::string str; + assign_val(str, val); + config.memory_file = std::filesystem::path(str); + }}, + {"use_mmap", + [&config](const std::pair &val) + { assign_val(config.use_mmap, val); }}, + {"algorithm", + [&config](const std::pair &val) + { + std::string str; + assign_val(str, val); + config.algorithm = + osrm_nb_util::str_to_enum(str, "Algorithm", algorithm_map); + }}, + {"verbosity", + [&config](const std::pair &val) + { assign_val(config.verbosity, val); }}, + {"dataset_name", [&config](const std::pair &val) { + assign_val(config.dataset_name, val); + }}}; + + for (auto kwarg : kwargs) + { + const std::string arg_str = nb::cast(kwarg.first); + auto itr = assign_map.find(arg_str); + + if (itr == assign_map.end()) + { + throw std::invalid_argument(arg_str); + } + + itr->second(kwarg); + } +} + +} // namespace osrm_nb_util diff --git a/src/python/src/utility/param_utility.cpp b/src/python/src/utility/param_utility.cpp new file mode 100644 index 0000000000..0b950cee26 --- /dev/null +++ b/src/python/src/utility/param_utility.cpp @@ -0,0 +1,107 @@ +#include "python/utility/param_utility.hpp" +#include "engine/api/base_parameters.hpp" +#include "engine/api/match_parameters.hpp" +#include "engine/api/route_parameters.hpp" +#include "engine/api/table_parameters.hpp" +#include "engine/api/trip_parameters.hpp" +#include "engine/approach.hpp" +#include "engine/hint.hpp" + +#include +#include +#include +#include + +using osrm::engine::Approach; +using osrm::engine::api::BaseParameters; +using osrm::engine::api::MatchParameters; +using osrm::engine::api::RouteParameters; +using osrm::engine::api::TableParameters; +using osrm::engine::api::TripParameters; + +namespace osrm_nb_util +{ + +void assign_baseparameters(BaseParameters *params, + std::vector coordinates, + std::vector> hints, + std::vector> radiuses, + std::vector> bearings, + const std::vector> &approaches, + bool generate_hints, + std::vector exclude, + const BaseParameters::SnappingType snapping) +{ + params->coordinates = std::move(coordinates); + params->hints.clear(); + for (const auto &h : hints) + { + if (h) + { + params->hints.push_back(osrm::engine::Hint::FromBase64(*h)); + } + else + { + params->hints.push_back(std::nullopt); + } + } + params->radiuses = std::move(radiuses); + params->bearings = std::move(bearings); + params->approaches = approaches; + params->generate_hints = generate_hints; + params->exclude = std::move(exclude); + params->snapping = snapping; +} + +void assign_routeparameters(RouteParameters *params, + const bool steps, + int number_of_alternatives, + const std::vector &annotations, + RouteParameters::GeometriesType geometries, + RouteParameters::OverviewType overview, + const std::optional continue_straight, + std::vector waypoints) +{ + params->steps = steps; + params->alternatives = (bool)number_of_alternatives; + params->number_of_alternatives = number_of_alternatives; + params->annotations = !annotations.empty(); + params->annotations_type = calculate_routeannotations_type(annotations); + params->geometries = geometries; + params->overview = overview; + params->continue_straight = continue_straight; + params->waypoints = std::move(waypoints); +} + +RouteParameters::AnnotationsType +calculate_routeannotations_type(const std::vector &annotations) +{ + RouteParameters::AnnotationsType res = RouteParameters::AnnotationsType::None; + + for (size_t i = 0; i < annotations.size(); ++i) + { + res = res | annotations[i]; + } + + return res; +} + +TableParameters::AnnotationsType +calculate_tableannotations_type(const std::vector &annotations) +{ + if (annotations.empty()) + { + return TableParameters::AnnotationsType::Duration; + } + + TableParameters::AnnotationsType res = TableParameters::AnnotationsType::None; + + for (size_t i = 0; i < annotations.size(); ++i) + { + res = res | annotations[i]; + } + + return res; +} + +} // namespace osrm_nb_util diff --git a/test/data/windows-build-test-data.bat b/test/data/windows-build-test-data.bat new file mode 100644 index 0000000000..1c14d23f4f --- /dev/null +++ b/test/data/windows-build-test-data.bat @@ -0,0 +1,44 @@ +@ECHO OFF +SETLOCAL EnableDelayedExpansion + +SET DATA_DIR=%CD% + +SET test_region=monaco +SET test_region_ch=ch\monaco +SET test_region_mld=mld\monaco +SET test_osm=%test_region%.osm.pbf + +SET CMD=python -m osrm extract -p %DATA_DIR%\..\..\profiles\car.lua %DATA_DIR%\monaco.osm.pbf +%CMD% +IF !ERRORLEVEL! NEQ 0 (SET EL=!ERRORLEVEL! & GOTO ERROR) + +MKDIR ch +XCOPY %test_region%.osrm.* ch\ +XCOPY %test_region%.osrm ch\ +MKDIR mld +XCOPY %test_region%.osrm.* mld\ +XCOPY %test_region%.osrm mld\ + +SET CMD=python -m osrm contract %test_region_ch%.osrm +%CMD% +IF !ERRORLEVEL! NEQ 0 (SET EL=!ERRORLEVEL! & GOTO ERROR) + +SET CMD=python -m osrm partition %test_region_mld%.osrm +%CMD% +IF !ERRORLEVEL! NEQ 0 (SET EL=!ERRORLEVEL! & GOTO ERROR) + +SET CMD=python -m osrm customize %test_region_mld%.osrm +%CMD% +IF !ERRORLEVEL! NEQ 0 (SET EL=!ERRORLEVEL! & GOTO ERROR) + +GOTO DONE + +:ERROR +ECHO ~~~~~~~~~~~~~~~~~~~~~~ ERROR %~f0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +ECHO Failed command: %CMD% +ECHO Exit code: %EL% + +:DONE +ECHO ~~~~~~~~~~~~~~~~~~~~~~ DONE %~f0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +EXIT /b %EL% diff --git a/test/python/constants.py b/test/python/constants.py new file mode 100755 index 0000000000..3173bf1e8c --- /dev/null +++ b/test/python/constants.py @@ -0,0 +1,18 @@ +from pathlib import Path + +path = Path(__file__).parent.parent.joinpath("data") + +# Constants and fixtures for Python tests on our Monaco dataset. + +# Somewhere in Monaco +# http://www.openstreetmap.org/#map=18/43.73185/7.41772 +three_test_coordinates = [(7.41337, 43.72956), (7.41546, 43.73077), (7.41862, 43.73216)] + +two_test_coordinates = three_test_coordinates[0:2] + +test_tile = {"at": [17059, 11948, 15], "size": 159125} + +data_path = str(path.joinpath("ch", "monaco.osrm").absolute()) +mld_data_path = str(path.joinpath("mld", "monaco.osrm").absolute()) +corech_data_path = str(path.joinpath("corech", "monaco.osrm").absolute()) +test_memory_path = str(path.joinpath("test_memory").absolute()) diff --git a/test/python/test_index.py b/test/python/test_index.py new file mode 100755 index 0000000000..1acb068855 --- /dev/null +++ b/test/python/test_index.py @@ -0,0 +1,118 @@ +import pytest +import osrm +import constants + +data_path = constants.data_path +mld_data_path = constants.mld_data_path +test_memory_path = constants.test_memory_path + + +class TestIndex: + def test_default_noparam(self): + osrm.OSRM() + + def test_throwifnecessarynotexist(self): + with pytest.raises(RuntimeError) as ex: + osrm.OSRM("missing.osrm") + assert str(ex.value) == "Required files are missing" + + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(storage_config="missing.osrm", algorithm="MLD") + assert str(ex.value) == "Config Parameters are Invalid" + + def test_shmemarg(self): + osrm.OSRM(storage_config=data_path, use_shared_memory=False) + + def test_memfile(self): + # memory_file is deprecated in OSRM v6 (equivalent to use_mmap=True); no datastore needed + osrm.OSRM( + storage_config=data_path, + use_shared_memory=False, + memory_file=test_memory_path, + ) + + def test_shmemfalsenopath(self): + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(use_shared_memory=False) + assert str(ex.value) == "Config Parameters are Invalid" + + def test_nonstringarg(self): + with pytest.raises(TypeError): + osrm.OSRM(True) + + def test_unknownalgo(self): + with pytest.raises(ValueError) as ex: + osrm.OSRM(algorithm="Foo") + ex_str = str(ex.value) + assert "Invalid Algorithm: 'Foo'" in ex_str + assert "'CH'" in ex_str + assert "'MLD'" in ex_str + + def test_invalidalgo(self): + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(algorithm=3) + assert str(ex.value) == "Invalid type passed for argument: algorithm" + + def test_validalgos(self): + osrm.OSRM(algorithm="MLD", storage_config=mld_data_path, use_shared_memory=False) + + osrm.OSRM(algorithm="CH", storage_config=data_path, use_shared_memory=False) + + def test_datamatchalgo(self): + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(algorithm="CH", storage_config=mld_data_path, use_shared_memory=False) + assert "Could not find any metrics for CH in the data." in str(ex.value) + + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(algorithm="MLD", storage_config=data_path, use_shared_memory=False) + assert "Could not find any metrics for MLD in the data." in str(ex.value) + + def test_datasetnamenotstring(self): + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(dataset_name=1337) + assert str(ex.value) == "Invalid type passed for argument: dataset_name" + + # osrm.OSRM(dataset_name = "") requires osrm-datastore (uses shared memory) + + with pytest.raises(RuntimeError) as ex: + osrm.OSRM(dataset_name="unsued_name___", use_shared_memory=True) + assert "shared memory" in str(ex.value).lower() or "osrm-datastore" in str(ex.value) + + def test_defaultradius(self): + osrm.OSRM(storage_config=data_path, use_shared_memory=False, default_radius=1) + + def test_unlimitedradius(self): + osrm.OSRM( + storage_config=data_path, + use_shared_memory=False, + default_radius="unlimited", + ) + + def test_customlimits(self): + osrm.OSRM( + storage_config=mld_data_path, + algorithm="MLD", + use_shared_memory=False, + max_locations_trip=3, + max_locations_viaroute=3, + max_locations_distance_table=3, + max_locations_map_matching=3, + max_results_nearest=1, + max_alternatives=1, + default_radius=1, + ) + + def test_invalidlimits(self): + with pytest.raises(RuntimeError) as ex: + osrm.OSRM( + storage_config=mld_data_path, + algorithm="MLD", + max_locations_trip=1, + max_locations_viaroute=True, + max_locations_distance_table=False, + max_locations_map_matching="a lot", + max_results_nearest=None, + max_alternatives="10", + default_radius="10", + ) + assert "Invalid type passed for argument" in str(ex.value) diff --git a/test/python/test_match.py b/test/python/test_match.py new file mode 100755 index 0000000000..41f5941050 --- /dev/null +++ b/test/python/test_match.py @@ -0,0 +1,244 @@ +import sys +import pytest +import osrm +import constants +import math + +data_path = constants.data_path +mld_data_path = constants.mld_data_path +three_test_coordinates = constants.three_test_coordinates +two_test_coordinates = constants.two_test_coordinates + + +@pytest.mark.skipif( + sys.platform == "win32", reason="Map matching segfaults on Windows (STATUS_ACCESS_VIOLATION)" +) +class TestMatch: + osrm_py = osrm.OSRM(storage_config=data_path, use_shared_memory=False) + + def test_match(self): + match_params = osrm.MatchParameters( + coordinates=three_test_coordinates, + timestamps=[1424684612, 1424684616, 1424684620], + ) + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + for m in res["matchings"]: + assert ( + m["distance"] + and m["duration"] + and isinstance(m["legs"], osrm.Array) + and m["geometry"] + and m["confidence"] > 0 + ) + assert len(res["tracepoints"]) == 3 + for t in res["tracepoints"]: + assert ( + t["hint"] + and not math.isnan(t["matchings_index"]) + and not math.isnan(t["waypoint_index"]) + and t["name"] + ) + + def test_match_no_timestamps(self): + match_params = osrm.MatchParameters( + coordinates=three_test_coordinates, + ) + res = self.osrm_py.Match(match_params) + assert len(res["tracepoints"]) == 3 + assert len(res["matchings"]) == 1 + + def test_match_no_geometrycompression(self): + match_params = osrm.MatchParameters(coordinates=three_test_coordinates, geometries="geojson") + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + assert isinstance(res["matchings"][0]["geometry"], osrm.Object) + assert isinstance(res["matchings"][0]["geometry"]["coordinates"], osrm.Array) + + def test_match_geometrycompression(self): + match_params = osrm.MatchParameters( + coordinates=three_test_coordinates, + ) + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + assert isinstance(res["matchings"][0]["geometry"], str) + + def test_match_speedannotations(self): + match_params = osrm.MatchParameters( + coordinates=three_test_coordinates, + timestamps=[1424684612, 1424684616, 1424684620], + radiuses=[4.07, 4.07, 4.07], + steps=True, + annotations=["speed"], + overview="false", + geometries="geojson", + ) + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + assert res["matchings"][0]["confidence"] > 0 + for l in res["matchings"][0]["legs"]: + assert len(l["steps"]) > 0 and l["annotation"] and l["annotation"]["speed"] + for l in res["matchings"][0]["legs"]: + assert ( + "weight" not in l["annotation"] + and "datasources" not in l["annotation"] + and "duration" not in l["annotation"] + and "distance" not in l["annotation"] + and "nodes" not in l["annotation"] + ) + assert "geometry" not in res["matchings"][0] + + def test_match_severalannotations(self): + match_params = osrm.MatchParameters( + coordinates=three_test_coordinates, + timestamps=[1424684612, 1424684616, 1424684620], + radiuses=[4.07, 4.07, 4.07], + steps=True, + annotations=["duration", "distance", "nodes"], + overview="false", + geometries="geojson", + ) + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + assert res["matchings"][0]["confidence"] > 0 + for l in res["matchings"][0]["legs"]: + assert ( + len(l["steps"]) > 0 + and l["annotation"] + and l["annotation"]["distance"] is not None + and l["annotation"]["duration"] is not None + and l["annotation"]["nodes"] is not None + ) + assert ( + "weight" not in l["annotation"] + and "datasources" not in l["annotation"] + and "speed" not in l["annotation"] + ) + assert "geometry" not in res["matchings"][0] + + def test_match_alloptions(self): + match_params = osrm.MatchParameters( + coordinates=three_test_coordinates, + timestamps=[1424684612, 1424684616, 1424684620], + radiuses=[4.07, 4.07, 4.07], + steps=True, + annotations=["all"], + overview="false", + geometries="geojson", + gaps="split", + tidy=False, + ) + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + assert res["matchings"][0]["confidence"] > 0 + for l in res["matchings"][0]["legs"]: + assert ( + len(l["steps"]) > 0 + and l["annotation"] + and l["annotation"]["distance"] is not None + and l["annotation"]["duration"] is not None + ) + assert "geometry" not in res["matchings"][0] + + def test_match_missing_arg(self): + with pytest.raises(Exception): + self.osrm_py.Match(osrm.MatchParameters()) + + def test_match_nonobj_arg(self): + with pytest.raises(TypeError): + osrm.MatchParameters(None) + + def test_match_invalidcoords(self): + match_params = osrm.MatchParameters(coordinates=[]) + with pytest.raises(Exception): + self.osrm_py.Match(match_params) + with pytest.raises(Exception): + match_params.coordinates = [three_test_coordinates[0]] + self.osrm_py.Match(match_params) + with pytest.raises(TypeError): + match_params.coordinates = three_test_coordinates[0] + with pytest.raises(TypeError): + match_params.coordinates = [ + three_test_coordinates[0][0], + three_test_coordinates[0][1], + ] + + def test_match_invalidtimestamps(self): + match_params = osrm.MatchParameters(coordinates=three_test_coordinates) + with pytest.raises(Exception): + match_params.timestamps = [1424684612, 1424684616] + self.osrm_py.Match(match_params) + + def test_match_without_motorways(self): + osrm_py = osrm.OSRM(storage_config=mld_data_path, algorithm="MLD", use_shared_memory=False) + match_params = osrm.MatchParameters(coordinates=three_test_coordinates, exclude=["motorway"]) + res = osrm_py.Match(match_params) + assert len(res["tracepoints"]) == 3 + assert len(res["matchings"]) == 1 + + # TODO: Would require custom validation bindings side + # def test_match_invalidwaypoints_needtwo(self): + # match_params = osrm.MatchParameters( + # steps = True, + # coordinates = three_test_coordinates, + # waypoints = [0] + # ) + # with pytest.raises(Exception): + # self.osrm_py.Match(match_params) + + # TODO: Would require custom validation bindings side + # def test_match_invalidwaypoints_needcoordindices(self): + # match_params = osrm.MatchParameters( + # steps = True, + # coordinates = three_test_coordinates, + # waypoints = [1, 2] + # ) + # with pytest.raises(Exception): + # self.osrm_py.Match(match_params) + + # TODO: Would require custom validation bindings side + # def test_match_invalidwaypoints_ordermatters(self): + # match_params = osrm.MatchParameters( + # steps = True, + # coordinates = three_test_coordinates, + # waypoints = [2, 0] + # ) + # with pytest.raises(Exception): + # self.osrm_py.Match(match_params) + + def test_match_invalidwaypoints_mustcorrespond(self): + match_params = osrm.MatchParameters( + steps=True, coordinates=three_test_coordinates, waypoints=[0, 3, 2] + ) + with pytest.raises(Exception): + self.osrm_py.Match(match_params) + + def test_match_error_on_splittrace(self): + match_params = osrm.MatchParameters( + steps=True, + coordinates=three_test_coordinates + [(7.41902, 43.73487)], + timestamps=[1700, 1750, 1424684616, 1424684620], + waypoints=[0, 3], + ) + with pytest.raises(RuntimeError) as ex: + self.osrm_py.Match(match_params) + assert "NoMatch" in str(ex.value) + + def test_match_waypoints(self): + match_params = osrm.MatchParameters( + steps=True, coordinates=three_test_coordinates, waypoints=[0, 2] + ) + res = self.osrm_py.Match(match_params) + assert len(res["matchings"]) == 1 + assert len(res["matchings"][0]["legs"]) == 1 + for m in res["matchings"]: + assert ( + m["distance"] + and m["duration"] + and isinstance(m["legs"], osrm.Array) + and m["geometry"] + and (m["confidence"] > 0) + ) + assert len(res["tracepoints"]) == 3 + for t in res["tracepoints"]: + assert t["hint"] and not math.isnan(t["matchings_index"]) and t["name"] diff --git a/test/python/test_nearest.py b/test/python/test_nearest.py new file mode 100755 index 0000000000..1675819f7c --- /dev/null +++ b/test/python/test_nearest.py @@ -0,0 +1,16 @@ +import osrm +import constants + +mld_data_path = constants.mld_data_path +two_test_coordinates = constants.two_test_coordinates + + +class TestNearest: + osrm_py = osrm.OSRM(storage_config=mld_data_path, algorithm="MLD", use_shared_memory=False) + + def test_nearest(self): + nearest_params = osrm.NearestParameters( + coordinates=[two_test_coordinates[0]], exclude=["motorway"] + ) + res = self.osrm_py.Nearest(nearest_params) + assert len(res["waypoints"]) == 1 diff --git a/test/python/test_route.py b/test/python/test_route.py new file mode 100755 index 0000000000..eaffc98104 --- /dev/null +++ b/test/python/test_route.py @@ -0,0 +1,287 @@ +import pytest +import osrm +import constants + +data_path = constants.data_path +mld_data_path = constants.mld_data_path +three_test_coordinates = constants.three_test_coordinates +two_test_coordinates = constants.two_test_coordinates + + +class TestRoute: + osrm_py = osrm.OSRM(storage_config=data_path, use_shared_memory=False) + + def test_route(self): + route_params = osrm.RouteParameters(coordinates=two_test_coordinates) + res = self.osrm_py.Route(route_params) + assert res["waypoints"] + assert res["routes"] + assert res["routes"][0]["geometry"] + + def test_route_mld(self): + engine = osrm.OSRM(algorithm="MLD", storage_config=mld_data_path, use_shared_memory=False) + route_params = osrm.RouteParameters(coordinates=[(13.43864, 52.51993), (13.415852, 52.513191)]) + res = engine.Route(route_params) + assert res["waypoints"] + assert res["routes"] + assert res["routes"][0]["geometry"] + + def test_route_alternatives(self): + route_params = osrm.RouteParameters(coordinates=two_test_coordinates) + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) == 1 + + route_params.alternatives = True + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) >= 1 + + route_params.number_of_alternatives = 3 + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) >= 1 + + def test_route_badparams(self): + route_params = osrm.RouteParameters(coordinates=[]) + with pytest.raises(Exception): + self.osrm_py.Route(route_params) + with pytest.raises(Exception): + route_params.coordinates = None + self.osrm_py.Route(route_params) + with pytest.raises(Exception): + route_params.coordinates = [[three_test_coordinates[0], three_test_coordinates[1]]] + self.osrm_py.Route(route_params) + with pytest.raises(Exception): + route_params.coordinates = [ + (213.43864, 252.51993), + (413.415852, 552.513191), + ] + self.osrm_py.Route(route_params) + + def test_route_shmem(self): + # Use file-mode OSRM (shared memory requires osrm-datastore) + route_params = osrm.RouteParameters(coordinates=two_test_coordinates) + res = self.osrm_py.Route(route_params) + assert isinstance(res["routes"][0]["geometry"], str) + + def test_route_geometrycompression(self): + route_params = osrm.RouteParameters(coordinates=two_test_coordinates, geometries="geojson") + res = self.osrm_py.Route(route_params) + assert isinstance(res["routes"][0]["geometry"]["coordinates"], osrm.Array) + assert res["routes"][0]["geometry"]["type"] == "LineString" + + def test_route_polyline6(self): + route_params = osrm.RouteParameters( + coordinates=two_test_coordinates, + continue_straight=False, + overview="false", + geometries="polyline6", + steps=True, + ) + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) == 1 + assert "geometry" not in res["routes"][0] + assert res["routes"][0]["legs"][0] + assert isinstance(res["routes"][0]["legs"][0]["steps"][0]["geometry"], str) + + def test_route_speedannotations(self): + route_params = osrm.RouteParameters( + coordinates=two_test_coordinates, + continue_straight=False, + overview="false", + geometries="polyline", + steps=True, + annotations=["speed"], + ) + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) == 1 + assert "geometry" not in res["routes"][0] + assert res["routes"][0]["legs"][0] + for l in res["routes"][0]["legs"]: + assert len(l["steps"]) > 0 and l["annotation"] and l["annotation"]["speed"] + assert ( + "weight" not in l["annotation"] + and "datasources" not in l["annotation"] + and "duration" not in l["annotation"] + and "distance" not in l["annotation"] + and "nodes" not in l["annotation"] + ) + + route_params.overview = "full" + full_res = self.osrm_py.Route(route_params) + + route_params.overview = "simplified" + simplified_res = self.osrm_py.Route(route_params) + + assert full_res["routes"][0]["geometry"] != simplified_res["routes"][0]["geometry"] + + def test_route_severalannotations(self): + route_params = osrm.RouteParameters( + coordinates=two_test_coordinates, + continue_straight=False, + overview="false", + geometries="polyline", + steps=True, + annotations=["duration", "distance", "nodes"], + ) + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) == 1 + assert "geometry" not in res["routes"][0] + assert res["routes"][0]["legs"][0] + for l in res["routes"][0]["legs"]: + assert len(l["steps"]) > 0 + assert ( + l["annotation"] + and l["annotation"]["distance"] + and l["annotation"]["duration"] + and l["annotation"]["nodes"] + ) + assert ( + "weight" not in l["annotation"] + and "datasources" not in l["annotation"] + and "speed" not in l["annotation"] + ) + + route_params.overview = "full" + full_res = self.osrm_py.Route(route_params) + + route_params.overview = "simplified" + simplified_res = self.osrm_py.Route(route_params) + + assert full_res["routes"][0]["geometry"] != simplified_res["routes"][0]["geometry"] + + def test_route_options(self): + route_params = osrm.RouteParameters( + coordinates=two_test_coordinates, + continue_straight=False, + overview="false", + geometries="polyline", + steps=True, + annotations=["all"], + ) + res = self.osrm_py.Route(route_params) + assert res["routes"] + assert len(res["routes"]) == 1 + assert "geometry" not in res["routes"][0] + assert res["routes"][0]["legs"][0] + for l in res["routes"][0]["legs"]: + assert len(l["steps"]) > 0 + assert ( + l["annotation"] + and l["annotation"]["distance"] + and l["annotation"]["duration"] + and l["annotation"]["nodes"] + and l["annotation"]["weight"] + and l["annotation"]["datasources"] + and l["annotation"]["speed"] + ) + + route_params.overview = "full" + full_res = self.osrm_py.Route(route_params) + + route_params.overview = "simplified" + simplified_res = self.osrm_py.Route(route_params) + + assert full_res["routes"][0]["geometry"] != simplified_res["routes"][0]["geometry"] + + # def test_route_validbearings(self): + # route_params = osrm.RouteParameters( + # coordinates = two_test_coordinates, + # bearings = [(200, 180), (250, 180)] + # ) + # res = self.osrm_py.Route(route_params) + # + # assert(res["routes"][0]) + + # route_params.bearings = [None, (200, 180)] + # res = self.osrm_py.Route(route_params) + # + # assert(res["routes"][0]) + + # def test_route_validradius(self): + # route_params = osrm.RouteParameters( + # coordinates = two_test_coordinates, + # radiuses = [100, 100] + # ) + # res = self.osrm_py.Route(route_params) + # + + # route_params.radiuses = [None, None] + # res = self.osrm_py.Route(route_params) + # + + # route_params.radiuses = [100, None] + # res = self.osrm_py.Route(route_params) + # + + # def test_route_validapproaches(self): + # route_params = osrm.RouteParameters( + # coordinates = two_test_coordinates, + # approaches = [None, osrm.Approach.CURB] + # ) + # res = self.osrm_py.Route(route_params) + # + + # route_params.approaches = [osrm.Approach.UNRESTRICTED, None] + # res = self.osrm_py.Route(route_params) + # + + def test_route_customlimitsmld(self): + engine = osrm.OSRM( + algorithm="MLD", + storage_config=mld_data_path, + max_alternatives=10, + use_shared_memory=False, + ) + route_params = osrm.RouteParameters(coordinates=two_test_coordinates, number_of_alternatives=10) + res = engine.Route(route_params) + assert isinstance(res["routes"], osrm.Array) + + route_params = osrm.RouteParameters(coordinates=two_test_coordinates, number_of_alternatives=11) + with pytest.raises(RuntimeError) as ex: + res = engine.Route(route_params) + assert "TooBig" in str(ex.value) + + def test_route_nomotorways(self): + engine = osrm.OSRM(algorithm="MLD", storage_config=mld_data_path, use_shared_memory=False) + route_params = osrm.RouteParameters(coordinates=two_test_coordinates, exclude=["motorway"]) + res = engine.Route(route_params) + assert len(res["waypoints"]) == 2 + assert len(res["routes"]) == 1 + + def test_route_invalidwaypoints(self): + route_params = osrm.RouteParameters( + steps=True, coordinates=three_test_coordinates, waypoints=[0] + ) + with pytest.raises(RuntimeError) as ex: + self.osrm_py.Route(route_params) + assert "InvalidValue" in str(ex.value) + + route_params = osrm.RouteParameters( + steps=True, coordinates=three_test_coordinates, waypoints=[1, 2] + ) + with pytest.raises(RuntimeError) as ex: + self.osrm_py.Route(route_params) + assert "InvalidValue" in str(ex.value) + + route_params = osrm.RouteParameters( + steps=True, coordinates=three_test_coordinates, waypoints=[2, 0] + ) + with pytest.raises(RuntimeError) as ex: + self.osrm_py.Route(route_params) + assert "InvalidValue" in str(ex.value) + + def test_route_snapping(self): + route_params = osrm.RouteParameters( + coordinates=[ + (7.448205209414596, 43.754001097311544), + (7.447122039202185, 43.75306156811368), + ], + snapping="any", + ) + res = self.osrm_py.Route(route_params) + assert round(res["routes"][0]["distance"] * 10) == 1315 diff --git a/test/python/test_table.py b/test/python/test_table.py new file mode 100755 index 0000000000..f122756b0f --- /dev/null +++ b/test/python/test_table.py @@ -0,0 +1,141 @@ +import pytest +import osrm +import constants + +data_path = constants.data_path +mld_data_path = constants.mld_data_path +three_test_coordinates = constants.three_test_coordinates +two_test_coordinates = constants.two_test_coordinates + + +class TestTable: + osrm_py = osrm.OSRM(storage_config=data_path, use_shared_memory=False) + + def test_table_annotations(self): + table_params = osrm.TableParameters( + coordinates=[three_test_coordinates[0], three_test_coordinates[1]], + annotations=["distance"], + ) + res = self.osrm_py.Table(table_params) + assert res["distances"] + assert "durations" not in res + + table_params = osrm.TableParameters( + coordinates=[three_test_coordinates[0], three_test_coordinates[1]], + annotations=["duration"], + ) + res = self.osrm_py.Table(table_params) + assert res["durations"] + assert "distances" not in res + + table_params = osrm.TableParameters( + coordinates=[three_test_coordinates[0], three_test_coordinates[1]], + annotations=["duration", "distance"], + ) + res = self.osrm_py.Table(table_params) + assert res["durations"] + assert res["distances"] + + table_params = osrm.TableParameters( + coordinates=[three_test_coordinates[0], three_test_coordinates[1]] + ) + res = self.osrm_py.Table(table_params) + assert res["durations"] + assert "distances" not in res + + def test_table_snapping(self): + table_params = osrm.TableParameters( + coordinates=[three_test_coordinates[0], three_test_coordinates[1]], + snapping="any", + ) + res = self.osrm_py.Table(table_params) + assert res["durations"] + + # def test_table_annotation(self): + # tables = ["distance", "duration"] + + # for annotation in tables: + # table_params = osrm.TableParameters( + # coordinates = [three_test_coordinates[0], three_test_coordinates[1]], + # annotations = [annotation] + # ) + # res = self.osrm_py.Table(table_params) + # + # rows = res[annotation] + # for i, col in enumerate(res[annotation]): + # assert(len(rows) == len(col)) + # for j, row in enumerate(col): + # if(i == j): + # # check that diagonal is zero + # assert(col[j] == 0) + # else: + # # everything else is non-zero and finite + # assert(not col[j] == 0) + # assert(math.isfinite(col[j])) + # assert(len(table_params.coordinates) == len(rows)) + + # for annotation in tables: + # table_params = osrm.TableParameters( + # coordinates = [three_test_coordinates[0], three_test_coordinates[1]], + # sources = [0], + # destinations = [0,1], + # annotations = [annotation] + # ) + # res = self.osrm_py.Table(table_params) + # + # rows = res[annotation] + # for i, col in enumerate(res[annotation]): + # assert(len(rows) == len(col)) + # for j, row in enumerate(col): + # if(i == j): + # # check that diagonal is zero + # assert(col[j] == 0) + # else: + # # everything else is non-zero and finite + # assert(not col[j] == 0) + # assert(math.isfinite(col[j])) + # assert(len(table_params.sources) == len(rows)) + + def test_table_withoutwaypoints(self): + table_params = osrm.TableParameters(coordinates=two_test_coordinates, annotations=["duration"]) + table_params.skip_waypoints = True + res = self.osrm_py.Table(table_params) + assert "sources" not in res + assert "destinations" not in res + + def test_table_fallbackspeeds(self): + table_params = osrm.TableParameters( + coordinates=two_test_coordinates, + annotations=["duration"], + fallback_speed=1, + fallback_coordinate_type="input", + ) + res = self.osrm_py.Table(table_params) + assert len(res["destinations"]) == 2 + assert len(res["fallback_speed_cells"]) == 0 + + def test_table_invalidfallbackspeeds(self): + osrm_py = osrm.OSRM(storage_config=mld_data_path, algorithm="MLD", use_shared_memory=False) + table_params = osrm.TableParameters( + coordinates=two_test_coordinates, + annotations=["duration"], + fallback_speed=-1, + ) + with pytest.raises(RuntimeError) as ex: + osrm_py.Table(table_params) + assert str(ex.value) == "Invalid Table Parameters" + + table_params.fallback_speed = 10 + osrm_py.Table(table_params) + + def test_table_invalidscalefactor(self): + osrm_py = osrm.OSRM(storage_config=mld_data_path, algorithm="MLD", use_shared_memory=False) + table_params = osrm.TableParameters( + coordinates=two_test_coordinates, annotations=["duration"], scale_factor=-1 + ) + with pytest.raises(RuntimeError) as ex: + osrm_py.Table(table_params) + assert str(ex.value) == "Invalid Table Parameters" + + table_params.scale_factor = 1 + osrm_py.Table(table_params) diff --git a/test/python/test_tile.py b/test/python/test_tile.py new file mode 100755 index 0000000000..4498ef0e50 --- /dev/null +++ b/test/python/test_tile.py @@ -0,0 +1,26 @@ +import pytest +import osrm +import constants + +data_path = constants.data_path +mld_data_path = constants.mld_data_path +test_tile = constants.test_tile + + +class TestTile: + osrm_py = osrm.OSRM(storage_config=data_path, use_shared_memory=False) + + def test_tile(self): + tile_params = osrm.TileParameters(test_tile["at"]) + res = self.osrm_py.Tile(tile_params) + assert len(res) == test_tile["size"] + + def test_tile_preconditions(self): + with pytest.raises(Exception): + # Must be an array + tile_params = osrm.TileParameters(17059, 11948, -15) + with pytest.raises(Exception): + # Must be unsigned + tile_params = osrm.TileParameters([17059, 11948, -15]) + tile_params = osrm.TileParameters([17059, 11948, 15]) + self.osrm_py.Tile(tile_params) diff --git a/test/python/test_trip.py b/test/python/test_trip.py new file mode 100755 index 0000000000..149efdc7f8 --- /dev/null +++ b/test/python/test_trip.py @@ -0,0 +1,110 @@ +import osrm +import constants + +data_path = constants.data_path +mld_data_path = constants.mld_data_path +three_test_coordinates = constants.three_test_coordinates +two_test_coordinates = constants.two_test_coordinates + + +class TestTrip: + osrm_py = osrm.OSRM(storage_config=data_path, use_shared_memory=False) + + def test_trip_manylocations(self): + trip_parameters = osrm.TripParameters(coordinates=three_test_coordinates[0:5]) + res = self.osrm_py.Trip(trip_parameters) + for trip in res["trips"]: + assert trip["geometry"] + + def test_trip_invalidargs(self): + # Previously used osrm.OSRM() (shared memory); use file mode here + trip_parameters = osrm.TripParameters(coordinates=two_test_coordinates) + res = self.osrm_py.Trip(trip_parameters) + for trip in res["trips"]: + assert trip["geometry"] + + def test_trip_geometrycompression(self): + # Previously used osrm.OSRM() (shared memory); use file mode here + trip_parameters = osrm.TripParameters( + coordinates=[three_test_coordinates[0], three_test_coordinates[1]] + ) + res = self.osrm_py.Trip(trip_parameters) + for trip in res["trips"]: + assert isinstance(trip["geometry"], str) + + def test_trip_nogeometrycompression(self): + # Previously used osrm.OSRM() (shared memory); use file mode here + trip_parameters = osrm.TripParameters(coordinates=two_test_coordinates, geometries="geojson") + res = self.osrm_py.Trip(trip_parameters) + for trip in res["trips"]: + assert isinstance(trip["geometry"]["coordinates"], osrm.Array) + + def test_trip_speedannotations(self): + # Previously used osrm.OSRM() (shared memory); use file mode here + trip_parameters = osrm.TripParameters( + coordinates=two_test_coordinates, + steps=True, + annotations=["speed"], + overview="false", + ) + res = self.osrm_py.Trip(trip_parameters) + for trip in res["trips"]: + assert trip + for l in trip["legs"]: + assert len(l["steps"]) > 0 and l["annotation"] and l["annotation"]["speed"] + assert ( + "weight" not in l["annotation"] + and "datasources" not in l["annotation"] + and "duration" not in l["annotation"] + and "distance" not in l["annotation"] + and "nodes" not in l["annotation"] + ) + assert "geometry" not in l + + def test_trip_severalannotations(self): + trip_params = osrm.TripParameters( + coordinates=two_test_coordinates, + steps=True, + annotations=["duration", "distance", "nodes"], + overview="false", + ) + res = self.osrm_py.Trip(trip_params) + assert len(res["trips"]) == 1 + for trip in res["trips"]: + assert trip + for l in trip["legs"]: + assert len(l["steps"]) > 0 + assert ( + l["annotation"] + and l["annotation"]["distance"] + and l["annotation"]["duration"] + and l["annotation"]["nodes"] + ) + assert ( + "weight" not in l["annotation"] + and "datasources" not in l["annotation"] + and "speed" not in l["annotation"] + ) + assert "geometry" not in l + + def test_trip_options(self): + trip_params = osrm.TripParameters( + coordinates=two_test_coordinates, + steps=True, + annotations=["all"], + overview="false", + ) + res = self.osrm_py.Trip(trip_params) + assert len(res["trips"]) == 1 + for trip in res["trips"]: + assert trip + for l in trip["legs"]: + assert len(l["steps"]) > 0 and l["annotation"] + assert "geometry" not in trip + + def test_trip_nomotorways(self): + engine = osrm.OSRM(algorithm="MLD", storage_config=mld_data_path, use_shared_memory=False) + trip_params = osrm.TripParameters(coordinates=two_test_coordinates, exclude=["motorway"]) + res = engine.Trip(trip_params) + assert len(res["waypoints"]) == 2 + assert len(res["trips"]) == 1 From d4e62f9347777919c25453e0a2062549bb75f618 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Thu, 16 Apr 2026 23:32:22 +0200 Subject: [PATCH 02/50] add to agent.md --- AGENT.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/AGENT.md b/AGENT.md index dc4fa5113e..fa6ff0179c 100644 --- a/AGENT.md +++ b/AGENT.md @@ -93,6 +93,37 @@ npx cucumber-js -p home -p mld -p mmap ``` +## Python Bindings + +Python bindings live under `src/python/` using nanobind + scikit-build-core. + +### Layout + +- `src/python/CMakeLists.txt` — nanobind module definition, installs everything under `osrm/` namespace +- `src/python/osrm/` — Python package (`__init__.py`, `__main__.py`, `.pyi` stubs) +- `src/python/src/` — C++ nanobind source files (16 files) +- `src/python/include/python/` — C++ binding headers (`.hpp`) + +### Key details + +- `ENABLE_PYTHON_BINDINGS=ON` triggers `add_subdirectory(src/python)` in root CMakeLists.txt +- Links against the in-tree `osrm` target directly (no FetchContent) +- Binding headers use `#include "python/..."` prefix; OSRM headers use `engine/...` paths (not the `osrm/` forwarding headers), except `osrm/osrm.hpp` +- LTO disabled for `osrm_ext` target (nanobind `NB_MAKE_OPAQUE` + GCC LTO + `-Werror` = ODR violation) +- Wheel installs executables to `osrm/bin/`, profiles to `osrm/share/profiles/`; `wheel.exclude` filters out root CMake install artifacts (`lib/`, `include/`, `bin/`, `share/`) +- `__main__.py` finds executables: `osrm/bin/` (wheel) → `build/*/` (editable) → PATH +- Type stubs (`osrm_ext.pyi`) auto-generated by `nanobind_add_stub()`; rebuild + commit after C++ changes +- C++ formatted by project clang-format; Python by ruff (both via `.pre-commit-config.yaml`) + +### Building & testing + +```bash +pip install -e ".[dev]" # editable install +cd test/data && make # build test data +python -m osrm datastore test/data/ch/monaco +pytest test/python/ +``` + ## Contributing From 9fe055b8f9b0d086dece33d40a357c2f1696ea47 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Thu, 16 Apr 2026 23:38:44 +0200 Subject: [PATCH 03/50] trigger CI From 77de8bd8767f6f2b8f22a5d99f0305d13fcfed94 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Thu, 16 Apr 2026 23:41:35 +0200 Subject: [PATCH 04/50] try CI --- .../osrm-backend-docker.yml | 0 .../osrm-backend.yml | 0 .../release-monthly.yml | 0 .../stale.yml | 0 .github/workflows/python-bindings.yml | 84 +++++++++++++++++++ 5 files changed, 84 insertions(+) rename .github/{workflows => workflows-disabled}/osrm-backend-docker.yml (100%) rename .github/{workflows => workflows-disabled}/osrm-backend.yml (100%) rename .github/{workflows => workflows-disabled}/release-monthly.yml (100%) rename .github/{workflows => workflows-disabled}/stale.yml (100%) create mode 100644 .github/workflows/python-bindings.yml diff --git a/.github/workflows/osrm-backend-docker.yml b/.github/workflows-disabled/osrm-backend-docker.yml similarity index 100% rename from .github/workflows/osrm-backend-docker.yml rename to .github/workflows-disabled/osrm-backend-docker.yml diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows-disabled/osrm-backend.yml similarity index 100% rename from .github/workflows/osrm-backend.yml rename to .github/workflows-disabled/osrm-backend.yml diff --git a/.github/workflows/release-monthly.yml b/.github/workflows-disabled/release-monthly.yml similarity index 100% rename from .github/workflows/release-monthly.yml rename to .github/workflows-disabled/release-monthly.yml diff --git a/.github/workflows/stale.yml b/.github/workflows-disabled/stale.yml similarity index 100% rename from .github/workflows/stale.yml rename to .github/workflows-disabled/stale.yml diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml new file mode 100644 index 0000000000..fd21f1b313 --- /dev/null +++ b/.github/workflows/python-bindings.yml @@ -0,0 +1,84 @@ +name: Python Bindings + +on: + pull_request: + branches: + - master + paths-ignore: + - '*.md' + - 'docs/**' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Lint & Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - uses: pre-commit/action@v3.0.1 + + build_wheels: + needs: [lint] + name: Build - cp312, Linux x86_64 + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Restore ccache + uses: actions/cache/restore@v5 + with: + path: /tmp/ccache + key: ccache-python-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref }}-${{ github.run_id }} + restore-keys: | + ccache-python-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref }}- + ccache-python-${{ runner.os }}-${{ runner.arch }}- + + - name: Run cibuildwheel + uses: pypa/cibuildwheel@v3.4.0 + env: + CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /tmp/ccache:/ccache" + CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" + + - name: Save ccache + uses: actions/cache/save@v5 + if: always() + with: + path: /tmp/ccache + key: ccache-python-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref }}-${{ github.run_id }} + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-linux-x86_64 + path: wheelhouse/*.whl + + check_stubs: + name: Check stubs are up to date + needs: [build_wheels] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - uses: actions/download-artifact@v4 + with: + name: wheels-linux-x86_64 + path: wheelhouse + - name: Install wheel and regenerate stubs + run: | + pip install nanobind ruff + pip install wheelhouse/*.whl + python -m nanobind.stubgen -m osrm.osrm_ext -o src/python/osrm/osrm_ext.pyi + ruff format src/python/osrm/osrm_ext.pyi + - name: Check for differences + run: git diff --exit-code src/python/osrm/osrm_ext.pyi || (echo "::error::Stubs are out of date. Rebuild locally and commit the updated .pyi file." && exit 1) From a752d927a81921a71fdb42a18d91ca0d82fa40a7 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Thu, 16 Apr 2026 23:56:26 +0200 Subject: [PATCH 05/50] try windows --- .../workflows-disabled/release-monthly.yml | 76 +++++++++++++++++++ .github/workflows/python-bindings.yml | 67 ++++------------ 2 files changed, 91 insertions(+), 52 deletions(-) diff --git a/.github/workflows-disabled/release-monthly.yml b/.github/workflows-disabled/release-monthly.yml index a1cf369721..989f6f671e 100644 --- a/.github/workflows-disabled/release-monthly.yml +++ b/.github/workflows-disabled/release-monthly.yml @@ -232,3 +232,79 @@ jobs: - name: Publish to npm run: npm publish + build_python_wheels: + name: Python wheels - ${{ matrix.name }} + needs: [release] + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: true + matrix: + include: + - name: Linux x86_64 + runner: ubuntu-latest + - name: Linux aarch64 + runner: ubuntu-24.04-arm + - name: macOS arm64 + runner: macos-latest + - name: Windows amd64 + runner: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Restore ccache + if: runner.os != 'Windows' + uses: actions/cache/restore@v5 + with: + path: /tmp/ccache + key: ccache-python-${{ runner.os }}-${{ runner.arch }}-master + restore-keys: | + ccache-python-${{ runner.os }}-${{ runner.arch }}- + + - name: Restore Conan cache + id: conan-cache + if: runner.os == 'Windows' + uses: actions/cache/restore@v5 + with: + path: ~/.conan2 + key: v4-conan-${{ runner.os }}-${{ hashFiles('pyproject.toml') }} + restore-keys: | + v4-conan-${{ runner.os }}- + + - name: Run cibuildwheel + uses: pypa/cibuildwheel@v3.4.0 + env: + CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /tmp/ccache:/ccache" + CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" + CIBW_CONFIG_SETTINGS_MACOS: "cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache" + CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: python-wheels-${{ matrix.name }} + path: wheelhouse/*.whl + + publish_python: + name: Publish Python wheels to PyPI + needs: [build_python_wheels] + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - uses: actions/download-artifact@v4 + with: + pattern: python-wheels-* + path: dist + merge-multiple: true + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@v1.13.0 + with: + verbose: true + # repository-url: https://test.pypi.org/legacy/ + diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index fd21f1b313..d39558b30e 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -1,12 +1,9 @@ name: Python Bindings on: - pull_request: + push: branches: - - master - paths-ignore: - - '*.md' - - 'docs/**' + - nn-py-bindings workflow_dispatch: concurrency: @@ -14,71 +11,37 @@ concurrency: cancel-in-progress: true jobs: - lint: - name: Lint & Format - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: '3.12' - - uses: pre-commit/action@v3.0.1 - build_wheels: - needs: [lint] - name: Build - cp312, Linux x86_64 - runs-on: ubuntu-latest + name: Build - cp312, Windows amd64 + runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@v6 - - name: Restore ccache + - name: Restore Conan cache + id: conan-cache uses: actions/cache/restore@v5 with: - path: /tmp/ccache - key: ccache-python-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref }}-${{ github.run_id }} + path: ~/.conan2 + key: v4-conan-${{ runner.os }}-${{ hashFiles('pyproject.toml') }} restore-keys: | - ccache-python-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref }}- - ccache-python-${{ runner.os }}-${{ runner.arch }}- + v4-conan-${{ runner.os }}- - name: Run cibuildwheel uses: pypa/cibuildwheel@v3.4.0 env: - CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /tmp/ccache:/ccache" - CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" + CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" - - name: Save ccache + - name: Save Conan cache uses: actions/cache/save@v5 - if: always() + if: "!cancelled() && steps.conan-cache.outputs.cache-hit != 'true'" with: - path: /tmp/ccache - key: ccache-python-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref }}-${{ github.run_id }} + path: ~/.conan2 + key: v4-conan-${{ runner.os }}-${{ hashFiles('pyproject.toml') }} - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: wheels-linux-x86_64 + name: wheels-windows-amd64 path: wheelhouse/*.whl - - check_stubs: - name: Check stubs are up to date - needs: [build_wheels] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: '3.12' - - uses: actions/download-artifact@v4 - with: - name: wheels-linux-x86_64 - path: wheelhouse - - name: Install wheel and regenerate stubs - run: | - pip install nanobind ruff - pip install wheelhouse/*.whl - python -m nanobind.stubgen -m osrm.osrm_ext -o src/python/osrm/osrm_ext.pyi - ruff format src/python/osrm/osrm_ext.pyi - - name: Check for differences - run: git diff --exit-code src/python/osrm/osrm_ext.pyi || (echo "::error::Stubs are out of date. Rebuild locally and commit the updated .pyi file." && exit 1) From e16b5259448bad40a18f1b5838d79e011960cf8b Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Thu, 16 Apr 2026 23:59:26 +0200 Subject: [PATCH 06/50] trigger CI From 54ec9417abe1af2f8a6f80afb2517a102f13c55e Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 00:19:30 +0200 Subject: [PATCH 07/50] more ci --- .../workflows-disabled/release-monthly.yml | 310 ------------------ .github/workflows/python-bindings.yml | 45 ++- .github/workflows/release-monthly.yml | 56 ++++ README.md | 18 + 4 files changed, 115 insertions(+), 314 deletions(-) delete mode 100644 .github/workflows-disabled/release-monthly.yml create mode 100644 .github/workflows/release-monthly.yml diff --git a/.github/workflows-disabled/release-monthly.yml b/.github/workflows-disabled/release-monthly.yml deleted file mode 100644 index 989f6f671e..0000000000 --- a/.github/workflows-disabled/release-monthly.yml +++ /dev/null @@ -1,310 +0,0 @@ -name: Monthly Release - -on: - schedule: - # 1st of each month at 08:00 UTC - - cron: '0 8 1 * *' - workflow_dispatch: - inputs: - version_override: - description: 'Override version (semver format, e.g., 26.4.0). Leave empty to auto-calculate.' - required: false - type: string - branch: - description: 'Branch to release from (defaults to master)' - required: false - type: string - default: 'master' - -concurrency: - group: release-monthly-${{ github.ref }} - cancel-in-progress: false - -jobs: - release: - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - actions: write - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.branch || 'master' }} - fetch-depth: 0 - - - uses: actions/setup-node@v6 - with: - node-version: 24 - registry-url: 'https://registry.npmjs.org' - - - name: Validate inputs - run: | - if [ -n "${{ inputs.version_override }}" ]; then - # Validate version_override matches semver format (YYYY-2000).M.patchlevel - # Month must be 1-12, no leading zeros except single digit 0 for patchlevel - if ! echo "${{ inputs.version_override }}" | grep -E '^(0|[1-9][0-9]?)\.(1[0-2]|[1-9])\.(0|[1-9][0-9]*)$' > /dev/null; then - echo "Error: version_override must be in format (YYYY-2000).M.patchlevel with month 1-12 and no leading zeros (e.g., 26.4.0)" - exit 1 - fi - fi - - # Validate branch input (prevent shell injection) - BRANCH="${{ inputs.branch || 'master' }}" - if ! echo "$BRANCH" | grep -E '^[a-zA-Z0-9._/-]+$' > /dev/null; then - echo "Error: branch name contains invalid characters" - exit 1 - fi - - - name: Calculate version - id: version - run: | - if [ -n "${{ inputs.version_override }}" ]; then - VERSION="${{ inputs.version_override }}" - else - # Calculate semver version automatically - # Use (YYYY-2000).M.patchlevel format (no leading zeros for month/patch) - YEAR=$(date -u +%Y) - MONTH=$(date -u +%-m) - YEAR_OFFSET=$((YEAR - 2000)) - - # Find highest patchlevel for this month - LATEST_TAG=$(git tag -l "v${YEAR_OFFSET}.${MONTH}.*" --sort=-version:refname | head -1) - - if [ -z "$LATEST_TAG" ]; then - PATCHLEVEL=0 - else - # Extract patchlevel from tag (e.g., v26.4.5 -> 5) - PATCHLEVEL=${LATEST_TAG##*.} - PATCHLEVEL=$((PATCHLEVEL + 1)) - fi - - VERSION="${YEAR_OFFSET}.${MONTH}.${PATCHLEVEL}" - fi - - echo "version=${VERSION}" >> $GITHUB_OUTPUT - echo "tag=v${VERSION}" >> $GITHUB_OUTPUT - - - name: Update package.json and package-lock.json - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - node -e " - const fs = require('fs'); - const pkg = require('./package.json'); - const lock = require('./package-lock.json'); - const oldVersion = pkg.version; - - // Update both package.json and package-lock.json - pkg.version = process.env.VERSION; - lock.version = process.env.VERSION; - fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n'); - fs.writeFileSync('./package-lock.json', JSON.stringify(lock, null, 2) + '\n'); - console.log('Updated version from ' + oldVersion + ' to ' + process.env.VERSION); - " - - - name: Check for changes - id: check_changes - run: | - if git diff --quiet package.json package-lock.json; then - echo "No changes to package files (version unchanged)" - echo "has_changes=false" >> $GITHUB_OUTPUT - else - echo "Changes detected in package files" - echo "has_changes=true" >> $GITHUB_OUTPUT - fi - - - name: Commit version update - if: steps.check_changes.outputs.has_changes == 'true' - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add package.json package-lock.json - git commit -m "chore: bump version to ${{ steps.version.outputs.version }}" - - - name: Create git tag - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag -a "${{ steps.version.outputs.tag }}" -m "Release ${{ steps.version.outputs.version }}" - - - name: Push changes and tag - run: | - BRANCH="${{ inputs.branch || 'master' }}" - git push origin "$BRANCH" - git push origin "${{ steps.version.outputs.tag }}" - - - name: Trigger CI for tag - env: - GH_TOKEN: ${{ github.token }} - run: | - # GITHUB_TOKEN pushes don't trigger workflow runs (prevents loops). - # Explicitly dispatch CI on the tag so binaries get built and uploaded. - gh workflow run osrm-backend.yml --ref "${{ steps.version.outputs.tag }}" - echo "Dispatched osrm-backend.yml on ${{ steps.version.outputs.tag }}" - - - name: Generate changelog - id: changelog - run: | - CURRENT_TAG="${{ steps.version.outputs.tag }}" - - # Get the previous release tag - PREVIOUS_TAG=$(git tag -l 'v*' --sort=-version:refname --merged "$CURRENT_TAG" | head -2 | tail -1) - - if [ -z "$PREVIOUS_TAG" ] || [ "$PREVIOUS_TAG" = "$CURRENT_TAG" ]; then - # No previous release, or selection resolved to the current tag; show all commits - CHANGELOG=$(git log "$CURRENT_TAG" --pretty=format:"* %h - %s") - else - # Show commits since last release - CHANGELOG=$(git log "${PREVIOUS_TAG}..$CURRENT_TAG" --pretty=format:"* %h - %s") - fi - - # Output as multiline variable using a unique delimiter to avoid collisions - DELIMITER=$(cat /proc/sys/kernel/random/uuid) - echo "body<<$DELIMITER" >> $GITHUB_OUTPUT - echo "$CHANGELOG" >> $GITHUB_OUTPUT - echo "$DELIMITER" >> $GITHUB_OUTPUT - - - name: Create GitHub Release - uses: ncipollo/release-action@v1 - with: - tag: ${{ steps.version.outputs.tag }} - name: Release ${{ steps.version.outputs.version }} - body: ${{ steps.changelog.outputs.body }} - draft: false - prerelease: false - - - name: Wait for CI to complete - env: - GH_TOKEN: ${{ github.token }} - run: | - # Wait for tag CI to complete before publishing npm package - # The osrm-backend.yml workflow is triggered by the tag push - # and uploads prebuilt binaries to the release assets - TAG="${{ steps.version.outputs.tag }}" - TAG_SHA="$(git rev-list -n 1 "$TAG")" - - echo "Waiting for CI workflow to complete for tag $TAG (commit $TAG_SHA)" - - MAX_WAIT=3600 # 1 hour - ELAPSED=0 - POLL_INTERVAL=10 - - while [ "$ELAPSED" -lt "$MAX_WAIT" ]; do - # Query runs for the osrm-backend.yml workflow for this tag's commit - RUN_JSON="$(gh run list --workflow=osrm-backend.yml --limit 50 --json databaseId,status,conclusion,headSha,displayTitle 2>/dev/null || echo '[]')" - - # Find run matching this tag's commit SHA - MATCHING_RUN="$(echo "$RUN_JSON" | jq -r --arg sha "$TAG_SHA" '.[] | select(.headSha == $sha) | @json' | head -1)" - - if [ -n "$MATCHING_RUN" ] && [ "$MATCHING_RUN" != "null" ]; then - STATUS="$(echo "$MATCHING_RUN" | jq -r '.status')" - CONCLUSION="$(echo "$MATCHING_RUN" | jq -r '.conclusion // empty')" - RUN_ID="$(echo "$MATCHING_RUN" | jq -r '.databaseId')" - - echo "Found matching CI run $RUN_ID: status=$STATUS conclusion=$CONCLUSION" - - if [ "$STATUS" = "completed" ]; then - if [ "$CONCLUSION" = "success" ]; then - echo "✓ CI workflow completed successfully, proceeding with npm publish" - exit 0 - else - echo "✗ CI workflow completed with conclusion=$CONCLUSION (expected success)" - exit 1 - fi - else - echo "CI workflow still running (status=$STATUS), waiting..." - fi - else - echo "No matching CI run found yet, waiting for workflow to start..." - fi - - sleep "$POLL_INTERVAL" - ELAPSED=$((ELAPSED + POLL_INTERVAL)) - done - - echo "✗ Timed out waiting for CI workflow to complete for tag $TAG" - exit 1 - - - name: Install dependencies (skip native build scripts) - run: npm ci --ignore-scripts - - - name: Publish to npm - run: npm publish - - build_python_wheels: - name: Python wheels - ${{ matrix.name }} - needs: [release] - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: true - matrix: - include: - - name: Linux x86_64 - runner: ubuntu-latest - - name: Linux aarch64 - runner: ubuntu-24.04-arm - - name: macOS arm64 - runner: macos-latest - - name: Windows amd64 - runner: windows-latest - - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Restore ccache - if: runner.os != 'Windows' - uses: actions/cache/restore@v5 - with: - path: /tmp/ccache - key: ccache-python-${{ runner.os }}-${{ runner.arch }}-master - restore-keys: | - ccache-python-${{ runner.os }}-${{ runner.arch }}- - - - name: Restore Conan cache - id: conan-cache - if: runner.os == 'Windows' - uses: actions/cache/restore@v5 - with: - path: ~/.conan2 - key: v4-conan-${{ runner.os }}-${{ hashFiles('pyproject.toml') }} - restore-keys: | - v4-conan-${{ runner.os }}- - - - name: Run cibuildwheel - uses: pypa/cibuildwheel@v3.4.0 - env: - CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /tmp/ccache:/ccache" - CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" - CIBW_CONFIG_SETTINGS_MACOS: "cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache" - CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: python-wheels-${{ matrix.name }} - path: wheelhouse/*.whl - - publish_python: - name: Publish Python wheels to PyPI - needs: [build_python_wheels] - runs-on: ubuntu-latest - permissions: - id-token: write - - steps: - - uses: actions/download-artifact@v4 - with: - pattern: python-wheels-* - path: dist - merge-multiple: true - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.13.0 - with: - verbose: true - # repository-url: https://test.pypi.org/legacy/ - diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index d39558b30e..9b84d35541 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -12,15 +12,30 @@ concurrency: jobs: build_wheels: - name: Build - cp312, Windows amd64 - runs-on: windows-latest + name: Build - cp312, ${{ matrix.name }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - name: Linux x86_64 + runner: ubuntu-latest + - name: Linux aarch64 + runner: ubuntu-24.04-arm + - name: macOS arm64 + runner: macos-14 + - name: Windows amd64 + runner: windows-latest steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Restore Conan cache id: conan-cache + if: runner.os == 'Windows' uses: actions/cache/restore@v5 with: path: ~/.conan2 @@ -31,11 +46,14 @@ jobs: - name: Run cibuildwheel uses: pypa/cibuildwheel@v3.4.0 env: + CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /tmp/ccache:/ccache" + CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" + CIBW_CONFIG_SETTINGS_MACOS: "cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache" CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" - name: Save Conan cache uses: actions/cache/save@v5 - if: "!cancelled() && steps.conan-cache.outputs.cache-hit != 'true'" + if: "!cancelled() && runner.os == 'Windows' && steps.conan-cache.outputs.cache-hit != 'true'" with: path: ~/.conan2 key: v4-conan-${{ runner.os }}-${{ hashFiles('pyproject.toml') }} @@ -43,5 +61,24 @@ jobs: - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: wheels-windows-amd64 + name: wheels-${{ matrix.name }} path: wheelhouse/*.whl + + publish_test_pypi: + name: Publish to TestPyPI + needs: [build_wheels] + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + pattern: wheels-* + merge-multiple: true + + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@v1.13.0 + with: + repository-url: https://test.pypi.org/legacy/ + verbose: true diff --git a/.github/workflows/release-monthly.yml b/.github/workflows/release-monthly.yml new file mode 100644 index 0000000000..d02fbe7ab9 --- /dev/null +++ b/.github/workflows/release-monthly.yml @@ -0,0 +1,56 @@ +name: Monthly Release + +on: + workflow_dispatch: + +concurrency: + group: release-monthly-${{ github.ref }} + cancel-in-progress: false + +jobs: + build_python_wheels: + name: Python wheels - ${{ matrix.name }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: true + matrix: + include: + - name: Windows amd64 + runner: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Restore Conan cache + id: conan-cache + if: runner.os == 'Windows' + uses: actions/cache/restore@v5 + with: + path: ~/.conan2 + key: v4-conan-${{ runner.os }}-${{ hashFiles('pyproject.toml') }} + restore-keys: | + v4-conan-${{ runner.os }}- + + - name: Run cibuildwheel + uses: pypa/cibuildwheel@v3.4.0 + env: + CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /tmp/ccache:/ccache" + CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" + CIBW_CONFIG_SETTINGS_MACOS: "cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache" + CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" + + - name: Save Conan cache + uses: actions/cache/save@v5 + if: "!cancelled() && runner.os == 'Windows' && steps.conan-cache.outputs.cache-hit != 'true'" + with: + path: ~/.conan2 + key: v4-conan-${{ runner.os }}-${{ hashFiles('pyproject.toml') }} + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: python-wheels-${{ matrix.name }} + path: wheelhouse/*.whl diff --git a/README.md b/README.md index 70f661502f..d468d646d8 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,24 @@ For usage details have a look [these API docs](docs/nodejs/api.md). An exemplary implementation by a 3rd party with Docker and Node.js can be found [here](https://github.com/door2door-io/osrm-express-server-demo). +### Using the Python Bindings + +The Python bindings provide read-only access to the routing engine via [nanobind](https://github.com/wjakob/nanobind). + +You can install the Python bindings from PyPI via + + pip install osrm-bindings + +We distribute `abi3` wheels for CPython 3.12+ on Linux (x86_64, aarch64), macOS (arm64) and Windows (x86_64). On other platforms `pip` will fall back to building from source, which requires CPython 3.10+ and the OSRM build dependencies. + +To build from source from this repository: + + pip install . + +#### Package docs + +For usage details and examples have a look at [the Python bindings README](src/python/README.md). + ## References in publications When using the code in a (scientific) publication, please cite From 0dd6b0df05d90c0f09bf7df3d8ed40619aacf346 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 00:27:07 +0200 Subject: [PATCH 08/50] suppress clang warnings --- src/python/CMakeLists.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 9fafaa8e48..d4f05c1863 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -52,12 +52,13 @@ set_target_properties(${EXT_NAME} PROPERTIES INTERPROCEDURAL_OPTIMIZATION FALSE) # differences between nanobind's type_caster and NB_MAKE_OPAQUE macros. target_link_options(${EXT_NAME} PRIVATE -Wno-error) -# nanobind's internal sources trigger warnings under OSRM's strict -Werror flags. -# Suppress -Werror for the nanobind static library target. +# nanobind's internal sources trigger warnings under OSRM's strict -Werror flags +# (inherited via CMAKE_CXX_FLAGS). Suppress for the nanobind static library target. +set(_nb_suppress -Wno-error -Wno-suggest-destructor-override -Wno-redundant-decls) if(TARGET nanobind-static-abi3) - target_compile_options(nanobind-static-abi3 PRIVATE -Wno-error) + target_compile_options(nanobind-static-abi3 PRIVATE ${_nb_suppress}) elseif(TARGET nanobind-static) - target_compile_options(nanobind-static PRIVATE -Wno-error) + target_compile_options(nanobind-static PRIVATE ${_nb_suppress}) endif() # Binding headers live in src/python/include/python/ From 91855bcb4461fb9a64a74ea8f3a113436ef72399 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 00:32:38 +0200 Subject: [PATCH 09/50] suppress more --- src/python/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index d4f05c1863..725630b224 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -47,6 +47,11 @@ nanobind_add_module( # flagged as errors during LTO). Disable IPO for the extension module — nanobind's # own LTO flag (passed to nanobind_add_module above) handles LTO for the binding. set_target_properties(${EXT_NAME} PROPERTIES INTERPROCEDURAL_OPTIMIZATION FALSE) +# Suppress warnings from nanobind headers included by our binding sources. +target_compile_options(${EXT_NAME} PRIVATE + -Wno-suggest-destructor-override + -Wno-redundant-decls +) # Also suppress LTO warnings-as-errors at link time, since the osrm library # is compiled with fat LTO objects and GCC's LTO linker plugin sees ODR # differences between nanobind's type_caster and NB_MAKE_OPAQUE macros. From cfa1065c58d1e085f53149df9abedd89991d65b9 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 10:22:11 +0200 Subject: [PATCH 10/50] fix mac & win --- pyproject.toml | 2 +- src/python/CMakeLists.txt | 34 ++++++++++++++++++---------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index babe77aa95..a52f1312ba 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ before-build = "ccache -s && ccache -M 500M" repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" [tool.cibuildwheel.macos] -environment = "MACOSX_DEPLOYMENT_TARGET=15 CCACHE_DIR=/tmp/ccache" +environment = "MACOSX_DEPLOYMENT_TARGET=14.0 CCACHE_DIR=/tmp/ccache" before-build = "ccache -s && ccache -M 500M" before-all = """ export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 725630b224..4883df97bc 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -47,23 +47,25 @@ nanobind_add_module( # flagged as errors during LTO). Disable IPO for the extension module — nanobind's # own LTO flag (passed to nanobind_add_module above) handles LTO for the binding. set_target_properties(${EXT_NAME} PROPERTIES INTERPROCEDURAL_OPTIMIZATION FALSE) -# Suppress warnings from nanobind headers included by our binding sources. -target_compile_options(${EXT_NAME} PRIVATE - -Wno-suggest-destructor-override - -Wno-redundant-decls -) -# Also suppress LTO warnings-as-errors at link time, since the osrm library -# is compiled with fat LTO objects and GCC's LTO linker plugin sees ODR -# differences between nanobind's type_caster and NB_MAKE_OPAQUE macros. -target_link_options(${EXT_NAME} PRIVATE -Wno-error) +if(NOT MSVC) + # Suppress warnings from nanobind headers included by our binding sources. + target_compile_options(${EXT_NAME} PRIVATE + -Wno-suggest-destructor-override + -Wno-redundant-decls + ) + # Also suppress LTO warnings-as-errors at link time, since the osrm library + # is compiled with fat LTO objects and GCC's LTO linker plugin sees ODR + # differences between nanobind's type_caster and NB_MAKE_OPAQUE macros. + target_link_options(${EXT_NAME} PRIVATE -Wno-error) -# nanobind's internal sources trigger warnings under OSRM's strict -Werror flags -# (inherited via CMAKE_CXX_FLAGS). Suppress for the nanobind static library target. -set(_nb_suppress -Wno-error -Wno-suggest-destructor-override -Wno-redundant-decls) -if(TARGET nanobind-static-abi3) - target_compile_options(nanobind-static-abi3 PRIVATE ${_nb_suppress}) -elseif(TARGET nanobind-static) - target_compile_options(nanobind-static PRIVATE ${_nb_suppress}) + # nanobind's internal sources trigger warnings under OSRM's strict -Werror flags + # (inherited via CMAKE_CXX_FLAGS). Suppress for the nanobind static library target. + set(_nb_suppress -Wno-error -Wno-suggest-destructor-override -Wno-redundant-decls) + if(TARGET nanobind-static-abi3) + target_compile_options(nanobind-static-abi3 PRIVATE ${_nb_suppress}) + elseif(TARGET nanobind-static) + target_compile_options(nanobind-static PRIVATE ${_nb_suppress}) + endif() endif() # Binding headers live in src/python/include/python/ From 3f3e4523029f6962171d89fa3c29a34d7ccd68c7 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 10:54:14 +0200 Subject: [PATCH 11/50] fix path to conanrun.bat --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a52f1312ba..a3d9e366be 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ before-all = """ [tool.cibuildwheel.windows] before-build = "pip install conan==2.27.0 delvewheel && conan profile detect --force" config-settings = "cmake.define.ENABLE_CONAN=ON" -repair-wheel-command = 'call build\cp312-abi3-win_amd64\_deps\libosrm-build\conanrun.bat && delvewheel repair --analyze-existing-exes --add-dll hwloc.dll --no-mangle tbb12.dll --no-mangle hwloc.dll -w {dest_dir} {wheel}' +repair-wheel-command = 'call build\cp312-abi3-win_amd64\conanrun.bat && delvewheel repair --analyze-existing-exes --add-dll hwloc.dll --no-mangle tbb12.dll --no-mangle hwloc.dll -w {dest_dir} {wheel}' test-command = [ "cd /d {project}/test/data", "windows-build-test-data.bat", From 3713f4447316f875b894583755e253afb28b1de8 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 11:45:09 +0200 Subject: [PATCH 12/50] add sdist so that version resolves properly for linux --- .github/workflows/python-bindings.yml | 53 ++++++++++++++++++++++++++- src/python/README.md | 52 +++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index 9b84d35541..1207c214ac 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -11,8 +11,35 @@ concurrency: cancel-in-progress: true jobs: + build_sdist: + name: Build sdist + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Build sdist + run: | + python -m pip install build + python -m build --sdist + + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + build_wheels: name: Build - cp312, ${{ matrix.name }} + needs: [build_sdist] runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -32,6 +59,20 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + fetch-tags: true + + - name: Download sdist + if: runner.os == 'Linux' + uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + + - name: Locate sdist + if: runner.os == 'Linux' + id: sdist + shell: bash + run: echo "path=$(ls dist/*.tar.gz | head -n1)" >> "$GITHUB_OUTPUT" - name: Restore Conan cache id: conan-cache @@ -45,6 +86,9 @@ jobs: - name: Run cibuildwheel uses: pypa/cibuildwheel@v3.4.0 + with: + # for linux build the wheel from the sdist + package-dir: ${{ runner.os == 'Linux' && steps.sdist.outputs.path || '.' }} env: CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /tmp/ccache:/ccache" CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" @@ -71,12 +115,19 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - name: Download wheels + uses: actions/download-artifact@v4 with: path: dist pattern: wheels-* merge-multiple: true + - name: Download sdist + uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + - name: Publish to TestPyPI uses: pypa/gh-action-pypi-publish@v1.13.0 with: diff --git a/src/python/README.md b/src/python/README.md index 58a20f75c9..f2ee4b4395 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -20,7 +20,7 @@ Windows | x86_64 ## From Source -osrm-bindings requires **CPython 3.10+** and can be installed from source by running the following command in the repository root: +osrm-bindings requires **CPython 3.10+** and can be installed from source (e.g. from an sdist on PyPI) by running the following command in the repository root: ``` pip install . @@ -28,6 +28,56 @@ pip install . The Python bindings are built alongside the OSRM C++ libraries. The version of the bindings matches the version of osrm-backend. +### System dependencies + +A source/sdist build compiles the full OSRM C++ library, so the usual C++ toolchain (CMake ≥ 3.18, a C++20 compiler, Git) plus OSRM's native dependencies must be available. Also a development Python installation has to be in the PATH. + +**Debian / Ubuntu** + +``` +sudo apt-get install -y \ + cmake g++ git pkg-config \ + libboost-all-dev libbz2-dev liblua5.4-dev \ + libtbb-dev libxml2-dev libzip-dev +``` + +**Fedora / RHEL / Rocky / AlmaLinux** + +``` +sudo dnf install -y \ + cmake gcc-c++ git pkgconf-pkg-config \ + boost-devel bzip2-devel lua-devel \ + tbb-devel libxml2-devel libzip-devel +``` + +**Alpine** + +``` +apk add --no-cache \ + cmake clang make git pkgconf \ + boost-dev bzip2-dev lua5.4-dev \ + onetbb-dev libxml2-dev libzip-dev expat-dev +``` + +**macOS (Homebrew)** + +``` +brew install cmake lua tbb boost@1.90 +brew link boost@1.90 +``` + +**Windows** + +Windows uses [Conan](https://conan.io/) for OSRM's C++ dependencies. Install Conan 2.x and pass `ENABLE_CONAN=ON` to CMake: + +``` +pip install conan==2.27.0 +conan profile detect --force +pip install . -C cmake.define.ENABLE_CONAN=ON +``` + +A full Visual Studio 2022 toolchain (or the Build Tools equivalent with the C++ workload) is required. + ## Example The following example will showcase the process of calculating routes between two coordinates. From 2493aebc5aa9006b394aa5262573a58df50ad0b2 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 12:03:00 +0200 Subject: [PATCH 13/50] fix sdist to wheel --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a3d9e366be..538c7ca34e 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ wheel.py-api = "cp312" wheel.packages = ["src/python/osrm"] wheel.exclude = ["include/**", "lib/**", "bin/**", "share/**"] sdist.include = ["pyproject.toml"] -sdist.exclude = ["test", "dist", "wheelhouse"] +sdist.exclude = ["dist", "wheelhouse"] [tool.scikit-build.cmake.define] ENABLE_PYTHON_BINDINGS = "ON" From d2f5e1aa479f634d7bc655c38ff7862a3787b991 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 12:25:22 +0200 Subject: [PATCH 14/50] debug --- .github/workflows/python-bindings.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index 1207c214ac..7e0586594a 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -26,10 +26,31 @@ jobs: with: python-version: "3.12" + - name: Debug git + setuptools_scm + run: | + echo "=== git --version ===" + git --version + echo "=== git describe ===" + git describe --tags --always --long || echo "describe failed" + echo "=== git tags (v*) ===" + git tag --list 'v*' | tail -5 + echo "=== git rev-list --count HEAD ===" + git rev-list --count HEAD + echo "=== git config --get safe.directory ===" + git config --get-all safe.directory || echo "(none)" + echo "=== git status of repo ownership ===" + ls -la .git | head -3 + id + echo "=== setuptools_scm.get_version() ===" + python -m pip install 'setuptools-scm>=10' + python -c "from setuptools_scm import get_version; print(get_version())" + - name: Build sdist run: | python -m pip install build python -m build --sdist + echo "=== sdist produced ===" + ls -la dist/ - name: Upload sdist uses: actions/upload-artifact@v4 From a3d2ef7e7ac11365d1201528e8774c2dc2e4d6d1 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 13:16:07 +0200 Subject: [PATCH 15/50] try embedding in huge workflow --- .../python-bindings.yml | 19 - .../osrm-backend.yml | 358 ++++++++++-------- 2 files changed, 200 insertions(+), 177 deletions(-) rename .github/{workflows => workflows-disabled}/python-bindings.yml (82%) rename .github/{workflows-disabled => workflows}/osrm-backend.yml (73%) diff --git a/.github/workflows/python-bindings.yml b/.github/workflows-disabled/python-bindings.yml similarity index 82% rename from .github/workflows/python-bindings.yml rename to .github/workflows-disabled/python-bindings.yml index 7e0586594a..1cb46f9844 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows-disabled/python-bindings.yml @@ -26,25 +26,6 @@ jobs: with: python-version: "3.12" - - name: Debug git + setuptools_scm - run: | - echo "=== git --version ===" - git --version - echo "=== git describe ===" - git describe --tags --always --long || echo "describe failed" - echo "=== git tags (v*) ===" - git tag --list 'v*' | tail -5 - echo "=== git rev-list --count HEAD ===" - git rev-list --count HEAD - echo "=== git config --get safe.directory ===" - git config --get-all safe.directory || echo "(none)" - echo "=== git status of repo ownership ===" - ls -la .git | head -3 - id - echo "=== setuptools_scm.get_version() ===" - python -m pip install 'setuptools-scm>=10' - python -c "from setuptools_scm import get_version; print(get_version())" - - name: Build sdist run: | python -m pip install build diff --git a/.github/workflows-disabled/osrm-backend.yml b/.github/workflows/osrm-backend.yml similarity index 73% rename from .github/workflows-disabled/osrm-backend.yml rename to .github/workflows/osrm-backend.yml index 678cc45141..d4b94e077f 100644 --- a/.github/workflows-disabled/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -176,142 +176,142 @@ jobs: strategy: matrix: include: - - name: clang-20-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-20 - CXXCOMPILER: clang++-20 - ENABLE_LTO: OFF - - - name: clang-19-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-19 - CXXCOMPILER: clang++-19 - ENABLE_LTO: OFF - - - name: clang-18-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_LTO: OFF - - - name: clang-18-debug - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_LTO: OFF - - - name: clang-18-debug-clang-tidy - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_CLANG_TIDY: ON - NODE_PACKAGE_TESTS_ONLY: ON - ENABLE_LTO: OFF - - - name: clang-18-debug-asan-ubsan - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_SANITIZER: ON - TARGET_ARCH: x86_64-asan-ubsan - OSRM_CONNECTION_RETRIES: 10 - OSRM_CONNECTION_EXP_BACKOFF_COEF: 1.5 - - - name: clang-17-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-17 - CXXCOMPILER: clang++-17 - ENABLE_LTO: OFF - - - name: clang-16-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-16 - CXXCOMPILER: clang++-16 - ENABLE_LTO: OFF - - - name: gcc-14-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: gcc-14 - CXXCOMPILER: g++-14 - CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' - - - name: gcc-13-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: gcc-13 - CXXCOMPILER: g++-13 - CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' - - - name: gcc-13-debug-cov - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: gcc-13 - CXXCOMPILER: g++-13 - ENABLE_COVERAGE: ON - - - name: gcc-12-release - continue-on-error: false - node: 24 - runs-on: ubuntu-22.04 - BUILD_TYPE: Release - CCOMPILER: gcc-12 - CXXCOMPILER: g++-12 - CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' - - - name: conan-linux-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_CONAN: ON - ENABLE_LTO: OFF - - - name: conan-linux-debug-asan-ubsan - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Release - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_CONAN: ON - ENABLE_SANITIZER: ON - ENABLE_LTO: OFF + # - name: clang-20-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-20 + # CXXCOMPILER: clang++-20 + # ENABLE_LTO: OFF + + # - name: clang-19-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-19 + # CXXCOMPILER: clang++-19 + # ENABLE_LTO: OFF + + # - name: clang-18-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_LTO: OFF + + # - name: clang-18-debug + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Debug + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_LTO: OFF + + # - name: clang-18-debug-clang-tidy + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Debug + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_CLANG_TIDY: ON + # NODE_PACKAGE_TESTS_ONLY: ON + # ENABLE_LTO: OFF + + # - name: clang-18-debug-asan-ubsan + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Debug + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_SANITIZER: ON + # TARGET_ARCH: x86_64-asan-ubsan + # OSRM_CONNECTION_RETRIES: 10 + # OSRM_CONNECTION_EXP_BACKOFF_COEF: 1.5 + + # - name: clang-17-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-17 + # CXXCOMPILER: clang++-17 + # ENABLE_LTO: OFF + + # - name: clang-16-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-16 + # CXXCOMPILER: clang++-16 + # ENABLE_LTO: OFF + + # - name: gcc-14-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: gcc-14 + # CXXCOMPILER: g++-14 + # CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' + + # - name: gcc-13-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: gcc-13 + # CXXCOMPILER: g++-13 + # CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' + + # - name: gcc-13-debug-cov + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Debug + # CCOMPILER: gcc-13 + # CXXCOMPILER: g++-13 + # ENABLE_COVERAGE: ON + + # - name: gcc-12-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-22.04 + # BUILD_TYPE: Release + # CCOMPILER: gcc-12 + # CXXCOMPILER: g++-12 + # CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' + + # - name: conan-linux-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_CONAN: ON + # ENABLE_LTO: OFF + + # - name: conan-linux-debug-asan-ubsan + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Release + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_CONAN: ON + # ENABLE_SANITIZER: ON + # ENABLE_LTO: OFF - name: conan-linux-release-node - build_node_package: true + build_bindings: true continue-on-error: false node: 24 runs-on: ubuntu-24.04 @@ -321,29 +321,28 @@ jobs: ENABLE_CONAN: ON NODE_PACKAGE_TESTS_ONLY: ON - - name: conan-linux-debug-node - build_node_package: true - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04 - BUILD_TYPE: Debug - CCOMPILER: clang-16 - CXXCOMPILER: clang++-16 - ENABLE_CONAN: ON - NODE_PACKAGE_TESTS_ONLY: ON - - - name: conan-linux-arm64-release - continue-on-error: false - node: 24 - runs-on: ubuntu-24.04-arm - BUILD_TYPE: Release - CCOMPILER: clang-18 - CXXCOMPILER: clang++-18 - ENABLE_CONAN: ON - ENABLE_LTO: OFF + # - name: conan-linux-debug-node + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04 + # BUILD_TYPE: Debug + # CCOMPILER: clang-16 + # CXXCOMPILER: clang++-16 + # ENABLE_CONAN: ON + # NODE_PACKAGE_TESTS_ONLY: ON + + # - name: conan-linux-arm64-release + # continue-on-error: false + # node: 24 + # runs-on: ubuntu-24.04-arm + # BUILD_TYPE: Release + # CCOMPILER: clang-18 + # CXXCOMPILER: clang++-18 + # ENABLE_CONAN: ON + # ENABLE_LTO: OFF - name: conan-macos-x64-release-node - build_node_package: true + build_bindings: true continue-on-error: true node: 24 runs-on: macos-15-intel # x86_64 @@ -354,7 +353,7 @@ jobs: ENABLE_CONAN: ON - name: conan-macos-arm64-release-node - build_node_package: true + build_bindings: true continue-on-error: true node: 24 runs-on: macos-15 # arm64 @@ -384,6 +383,7 @@ jobs: OSRM_CONNECTION_RETRIES: ${{ matrix.OSRM_CONNECTION_RETRIES }} OSRM_CONNECTION_EXP_BACKOFF_COEF: ${{ matrix.OSRM_CONNECTION_EXP_BACKOFF_COEF }} ENABLE_LTO: ${{ matrix.ENABLE_LTO }} + BUILD_BINDINGS: ${{ matrix.build_bindings }} steps: - uses: actions/checkout@v6 - name: Build machine architecture @@ -606,10 +606,10 @@ jobs: path: test/logs/ - name: Build Node package - if: ${{ matrix.build_node_package }} + if: ${{ env.BUILD_BINDINGS }} run: ./scripts/ci/node_package.sh - name: Publish Node package - if: ${{ matrix.build_node_package && env.PUBLISH == 'On' }} + if: ${{ env.BUILD_BINDINGS && env.PUBLISH == 'On' }} uses: ncipollo/release-action@v1 with: allowUpdates: true @@ -621,6 +621,48 @@ jobs: omitNameDuringUpdate: true replacesArtifacts: true token: ${{ secrets.GITHUB_TOKEN }} + + # Python bindings + + - name: Set up Python + if: ${{ env.BUILD_BINDINGS }} + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Build sdist + if: ${{ env.BUILD_BINDINGS && runner.os == 'Linux' }} + run: | + python -m pip install build + python -m build --sdist + + - name: Locate sdist + if: ${{ env.BUILD_BINDINGS && runner.os == 'Linux' }} + id: sdist + shell: bash + run: echo "path=$(ls dist/*.tar.gz | head -n1)" >> "$GITHUB_OUTPUT" + + - name: Run cibuildwheel + uses: pypa/cibuildwheel@v3 + if: ${{ env.BUILD_BINDINGS }} + with: + # for linux build the wheel from the sdist + package-dir: ${{ runner.os == 'Linux' && steps.sdist.outputs.path || '.' }} + output-dir: dist + env: + # /tmp/ccache can be cached too, esp useful for linux + CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /tmp/ccache:/ccache" + CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" + CIBW_CONFIG_SETTINGS_MACOS: "cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache" + CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" + + - name: Publish to TestPyPI + if: ${{ env.BUILD_BINDINGS && env.PUBLISH == 'On' }} + uses: pypa/gh-action-pypi-publish@v1.13.0 + with: + repository-url: https://test.pypi.org/legacy/ + verbose: true + - name: Show CCache statistics run: | ccache -p From da8e06835dce4620e87101cb68cbb14f8654dc26 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 13:17:13 +0200 Subject: [PATCH 16/50] trigger CI From ab38ab8f80ac1ef3fea115068a86c876e387c10a Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 13:17:34 +0200 Subject: [PATCH 17/50] minor fix --- .github/workflows/osrm-backend.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index d4b94e077f..df25c9d113 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -366,6 +366,9 @@ jobs: name: ${{ matrix.name}} continue-on-error: ${{ matrix.continue-on-error }} runs-on: ${{ matrix.runs-on }} + # for PyPI trusted publisher + permissions: + id-token: write env: BUILD_TYPE: ${{ matrix.BUILD_TYPE }} BUILD_SHARED_LIBS: ${{ matrix.BUILD_SHARED_LIBS }} From 3c5770f4d68ead5fb871938d24b419f7efe7df3a Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 13:20:07 +0200 Subject: [PATCH 18/50] huh, needs explicit vesion --- .github/workflows/osrm-backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index df25c9d113..fdd10b0154 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -646,7 +646,7 @@ jobs: run: echo "path=$(ls dist/*.tar.gz | head -n1)" >> "$GITHUB_OUTPUT" - name: Run cibuildwheel - uses: pypa/cibuildwheel@v3 + uses: pypa/cibuildwheel@v3.4.1 if: ${{ env.BUILD_BINDINGS }} with: # for linux build the wheel from the sdist From 062c020b044568bd8ecd2099282dd34ef41d9bf4 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 13:28:27 +0200 Subject: [PATCH 19/50] more excluded for sdist --- .github/workflows/osrm-backend.yml | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index fdd10b0154..ecee0fcdf1 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -389,6 +389,9 @@ jobs: BUILD_BINDINGS: ${{ matrix.build_bindings }} steps: - uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true - name: Build machine architecture run: uname -m - name: Use Node.js diff --git a/pyproject.toml b/pyproject.toml index 538c7ca34e..e1d55a9b47 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ wheel.py-api = "cp312" wheel.packages = ["src/python/osrm"] wheel.exclude = ["include/**", "lib/**", "bin/**", "share/**"] sdist.include = ["pyproject.toml"] -sdist.exclude = ["dist", "wheelhouse"] +sdist.exclude = ["dist", "wheelhouse", "build", "build-*", ".venv", "**/.venv"] [tool.scikit-build.cmake.define] ENABLE_PYTHON_BINDINGS = "ON" From 216bc0d4b7dd0182be3866db7605b69926c88a3d Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 20:28:33 +0200 Subject: [PATCH 20/50] osx diagnostics --- .github/workflows/osrm-backend.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index ecee0fcdf1..45c4623b80 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -392,6 +392,12 @@ jobs: with: fetch-depth: 0 fetch-tags: true + - name: Use Xcode SDK (macOS) + if: runner.os == 'macOS' + run: | + sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer + echo "SDKROOT=$(xcrun --show-sdk-path)" >> "$GITHUB_ENV" + echo "DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer" >> "$GITHUB_ENV" - name: Build machine architecture run: uname -m - name: Use Node.js From 427b420a67a3e8950f12ec790fd1e10e1eec5413 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 20:38:51 +0200 Subject: [PATCH 21/50] trigger CI From a1209b052b2aef48be8884d65e0653ea86ff1da4 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Fri, 17 Apr 2026 21:00:49 +0200 Subject: [PATCH 22/50] more ci --- .github/workflows/osrm-backend.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index aae5b80123..ce6fb07cef 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -397,7 +397,14 @@ jobs: - name: Use Xcode SDK (macOS) if: runner.os == 'macOS' run: | + echo "--- before ---" + xcode-select -p + xcrun --show-sdk-path + ls /Applications | grep -i Xcode || true sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer + echo "--- after ---" + xcode-select -p + xcrun --show-sdk-path echo "SDKROOT=$(xcrun --show-sdk-path)" >> "$GITHUB_ENV" echo "DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer" >> "$GITHUB_ENV" - name: Build machine architecture From 89b232268f1368bb6bf88f3ae6238236909ccb5a Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Mon, 20 Apr 2026 21:59:32 +0200 Subject: [PATCH 23/50] try again macos --- .github/workflows/osrm-backend.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index ce6fb07cef..3108f801b7 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -394,19 +394,6 @@ jobs: with: fetch-depth: 0 fetch-tags: true - - name: Use Xcode SDK (macOS) - if: runner.os == 'macOS' - run: | - echo "--- before ---" - xcode-select -p - xcrun --show-sdk-path - ls /Applications | grep -i Xcode || true - sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer - echo "--- after ---" - xcode-select -p - xcrun --show-sdk-path - echo "SDKROOT=$(xcrun --show-sdk-path)" >> "$GITHUB_ENV" - echo "DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer" >> "$GITHUB_ENV" - name: Build machine architecture run: uname -m - name: Use Node.js From f932c2e8dc613820e0bbfaa65f2478528ae8662e Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Tue, 21 Apr 2026 09:58:00 +0200 Subject: [PATCH 24/50] let's try --- CMakeLists.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b51be76f37..7e2d054fb2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -490,8 +490,18 @@ add_dependency_includes(${ZLIB_INCLUDE_DIRS}) set(ZLIB_LIBRARY ${ZLIB_LIBRARIES}) add_definitions(${OSRM_DEFINES}) -include_directories(SYSTEM ${DEPENDENCIES_INCLUDE_DIRS}) - +# Drop paths the compiler already searches implicitly. Passing e.g. the SDK's +# /usr/include as an explicit -isystem reorders it ahead of libc++'s own +# header directory and breaks libc++'s guard on macOS. +set(_osrm_system_includes ${DEPENDENCIES_INCLUDE_DIRS}) +if(CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES) + list(REMOVE_ITEM _osrm_system_includes ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES}) +endif() +if(CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES) + list(REMOVE_ITEM _osrm_system_includes ${CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES}) +endif() +include_directories(SYSTEM ${_osrm_system_includes}) +unset(_osrm_system_includes) # Binaries target_link_libraries(osrm-datastore osrm_store ${Boost_PROGRAM_OPTIONS_LIBRARY}) From 1a6c95a39433f7ed62c296cf6d3903e52e588587 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Tue, 21 Apr 2026 10:54:51 +0200 Subject: [PATCH 25/50] change caching behavior, try to remove libiconv workaround for macos --- .github/workflows/osrm-backend.yml | 45 +++++++++++++++++------------- CMakeLists.txt | 13 +-------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 3108f801b7..5fc6cb3e50 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -407,18 +407,20 @@ jobs: key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - - name: Enable compiler cache - uses: actions/cache@v5 + - name: Restore compiler cache + id: ccache-restore + uses: actions/cache/restore@v5 with: path: ~/.ccache key: ccache-${{ matrix.name }}-${{ github.sha }} restore-keys: | ccache-${{ matrix.name }}- - - name: Enable Conan cache - uses: actions/cache@v5 + - name: Restore Conan cache + id: conan-restore + uses: actions/cache/restore@v5 with: path: ~/.conan2 - key: v10-conan-${{ matrix.name }}-${{ github.sha }} + key: v10-conan-${{ matrix.name }}-${{ hashFiles('conanfile.py') }} restore-keys: | v10-conan-${{ matrix.name }}- - name: Enable test cache @@ -490,17 +492,15 @@ jobs: fi fi - # TBB - TBB_VERSION=2021.12.0 + # TBB (Linux only; macOS gets it from conan or brew) if [[ "${RUNNER_OS}" == "Linux" ]]; then + TBB_VERSION=2021.12.0 TBB_URL="https://github.com/oneapi-src/oneTBB/releases/download/v${TBB_VERSION}/oneapi-tbb-${TBB_VERSION}-lin.tgz" - elif [[ "${RUNNER_OS}" == "macOS" ]]; then - TBB_URL="https://github.com/oneapi-src/oneTBB/releases/download/v${TBB_VERSION}/oneapi-tbb-${TBB_VERSION}-mac.tgz" + wget --tries 5 ${TBB_URL} -O onetbb.tgz + tar zxvf onetbb.tgz + sudo cp -a oneapi-tbb-${TBB_VERSION}/lib/. /usr/local/lib/ + sudo cp -a oneapi-tbb-${TBB_VERSION}/include/. /usr/local/include/ fi - wget --tries 5 ${TBB_URL} -O onetbb.tgz - tar zxvf onetbb.tgz - sudo cp -a oneapi-tbb-${TBB_VERSION}/lib/. /usr/local/lib/ - sudo cp -a oneapi-tbb-${TBB_VERSION}/include/. /usr/local/include/ - name: Prepare build run: | mkdir ${OSRM_BUILD_DIR} @@ -511,14 +511,9 @@ jobs: fi echo "CC=${CCOMPILER}" >> $GITHUB_ENV echo "CXX=${CXXCOMPILER}" >> $GITHUB_ENV - if [[ "${RUNNER_OS}" == "macOS" ]]; then - # missing from GCC path, needed for conan builds of libiconv, for example. - sudo xcode-select --switch /Library/Developer/CommandLineTools - echo "LIBRARY_PATH=${LIBRARY_PATH}:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib" >> $GITHUB_ENV - echo "CPATH=${CPATH}:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include" >> $GITHUB_ENV - fi - name: Build and install OSRM + id: build run: | echo "Using ${JOBS} jobs" pushd ${OSRM_BUILD_DIR} @@ -562,6 +557,18 @@ jobs: popd env: Boost_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} + - name: Save compiler cache + if: steps.build.outcome == 'success' && steps.ccache-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ~/.ccache + key: ccache-${{ matrix.name }}-${{ github.sha }} + - name: Save Conan cache + if: steps.build.outcome == 'success' && steps.conan-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ~/.conan2 + key: v10-conan-${{ matrix.name }}-${{ hashFiles('conanfile.py') }} - name: Run all tests if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY != 'ON' }} run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e2d054fb2..f4cbed7230 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -490,18 +490,7 @@ add_dependency_includes(${ZLIB_INCLUDE_DIRS}) set(ZLIB_LIBRARY ${ZLIB_LIBRARIES}) add_definitions(${OSRM_DEFINES}) -# Drop paths the compiler already searches implicitly. Passing e.g. the SDK's -# /usr/include as an explicit -isystem reorders it ahead of libc++'s own -# header directory and breaks libc++'s guard on macOS. -set(_osrm_system_includes ${DEPENDENCIES_INCLUDE_DIRS}) -if(CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES) - list(REMOVE_ITEM _osrm_system_includes ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES}) -endif() -if(CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES) - list(REMOVE_ITEM _osrm_system_includes ${CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES}) -endif() -include_directories(SYSTEM ${_osrm_system_includes}) -unset(_osrm_system_includes) +include_directories(SYSTEM ${DEPENDENCIES_INCLUDE_DIRS}) # Binaries target_link_libraries(osrm-datastore osrm_store ${Boost_PROGRAM_OPTIONS_LIBRARY}) From a964f6643806ed1d568cdee058e1eed7a2b15982 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Tue, 21 Apr 2026 11:30:34 +0200 Subject: [PATCH 26/50] change runner back to 15. alternatively python bindings could become their own workflow --- .github/workflows/osrm-backend.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 5fc6cb3e50..742a1094b0 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -347,7 +347,7 @@ jobs: build_bindings: true continue-on-error: true node: 24 - runs-on: macos-26-intel # x86_64 + runs-on: macos-15-intel # x86_64 BUILD_TYPE: Release CCOMPILER: clang CXXCOMPILER: clang++ @@ -358,7 +358,7 @@ jobs: build_bindings: true continue-on-error: true node: 24 - runs-on: macos-26 # arm64 + runs-on: macos-15 # arm64 BUILD_TYPE: Release CCOMPILER: clang CXXCOMPILER: clang++ diff --git a/pyproject.toml b/pyproject.toml index e1d55a9b47..2cdf669678 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ before-build = "ccache -s && ccache -M 500M" repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" [tool.cibuildwheel.macos] -environment = "MACOSX_DEPLOYMENT_TARGET=14.0 CCACHE_DIR=/tmp/ccache" +environment = "MACOSX_DEPLOYMENT_TARGET=15.0 CCACHE_DIR=/tmp/ccache" before-build = "ccache -s && ccache -M 500M" before-all = """ export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 From 74a9b4deeb5a8c2f6ae4e53318ce2d8c0b780db9 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Tue, 21 Apr 2026 11:33:12 +0200 Subject: [PATCH 27/50] disable all other workflows --- .github/workflows/osrm-backend.yml | 228 ++++++++++++++--------------- 1 file changed, 114 insertions(+), 114 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 742a1094b0..bd146b5bf7 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -22,75 +22,75 @@ concurrency: cancel-in-progress: true jobs: - conan-windows-release-node: - needs: format-taginfo-docs - runs-on: windows-2025 - continue-on-error: false - env: - BUILD_TYPE: Release - steps: - - uses: actions/checkout@v6 - - run: cmake --version - - uses: actions/setup-node@v6 - with: - node-version: 24 - - run: node --version - - run: npm --version - - name: Prepare environment - shell: bash - run: | - PACKAGE_JSON_VERSION=$(node -e "console.log(require('./package.json').version)") - echo PUBLISH=$([[ "${GITHUB_REF:-}" == "refs/tags/v${PACKAGE_JSON_VERSION}" ]] && echo "On" || echo "Off") >> $GITHUB_ENV - - run: npm install --ignore-scripts - - run: npm link --ignore-scripts - - name: Build - shell: bash - run: | - mkdir build - cd build - - python3 -m venv .venv - source .venv/Scripts/Activate - python3 -m pip install conan==2.27.1 - conan profile detect --force - - cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_CONAN=ON -DENABLE_NODE_BINDINGS=ON .. - cmake --build . --config Release - - # TODO: MSVC goes out of memory when building our tests - # - name: Run tests - # shell: bash - # run: | - # cd build - # cmake --build . --config Release --target tests - # # TODO: run tests - # - name: Run node tests - # shell: bash - # run: | - # ./lib/binding_napi_v8/osrm-extract.exe -p profiles/car.lua test/data/monaco.osm.pbf - - # mkdir -p test/data/ch - # cp test/data/monaco.osrm* test/data/ch/ - # ./lib/binding_napi_v8/osrm-contract.exe test/data/ch/monaco.osrm - - # ./lib/binding_napi_v8/osrm-datastore.exe test/data/ch/monaco.osrm - # node test/nodejs/index.js - - name: Build Node package - shell: bash - run: ./scripts/ci/node_package.sh - - name: Publish Node package - if: ${{ env.PUBLISH == 'On' }} - uses: ncipollo/release-action@v1 - with: - allowUpdates: true - artifactErrorsFailBuild: true - artifacts: build/stage/**/*.tar.gz - omitBody: true - omitBodyDuringUpdate: true - omitName: true - omitNameDuringUpdate: true - replacesArtifacts: true - token: ${{ secrets.GITHUB_TOKEN }} + # conan-windows-release-node: + # needs: format-taginfo-docs + # runs-on: windows-2025 + # continue-on-error: false + # env: + # BUILD_TYPE: Release + # steps: + # - uses: actions/checkout@v6 + # - run: cmake --version + # - uses: actions/setup-node@v6 + # with: + # node-version: 24 + # - run: node --version + # - run: npm --version + # - name: Prepare environment + # shell: bash + # run: | + # PACKAGE_JSON_VERSION=$(node -e "console.log(require('./package.json').version)") + # echo PUBLISH=$([[ "${GITHUB_REF:-}" == "refs/tags/v${PACKAGE_JSON_VERSION}" ]] && echo "On" || echo "Off") >> $GITHUB_ENV + # - run: npm install --ignore-scripts + # - run: npm link --ignore-scripts + # - name: Build + # shell: bash + # run: | + # mkdir build + # cd build + + # python3 -m venv .venv + # source .venv/Scripts/Activate + # python3 -m pip install conan==2.27.1 + # conan profile detect --force + + # cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_CONAN=ON -DENABLE_NODE_BINDINGS=ON .. + # cmake --build . --config Release + + # # TODO: MSVC goes out of memory when building our tests + # # - name: Run tests + # # shell: bash + # # run: | + # # cd build + # # cmake --build . --config Release --target tests + # # # TODO: run tests + # # - name: Run node tests + # # shell: bash + # # run: | + # # ./lib/binding_napi_v8/osrm-extract.exe -p profiles/car.lua test/data/monaco.osm.pbf + + # # mkdir -p test/data/ch + # # cp test/data/monaco.osrm* test/data/ch/ + # # ./lib/binding_napi_v8/osrm-contract.exe test/data/ch/monaco.osrm + + # # ./lib/binding_napi_v8/osrm-datastore.exe test/data/ch/monaco.osrm + # # node test/nodejs/index.js + # - name: Build Node package + # shell: bash + # run: ./scripts/ci/node_package.sh + # - name: Publish Node package + # if: ${{ env.PUBLISH == 'On' }} + # uses: ncipollo/release-action@v1 + # with: + # allowUpdates: true + # artifactErrorsFailBuild: true + # artifacts: build/stage/**/*.tar.gz + # omitBody: true + # omitBodyDuringUpdate: true + # omitName: true + # omitNameDuringUpdate: true + # replacesArtifacts: true + # token: ${{ secrets.GITHUB_TOKEN }} format-taginfo-docs: runs-on: ubuntu-slim @@ -128,50 +128,50 @@ jobs: npm run docs && ./scripts/error_on_dirty.sh "Run 'npm run docs' locally and commit the changes." npm audit --production - docker-image-matrix: - strategy: - matrix: - docker-base-image: ['debian', 'alpine'] - needs: format-taginfo-docs - runs-on: ubuntu-22.04 - continue-on-error: false - steps: - - name: Check out the repo - uses: actions/checkout@v6 - - name: Enable osm.pbf cache - uses: actions/cache@v5 - with: - path: berlin-latest.osm.pbf - key: v1-berlin-osm-pbf - restore-keys: | - v1-berlin-osm-pbf - - name: Docker build - run: | - docker build -t osrm-backend-local -f docker/Dockerfile-${{ matrix.docker-base-image }} . - - name: Test Docker image - run: | - if [ ! -f "${PWD}/berlin-latest.osm.pbf" ]; then - wget http://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf - fi - TAG=osrm-backend-local - # when `--memory-swap` value equals `--memory` it means container won't use swap - # see https://docs.docker.com/config/containers/resource_constraints/#--memory-swap-details - MEMORY_ARGS="--memory=1g --memory-swap=1g" - docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-extract --dump-nbg-graph -p /opt/car.lua /data/berlin-latest.osm.pbf - docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-components /data/berlin-latest.osrm.nbg /data/berlin-latest.geojson - if [ ! -s "${PWD}/berlin-latest.geojson" ] - then - >&2 echo "No berlin-latest.geojson found" - exit 1 - fi - # removing `.osrm.nbg` to check that whole pipeline works without it - rm -rf "${PWD}/berlin-latest.osrm.nbg" - - docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-partition /data/berlin-latest.osrm - docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-customize /data/berlin-latest.osrm - docker run $MEMORY_ARGS --name=osrm-container -t -p 5000:5000 -v "${PWD}:/data" "${TAG}" osrm-routed --algorithm mld /data/berlin-latest.osrm & - curl --retry-delay 3 --retry 10 --retry-all-errors "http://127.0.0.1:5000/route/v1/driving/13.388860,52.517037;13.385983,52.496891?steps=true" - docker stop osrm-container + # docker-image-matrix: + # strategy: + # matrix: + # docker-base-image: ['debian', 'alpine'] + # needs: format-taginfo-docs + # runs-on: ubuntu-22.04 + # continue-on-error: false + # steps: + # - name: Check out the repo + # uses: actions/checkout@v6 + # - name: Enable osm.pbf cache + # uses: actions/cache@v5 + # with: + # path: berlin-latest.osm.pbf + # key: v1-berlin-osm-pbf + # restore-keys: | + # v1-berlin-osm-pbf + # - name: Docker build + # run: | + # docker build -t osrm-backend-local -f docker/Dockerfile-${{ matrix.docker-base-image }} . + # - name: Test Docker image + # run: | + # if [ ! -f "${PWD}/berlin-latest.osm.pbf" ]; then + # wget http://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf + # fi + # TAG=osrm-backend-local + # # when `--memory-swap` value equals `--memory` it means container won't use swap + # # see https://docs.docker.com/config/containers/resource_constraints/#--memory-swap-details + # MEMORY_ARGS="--memory=1g --memory-swap=1g" + # docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-extract --dump-nbg-graph -p /opt/car.lua /data/berlin-latest.osm.pbf + # docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-components /data/berlin-latest.osrm.nbg /data/berlin-latest.geojson + # if [ ! -s "${PWD}/berlin-latest.geojson" ] + # then + # >&2 echo "No berlin-latest.geojson found" + # exit 1 + # fi + # # removing `.osrm.nbg` to check that whole pipeline works without it + # rm -rf "${PWD}/berlin-latest.osrm.nbg" + + # docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-partition /data/berlin-latest.osrm + # docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-customize /data/berlin-latest.osrm + # docker run $MEMORY_ARGS --name=osrm-container -t -p 5000:5000 -v "${PWD}:/data" "${TAG}" osrm-routed --algorithm mld /data/berlin-latest.osrm & + # curl --retry-delay 3 --retry 10 --retry-all-errors "http://127.0.0.1:5000/route/v1/driving/13.388860,52.517037;13.385983,52.496891?steps=true" + # docker stop osrm-container build-matrix: needs: format-taginfo-docs @@ -689,6 +689,6 @@ jobs: ci-complete: runs-on: ubuntu-latest - needs: [build-matrix, conan-windows-release-node, docker-image-matrix] + needs: [build-matrix] steps: - run: echo "CI complete" From 63c50f12bcc6fef8a72af45054c48b6898df8e3e Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Tue, 21 Apr 2026 12:17:27 +0200 Subject: [PATCH 28/50] try pinning boost to 1.85 --- .github/workflows/osrm-backend.yml | 6 +++--- pyproject.toml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index bd146b5bf7..67a5d7419e 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -420,9 +420,9 @@ jobs: uses: actions/cache/restore@v5 with: path: ~/.conan2 - key: v10-conan-${{ matrix.name }}-${{ hashFiles('conanfile.py') }} + key: v11-conan-${{ matrix.name }}-${{ hashFiles('conanfile.py') }} restore-keys: | - v10-conan-${{ matrix.name }}- + v11-conan-${{ matrix.name }}- - name: Enable test cache uses: actions/cache@v5 with: @@ -568,7 +568,7 @@ jobs: uses: actions/cache/save@v5 with: path: ~/.conan2 - key: v10-conan-${{ matrix.name }}-${{ hashFiles('conanfile.py') }} + key: v11-conan-${{ matrix.name }}-${{ hashFiles('conanfile.py') }} - name: Run all tests if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY != 'ON' }} run: | diff --git a/pyproject.toml b/pyproject.toml index 2cdf669678..62b873e4c7 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,8 +87,8 @@ environment = "MACOSX_DEPLOYMENT_TARGET=15.0 CCACHE_DIR=/tmp/ccache" before-build = "ccache -s && ccache -M 500M" before-all = """ export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 - brew install lua tbb boost@1.90 ccache - brew link boost@1.90 + brew install lua tbb boost@1.85 ccache + brew link boost@1.85 """ [tool.cibuildwheel.windows] From e203d40999932969800f7b24b99b6bf27d1e7712 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Tue, 21 Apr 2026 13:24:11 +0200 Subject: [PATCH 29/50] push clang diagnostic around boost crc header include --- pyproject.toml | 4 ++-- src/util/fingerprint.cpp | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 62b873e4c7..2cdf669678 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,8 +87,8 @@ environment = "MACOSX_DEPLOYMENT_TARGET=15.0 CCACHE_DIR=/tmp/ccache" before-build = "ccache -s && ccache -M 500M" before-all = """ export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 - brew install lua tbb boost@1.85 ccache - brew link boost@1.85 + brew install lua tbb boost@1.90 ccache + brew link boost@1.90 """ [tool.cibuildwheel.windows] diff --git a/src/util/fingerprint.cpp b/src/util/fingerprint.cpp index 50e0f973ac..4f49f51147 100644 --- a/src/util/fingerprint.cpp +++ b/src/util/fingerprint.cpp @@ -3,7 +3,20 @@ #include "util/exception_utils.hpp" #include "util/version.hpp" +// boost/crc.hpp's template body contains a UB shift (`remainder <<= CHAR_BIT` +// where remainder is 8 bits) that gets attributed to the instantiation site — +// i.e. user code — when boost's header is reached via an implicit system search +// path (macOS Intel brew at /usr/local/include). Clang's system-header +// suppression doesn't always catch template-instantiation warnings in that +// case, so silence it at the include itself. +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wshift-count-overflow" +#endif #include +#if defined(__clang__) +#pragma clang diagnostic pop +#endif #include From 5f713d0da5961921fa2f23e1ea2af154e6f8dc28 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Tue, 21 Apr 2026 13:28:51 +0200 Subject: [PATCH 30/50] trigger CI From 4ccad42b9b8feef2bd08f78ebf9780c6f947ddd2 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Tue, 21 Apr 2026 13:34:45 +0200 Subject: [PATCH 31/50] use ccache cache on osx & linux for cibuildwheel --- .github/workflows/osrm-backend.yml | 5 +++-- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 67a5d7419e..fe2e494f7d 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -669,8 +669,9 @@ jobs: package-dir: ${{ runner.os == 'Linux' && steps.sdist.outputs.path || '.' }} output-dir: dist env: - # /tmp/ccache can be cached too, esp useful for linux - CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /tmp/ccache:/ccache" + # Mount the workflow-cached ~/.ccache into the container so Linux + # wheel builds share the main-build ccache across runs. + CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /home/runner/.ccache:/ccache" CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" CIBW_CONFIG_SETTINGS_MACOS: "cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache" CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" diff --git a/pyproject.toml b/pyproject.toml index 2cdf669678..e7178176b3 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ before-build = "ccache -s && ccache -M 500M" repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" [tool.cibuildwheel.macos] -environment = "MACOSX_DEPLOYMENT_TARGET=15.0 CCACHE_DIR=/tmp/ccache" +environment = "MACOSX_DEPLOYMENT_TARGET=15.0 CCACHE_DIR=$HOME/.ccache" before-build = "ccache -s && ccache -M 500M" before-all = """ export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 From 25d54d401fed4517648460d4550fc92110927840 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Tue, 21 Apr 2026 14:04:34 +0200 Subject: [PATCH 32/50] try forcing brew headers to be -isystem --- CMakeLists.txt | 16 ++++++++++++++++ src/util/fingerprint.cpp | 13 ------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f4cbed7230..5bf1f8c9ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,6 +151,22 @@ include(CheckCXXCompilerFlag) include(FindPackageHandleStandardArgs) include(GNUInstallDirs) +# On macOS, brew-installed headers land in prefixes that clang may already +# search implicitly (/usr/local on Intel, /opt/homebrew on ARM in some +# configurations). When a path is implicit, CMake filters it out of +# include_directories(SYSTEM ...) — no -isystem is emitted — and clang's +# system-header warning suppression stops reliably catching template- +# instantiation diagnostics from headers under that prefix. Force -isystem +# via add_compile_options so brew-installed boost/lua/tbb headers are +# treated as system headers regardless of implicit status. +if(APPLE) + foreach(_brew_prefix /usr/local /opt/homebrew) + if(EXISTS "${_brew_prefix}/include") + add_compile_options(SHELL:-isystem ${_brew_prefix}/include) + endif() + endforeach() +endif() + include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}/include/) include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}/include/) include_directories(SYSTEM ${CMAKE_CURRENT_SOURCE_DIR}/generated/include/) diff --git a/src/util/fingerprint.cpp b/src/util/fingerprint.cpp index 4f49f51147..50e0f973ac 100644 --- a/src/util/fingerprint.cpp +++ b/src/util/fingerprint.cpp @@ -3,20 +3,7 @@ #include "util/exception_utils.hpp" #include "util/version.hpp" -// boost/crc.hpp's template body contains a UB shift (`remainder <<= CHAR_BIT` -// where remainder is 8 bits) that gets attributed to the instantiation site — -// i.e. user code — when boost's header is reached via an implicit system search -// path (macOS Intel brew at /usr/local/include). Clang's system-header -// suppression doesn't always catch template-instantiation warnings in that -// case, so silence it at the include itself. -#if defined(__clang__) -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wshift-count-overflow" -#endif #include -#if defined(__clang__) -#pragma clang diagnostic pop -#endif #include From f26f848c4123828e60ed41bbd79542cfe1ff26f4 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 10:39:15 +0200 Subject: [PATCH 33/50] try windows now --- .github/workflows/osrm-backend.yml | 193 +++++++++++++++++------------ 1 file changed, 111 insertions(+), 82 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index fe2e494f7d..675bd6011a 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -2,7 +2,7 @@ name: osrm-backend CI on: push: branches: - - master + - nn-py-bindings tags: - v* pull_request: @@ -22,75 +22,105 @@ concurrency: cancel-in-progress: true jobs: - # conan-windows-release-node: - # needs: format-taginfo-docs - # runs-on: windows-2025 - # continue-on-error: false - # env: - # BUILD_TYPE: Release - # steps: - # - uses: actions/checkout@v6 - # - run: cmake --version - # - uses: actions/setup-node@v6 - # with: - # node-version: 24 - # - run: node --version - # - run: npm --version - # - name: Prepare environment - # shell: bash - # run: | - # PACKAGE_JSON_VERSION=$(node -e "console.log(require('./package.json').version)") - # echo PUBLISH=$([[ "${GITHUB_REF:-}" == "refs/tags/v${PACKAGE_JSON_VERSION}" ]] && echo "On" || echo "Off") >> $GITHUB_ENV - # - run: npm install --ignore-scripts - # - run: npm link --ignore-scripts - # - name: Build - # shell: bash - # run: | - # mkdir build - # cd build - - # python3 -m venv .venv - # source .venv/Scripts/Activate - # python3 -m pip install conan==2.27.1 - # conan profile detect --force - - # cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_CONAN=ON -DENABLE_NODE_BINDINGS=ON .. - # cmake --build . --config Release - - # # TODO: MSVC goes out of memory when building our tests - # # - name: Run tests - # # shell: bash - # # run: | - # # cd build - # # cmake --build . --config Release --target tests - # # # TODO: run tests - # # - name: Run node tests - # # shell: bash - # # run: | - # # ./lib/binding_napi_v8/osrm-extract.exe -p profiles/car.lua test/data/monaco.osm.pbf - - # # mkdir -p test/data/ch - # # cp test/data/monaco.osrm* test/data/ch/ - # # ./lib/binding_napi_v8/osrm-contract.exe test/data/ch/monaco.osrm - - # # ./lib/binding_napi_v8/osrm-datastore.exe test/data/ch/monaco.osrm - # # node test/nodejs/index.js - # - name: Build Node package - # shell: bash - # run: ./scripts/ci/node_package.sh - # - name: Publish Node package - # if: ${{ env.PUBLISH == 'On' }} - # uses: ncipollo/release-action@v1 - # with: - # allowUpdates: true - # artifactErrorsFailBuild: true - # artifacts: build/stage/**/*.tar.gz - # omitBody: true - # omitBodyDuringUpdate: true - # omitName: true - # omitNameDuringUpdate: true - # replacesArtifacts: true - # token: ${{ secrets.GITHUB_TOKEN }} + conan-windows-release-node: + needs: format-taginfo-docs + strategy: + matrix: + os: [windows-2025] + runs-on: ${{ matrix.os }} + continue-on-error: false + env: + BUILD_TYPE: Release + steps: + - uses: actions/checkout@v6 + - run: cmake --version + - uses: actions/setup-node@v6 + with: + node-version: 24 + - run: node --version + - run: npm --version + - name: Prepare environment + shell: bash + run: | + PACKAGE_JSON_VERSION=$(node -e "console.log(require('./package.json').version)") + echo PUBLISH=$([[ "${GITHUB_REF:-}" == "refs/tags/v${PACKAGE_JSON_VERSION}" ]] && echo "On" || echo "Off") >> $GITHUB_ENV + - run: npm install --ignore-scripts + - run: npm link --ignore-scripts + - name: Restore Conan cache + id: conan-restore + uses: actions/cache/restore@v5 + with: + path: ~/.conan2 + key: v11-conan-${{ matrix.os }}-${{ hashFiles('conanfile.py') }} + restore-keys: | + v11-conan-${{ matrix.os }}- + - name: Build + shell: bash + run: | + mkdir build + cd build + + python3 -m venv .venv + source .venv/Scripts/Activate + python3 -m pip install conan==2.27.1 + conan profile detect --force + + cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_CONAN=ON -DENABLE_NODE_BINDINGS=ON .. + cmake --build . --config Release + - name: Save Conan cache + if: steps.build.outcome == 'success' && steps.conan-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ~/.conan2 + key: v11-conan-${{ matrix.name }}-${{ hashFiles('conanfile.py') }} + + # TODO: MSVC goes out of memory when building our tests + # - name: Run tests + # shell: bash + # run: | + # cd build + # cmake --build . --config Release --target tests + # # TODO: run tests + # - name: Run node tests + # shell: bash + # run: | + # ./lib/binding_napi_v8/osrm-extract.exe -p profiles/car.lua test/data/monaco.osm.pbf + + # mkdir -p test/data/ch + # cp test/data/monaco.osrm* test/data/ch/ + # ./lib/binding_napi_v8/osrm-contract.exe test/data/ch/monaco.osrm + + # ./lib/binding_napi_v8/osrm-datastore.exe test/data/ch/monaco.osrm + # node test/nodejs/index.js + # - name: Build Node package + # shell: bash + # run: ./scripts/ci/node_package.sh + - name: Publish Node package + if: ${{ env.PUBLISH == 'On' }} + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + artifactErrorsFailBuild: true + artifacts: build/stage/**/*.tar.gz + omitBody: true + omitBodyDuringUpdate: true + omitName: true + omitNameDuringUpdate: true + replacesArtifacts: true + token: ${{ secrets.GITHUB_TOKEN }} + - name: Run CIBuildWheel + if: github.event_name != 'pull_request' + uses: pypa/cibuildwheel@v3.4.0 + with: + output-dir: dist + env: + CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" + - name: Publish to TestPyPI + # if: ${{ env.BUILD_BINDINGS && env.PUBLISH == 'On' }} + uses: pypa/gh-action-pypi-publish@v1.13.0 + with: + repository-url: https://test.pypi.org/legacy/ + verbose: true format-taginfo-docs: runs-on: ubuntu-slim @@ -354,16 +384,16 @@ jobs: ENABLE_ASSERTIONS: ON ENABLE_CONAN: ON - - name: conan-macos-arm64-release-node - build_bindings: true - continue-on-error: true - node: 24 - runs-on: macos-15 # arm64 - BUILD_TYPE: Release - CCOMPILER: clang - CXXCOMPILER: clang++ - ENABLE_ASSERTIONS: ON - ENABLE_CONAN: ON + # - name: conan-macos-arm64-release-node + # build_bindings: true + # continue-on-error: true + # node: 24 + # runs-on: macos-15 # arm64 + # BUILD_TYPE: Release + # CCOMPILER: clang + # CXXCOMPILER: clang++ + # ENABLE_ASSERTIONS: ON + # ENABLE_CONAN: ON name: ${{ matrix.name}} continue-on-error: ${{ matrix.continue-on-error }} @@ -674,10 +704,9 @@ jobs: CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /home/runner/.ccache:/ccache" CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" CIBW_CONFIG_SETTINGS_MACOS: "cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache" - CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" - name: Publish to TestPyPI - if: ${{ env.BUILD_BINDINGS && env.PUBLISH == 'On' }} + # if: ${{ env.BUILD_BINDINGS && env.PUBLISH == 'On' }} uses: pypa/gh-action-pypi-publish@v1.13.0 with: repository-url: https://test.pypi.org/legacy/ From 1f3c2018511787d401915dc696939f2aa338bfca Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 12:11:13 +0200 Subject: [PATCH 34/50] try release on my fork --- .github/workflows/osrm-backend.yml | 39 ++++++++++++++------------- .github/workflows/release-monthly.yml | 29 +++++++++++++++++--- 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 675bd6011a..7bcc85fecd 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -1,13 +1,13 @@ name: osrm-backend CI on: - push: - branches: - - nn-py-bindings + # push: + # branches: + # - nn-py-bindings tags: - v* - pull_request: - branches: - - master + # pull_request: + # branches: + # - master workflow_dispatch: env: @@ -115,12 +115,13 @@ jobs: output-dir: dist env: CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" - - name: Publish to TestPyPI - # if: ${{ env.BUILD_BINDINGS && env.PUBLISH == 'On' }} - uses: pypa/gh-action-pypi-publish@v1.13.0 + - name: Upload Windows wheel + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 with: - repository-url: https://test.pypi.org/legacy/ - verbose: true + name: wheels-windows + path: dist/*.whl + if-no-files-found: error format-taginfo-docs: runs-on: ubuntu-slim @@ -398,9 +399,6 @@ jobs: name: ${{ matrix.name}} continue-on-error: ${{ matrix.continue-on-error }} runs-on: ${{ matrix.runs-on }} - # for PyPI trusted publisher - permissions: - id-token: write env: BUILD_TYPE: ${{ matrix.BUILD_TYPE }} BUILD_SHARED_LIBS: ${{ matrix.BUILD_SHARED_LIBS }} @@ -705,12 +703,15 @@ jobs: CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" CIBW_CONFIG_SETTINGS_MACOS: "cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache" - - name: Publish to TestPyPI - # if: ${{ env.BUILD_BINDINGS && env.PUBLISH == 'On' }} - uses: pypa/gh-action-pypi-publish@v1.13.0 + - name: Upload wheels and sdist + if: ${{ env.BUILD_BINDINGS && github.event_name != 'pull_request' }} + uses: actions/upload-artifact@v4 with: - repository-url: https://test.pypi.org/legacy/ - verbose: true + name: wheels-${{ matrix.name }} + path: | + dist/*.whl + dist/*.tar.gz + if-no-files-found: error - name: Show CCache statistics run: | diff --git a/.github/workflows/release-monthly.yml b/.github/workflows/release-monthly.yml index 12321d759b..39d02b822d 100644 --- a/.github/workflows/release-monthly.yml +++ b/.github/workflows/release-monthly.yml @@ -1,6 +1,9 @@ name: Monthly Release on: + push: + branches: + - nn-py-bindings schedule: # 1st of each month at 08:00 UTC - cron: '0 8 1 * *' @@ -177,6 +180,7 @@ jobs: prerelease: false - name: Wait for CI to complete + id: wait_ci env: GH_TOKEN: ${{ github.token }} run: | @@ -209,6 +213,7 @@ jobs: if [ "$STATUS" = "completed" ]; then if [ "$CONCLUSION" = "success" ]; then echo "✓ CI workflow completed successfully, proceeding with npm publish" + echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT" exit 0 else echo "✗ CI workflow completed with conclusion=$CONCLUSION (expected success)" @@ -228,9 +233,25 @@ jobs: echo "✗ Timed out waiting for CI workflow to complete for tag $TAG" exit 1 - - name: Install dependencies (skip native build scripts) - run: npm ci --ignore-scripts + - name: Download wheels from CI run + uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist + merge-multiple: true + run-id: ${{ steps.wait_ci.outputs.run_id }} + github-token: ${{ github.token }} + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@v1.13.0 + with: + skip-existing: true + verbose: true + repository-url: https://test.pypi.org/legacy/ + + # - name: Install dependencies (skip native build scripts) + # run: npm ci --ignore-scripts - - name: Publish to npm - run: npm publish + # - name: Publish to npm + # run: npm publish From b57cd7bed721050216d9d25807138e5940eef3f8 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 12:19:45 +0200 Subject: [PATCH 35/50] strip to only use python stuff and release that --- .github/workflows/osrm-backend.yml | 171 +++++++++++++++-------------- 1 file changed, 86 insertions(+), 85 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 7bcc85fecd..0977711ebe 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -3,8 +3,8 @@ on: # push: # branches: # - nn-py-bindings - tags: - - v* + tags: + - v* # pull_request: # branches: # - master @@ -55,6 +55,7 @@ jobs: restore-keys: | v11-conan-${{ matrix.os }}- - name: Build + id: build shell: bash run: | mkdir build @@ -95,19 +96,19 @@ jobs: # - name: Build Node package # shell: bash # run: ./scripts/ci/node_package.sh - - name: Publish Node package - if: ${{ env.PUBLISH == 'On' }} - uses: ncipollo/release-action@v1 - with: - allowUpdates: true - artifactErrorsFailBuild: true - artifacts: build/stage/**/*.tar.gz - omitBody: true - omitBodyDuringUpdate: true - omitName: true - omitNameDuringUpdate: true - replacesArtifacts: true - token: ${{ secrets.GITHUB_TOKEN }} + # - name: Publish Node package + # if: ${{ env.PUBLISH == 'On' }} + # uses: ncipollo/release-action@v1 + # with: + # allowUpdates: true + # artifactErrorsFailBuild: true + # artifacts: build/stage/**/*.tar.gz + # omitBody: true + # omitBodyDuringUpdate: true + # omitName: true + # omitNameDuringUpdate: true + # replacesArtifacts: true + # token: ${{ secrets.GITHUB_TOKEN }} - name: Run CIBuildWheel if: github.event_name != 'pull_request' uses: pypa/cibuildwheel@v3.4.0 @@ -586,7 +587,7 @@ jobs: env: Boost_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} - name: Save compiler cache - if: steps.build.outcome == 'success' && steps.ccache-restore.outputs.cache-hit != 'true' + if: steps.build.outcome == 'success' uses: actions/cache/save@v5 with: path: ~/.ccache @@ -597,77 +598,77 @@ jobs: with: path: ~/.conan2 key: v11-conan-${{ matrix.name }}-${{ hashFiles('conanfile.py') }} - - name: Run all tests - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY != 'ON' }} - run: | - make -C test/data benchmark - - # macOS SIP strips the linker path. Reset this inside the running shell - export LD_LIBRARY_PATH=${{ env.LD_LIBRARY_PATH }} - - # All tests assume to be run from the build directory - pushd ${OSRM_BUILD_DIR} - for i in ./unit_tests/*-tests ; do echo Running $i ; $i ; done - if [ -z "${ENABLE_SANITIZER}" ]; then - npm run nodejs-tests - fi - popd - npm test -- --parallel $JOBS - - - name: Use Node 22 - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - uses: actions/setup-node@v6 - with: - node-version: 22 - - name: Run Node package tests on Node 22 - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - run: | - node --version - npm run nodejs-tests - - name: Use Node 24 - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - uses: actions/setup-node@v6 - with: - node-version: 24 - - name: Run Node package tests on Node 24 - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - run: | - node --version - npm run nodejs-tests - - name: Use Node latest - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - uses: actions/setup-node@v6 - with: - node-version: latest - - name: Run Node package tests on Node-latest - if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - run: | - node --version - npm run nodejs-tests + # - name: Run all tests + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY != 'ON' }} + # run: | + # make -C test/data benchmark + + # # macOS SIP strips the linker path. Reset this inside the running shell + # export LD_LIBRARY_PATH=${{ env.LD_LIBRARY_PATH }} + + # # All tests assume to be run from the build directory + # pushd ${OSRM_BUILD_DIR} + # for i in ./unit_tests/*-tests ; do echo Running $i ; $i ; done + # if [ -z "${ENABLE_SANITIZER}" ]; then + # npm run nodejs-tests + # fi + # popd + # npm test -- --parallel $JOBS + + # - name: Use Node 22 + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + # uses: actions/setup-node@v6 + # with: + # node-version: 22 + # - name: Run Node package tests on Node 22 + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + # run: | + # node --version + # npm run nodejs-tests + # - name: Use Node 24 + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + # uses: actions/setup-node@v6 + # with: + # node-version: 24 + # - name: Run Node package tests on Node 24 + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + # run: | + # node --version + # npm run nodejs-tests + # - name: Use Node latest + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + # uses: actions/setup-node@v6 + # with: + # node-version: latest + # - name: Run Node package tests on Node-latest + # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + # run: | + # node --version + # npm run nodejs-tests - - name: Upload test logs - uses: actions/upload-artifact@v6 - if: failure() - with: - name: logs - path: test/logs/ + # - name: Upload test logs + # uses: actions/upload-artifact@v6 + # if: failure() + # with: + # name: logs + # path: test/logs/ - - name: Build Node package - if: ${{ env.BUILD_BINDINGS }} - run: ./scripts/ci/node_package.sh - - name: Publish Node package - if: ${{ env.BUILD_BINDINGS && env.PUBLISH == 'On' }} - uses: ncipollo/release-action@v1 - with: - allowUpdates: true - artifactErrorsFailBuild: true - artifacts: build/stage/**/*.tar.gz - omitBody: true - omitBodyDuringUpdate: true - omitName: true - omitNameDuringUpdate: true - replacesArtifacts: true - token: ${{ secrets.GITHUB_TOKEN }} + # - name: Build Node package + # if: ${{ env.BUILD_BINDINGS }} + # run: ./scripts/ci/node_package.sh + # - name: Publish Node package + # if: ${{ env.BUILD_BINDINGS && env.PUBLISH == 'On' }} + # uses: ncipollo/release-action@v1 + # with: + # allowUpdates: true + # artifactErrorsFailBuild: true + # artifacts: build/stage/**/*.tar.gz + # omitBody: true + # omitBodyDuringUpdate: true + # omitName: true + # omitNameDuringUpdate: true + # replacesArtifacts: true + # token: ${{ secrets.GITHUB_TOKEN }} # Python bindings From 54b93473558e17bcb2eb88e6ebec6be070a6f12c Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 12:28:06 +0200 Subject: [PATCH 36/50] use workflows from this branch --- .github/workflows/release-monthly.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-monthly.yml b/.github/workflows/release-monthly.yml index 39d02b822d..6e95a53909 100644 --- a/.github/workflows/release-monthly.yml +++ b/.github/workflows/release-monthly.yml @@ -14,10 +14,9 @@ on: required: false type: string branch: - description: 'Branch to release from (defaults to master)' + description: 'Branch to release from (defaults to the ref that triggered this workflow)' required: false type: string - default: 'master' concurrency: group: release-monthly-${{ github.ref }} @@ -33,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v6 with: - ref: ${{ inputs.branch || 'master' }} + ref: ${{ inputs.branch || github.ref_name }} fetch-depth: 0 - uses: actions/setup-node@v6 @@ -53,7 +52,7 @@ jobs: fi # Validate branch input (prevent shell injection) - BRANCH="${{ inputs.branch || 'master' }}" + BRANCH="${{ inputs.branch || github.ref_name }}" if ! echo "$BRANCH" | grep -E '^[a-zA-Z0-9._/-]+$' > /dev/null; then echo "Error: branch name contains invalid characters" exit 1 @@ -135,7 +134,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.BACKEND_RELEASE_TOKEN }} run: | - BRANCH="${{ inputs.branch || 'master' }}" + BRANCH="${{ inputs.branch || github.ref_name }}" git push origin "$BRANCH" git push origin "${{ steps.version.outputs.tag }}" From 7eed6d3b56cf9bad4018a5f6ebe9b2f69effcf98 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Apr 2026 10:28:25 +0000 Subject: [PATCH 37/50] chore: bump version to 26.4.4 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index e95a8f3cc4..d95a687afc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@project-osrm/osrm", - "version": "26.4.1", + "version": "26.4.4", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 22f88fe929..0fead05965 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@project-osrm/osrm", - "version": "26.4.1", + "version": "26.4.4", "private": false, "type": "module", "description": "The Open Source Routing Machine is a high performance routing engine written in C++ designed to run on OpenStreetMap data.", From ddd0a440d204a80d0065404823528a913fc33de9 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 12:30:30 +0200 Subject: [PATCH 38/50] oops --- .github/workflows/osrm-backend.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 0977711ebe..5554417bcf 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -1,10 +1,8 @@ name: osrm-backend CI on: - # push: - # branches: - # - nn-py-bindings - tags: - - v* + push: + tags: + - v* # pull_request: # branches: # - master From a1ba443d547a823508d2df57ff8a2ed08b6e2025 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Apr 2026 10:31:17 +0000 Subject: [PATCH 39/50] chore: bump version to 26.4.5 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index d95a687afc..2191dd965a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@project-osrm/osrm", - "version": "26.4.4", + "version": "26.4.5", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 0fead05965..81562f8e97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@project-osrm/osrm", - "version": "26.4.4", + "version": "26.4.5", "private": false, "type": "module", "description": "The Open Source Routing Machine is a high performance routing engine written in C++ designed to run on OpenStreetMap data.", From 17174d1426ea4e08c7b6243bc295a72c26d58e95 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 13:46:59 +0200 Subject: [PATCH 40/50] revert CI testing stuff --- .github/workflows/osrm-backend.yml | 562 +++++++++++++------------- .github/workflows/release-monthly.yml | 48 ++- 2 files changed, 315 insertions(+), 295 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 5554417bcf..913f463f54 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -1,11 +1,13 @@ name: osrm-backend CI on: push: + branches: + - master tags: - v* - # pull_request: - # branches: - # - master + pull_request: + branches: + - master workflow_dispatch: env: @@ -115,7 +117,7 @@ jobs: env: CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" - name: Upload Windows wheel - if: github.event_name != 'pull_request' + if: startsWith(github.ref, 'refs/tags/v') uses: actions/upload-artifact@v4 with: name: wheels-windows @@ -158,189 +160,189 @@ jobs: npm run docs && ./scripts/error_on_dirty.sh "Run 'npm run docs' locally and commit the changes." npm audit --production - # docker-image-matrix: - # strategy: - # matrix: - # docker-base-image: ['debian', 'alpine'] - # needs: format-taginfo-docs - # runs-on: ubuntu-22.04 - # continue-on-error: false - # steps: - # - name: Check out the repo - # uses: actions/checkout@v6 - # - name: Enable osm.pbf cache - # uses: actions/cache@v5 - # with: - # path: berlin-latest.osm.pbf - # key: v1-berlin-osm-pbf - # restore-keys: | - # v1-berlin-osm-pbf - # - name: Docker build - # run: | - # docker build -t osrm-backend-local -f docker/Dockerfile-${{ matrix.docker-base-image }} . - # - name: Test Docker image - # run: | - # if [ ! -f "${PWD}/berlin-latest.osm.pbf" ]; then - # wget http://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf - # fi - # TAG=osrm-backend-local - # # when `--memory-swap` value equals `--memory` it means container won't use swap - # # see https://docs.docker.com/config/containers/resource_constraints/#--memory-swap-details - # MEMORY_ARGS="--memory=1g --memory-swap=1g" - # docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-extract --dump-nbg-graph -p /opt/car.lua /data/berlin-latest.osm.pbf - # docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-components /data/berlin-latest.osrm.nbg /data/berlin-latest.geojson - # if [ ! -s "${PWD}/berlin-latest.geojson" ] - # then - # >&2 echo "No berlin-latest.geojson found" - # exit 1 - # fi - # # removing `.osrm.nbg` to check that whole pipeline works without it - # rm -rf "${PWD}/berlin-latest.osrm.nbg" - - # docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-partition /data/berlin-latest.osrm - # docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-customize /data/berlin-latest.osrm - # docker run $MEMORY_ARGS --name=osrm-container -t -p 5000:5000 -v "${PWD}:/data" "${TAG}" osrm-routed --algorithm mld /data/berlin-latest.osrm & - # curl --retry-delay 3 --retry 10 --retry-all-errors "http://127.0.0.1:5000/route/v1/driving/13.388860,52.517037;13.385983,52.496891?steps=true" - # docker stop osrm-container + docker-image-matrix: + strategy: + matrix: + docker-base-image: ['debian', 'alpine'] + needs: format-taginfo-docs + runs-on: ubuntu-22.04 + continue-on-error: false + steps: + - name: Check out the repo + uses: actions/checkout@v6 + - name: Enable osm.pbf cache + uses: actions/cache@v5 + with: + path: berlin-latest.osm.pbf + key: v1-berlin-osm-pbf + restore-keys: | + v1-berlin-osm-pbf + - name: Docker build + run: | + docker build -t osrm-backend-local -f docker/Dockerfile-${{ matrix.docker-base-image }} . + - name: Test Docker image + run: | + if [ ! -f "${PWD}/berlin-latest.osm.pbf" ]; then + wget http://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf + fi + TAG=osrm-backend-local + # when `--memory-swap` value equals `--memory` it means container won't use swap + # see https://docs.docker.com/config/containers/resource_constraints/#--memory-swap-details + MEMORY_ARGS="--memory=1g --memory-swap=1g" + docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-extract --dump-nbg-graph -p /opt/car.lua /data/berlin-latest.osm.pbf + docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-components /data/berlin-latest.osrm.nbg /data/berlin-latest.geojson + if [ ! -s "${PWD}/berlin-latest.geojson" ] + then + >&2 echo "No berlin-latest.geojson found" + exit 1 + fi + # removing `.osrm.nbg` to check that whole pipeline works without it + rm -rf "${PWD}/berlin-latest.osrm.nbg" + + docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-partition /data/berlin-latest.osrm + docker run $MEMORY_ARGS -t -v "${PWD}:/data" "${TAG}" osrm-customize /data/berlin-latest.osrm + docker run $MEMORY_ARGS --name=osrm-container -t -p 5000:5000 -v "${PWD}:/data" "${TAG}" osrm-routed --algorithm mld /data/berlin-latest.osrm & + curl --retry-delay 3 --retry 10 --retry-all-errors "http://127.0.0.1:5000/route/v1/driving/13.388860,52.517037;13.385983,52.496891?steps=true" + docker stop osrm-container build-matrix: needs: format-taginfo-docs strategy: matrix: include: - # - name: clang-20-release - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Release - # CCOMPILER: clang-20 - # CXXCOMPILER: clang++-20 - # ENABLE_LTO: OFF - - # - name: clang-19-release - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Release - # CCOMPILER: clang-19 - # CXXCOMPILER: clang++-19 - # ENABLE_LTO: OFF - - # - name: clang-18-release - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Release - # CCOMPILER: clang-18 - # CXXCOMPILER: clang++-18 - # ENABLE_LTO: OFF - - # - name: clang-18-debug - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Debug - # CCOMPILER: clang-18 - # CXXCOMPILER: clang++-18 - # ENABLE_LTO: OFF - - # - name: clang-18-debug-clang-tidy - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Debug - # CCOMPILER: clang-18 - # CXXCOMPILER: clang++-18 - # ENABLE_CLANG_TIDY: ON - # NODE_PACKAGE_TESTS_ONLY: ON - # ENABLE_LTO: OFF - - # - name: clang-18-debug-asan-ubsan - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Debug - # CCOMPILER: clang-18 - # CXXCOMPILER: clang++-18 - # ENABLE_SANITIZER: ON - # TARGET_ARCH: x86_64-asan-ubsan - # OSRM_CONNECTION_RETRIES: 10 - # OSRM_CONNECTION_EXP_BACKOFF_COEF: 1.5 - - # - name: clang-17-release - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Release - # CCOMPILER: clang-17 - # CXXCOMPILER: clang++-17 - # ENABLE_LTO: OFF - - # - name: clang-16-release - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Release - # CCOMPILER: clang-16 - # CXXCOMPILER: clang++-16 - # ENABLE_LTO: OFF - - # - name: gcc-14-release - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Release - # CCOMPILER: gcc-14 - # CXXCOMPILER: g++-14 - # CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' - - # - name: gcc-13-release - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Release - # CCOMPILER: gcc-13 - # CXXCOMPILER: g++-13 - # CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' - - # - name: gcc-13-debug-cov - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Debug - # CCOMPILER: gcc-13 - # CXXCOMPILER: g++-13 - # ENABLE_COVERAGE: ON - - # - name: gcc-12-release - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-22.04 - # BUILD_TYPE: Release - # CCOMPILER: gcc-12 - # CXXCOMPILER: g++-12 - # CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' - - # - name: conan-linux-release - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Release - # CCOMPILER: clang-18 - # CXXCOMPILER: clang++-18 - # ENABLE_CONAN: ON - # ENABLE_LTO: OFF - - # - name: conan-linux-debug-asan-ubsan - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Release - # CCOMPILER: clang-18 - # CXXCOMPILER: clang++-18 - # ENABLE_CONAN: ON - # ENABLE_SANITIZER: ON - # ENABLE_LTO: OFF + - name: clang-20-release + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Release + CCOMPILER: clang-20 + CXXCOMPILER: clang++-20 + ENABLE_LTO: OFF + + - name: clang-19-release + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Release + CCOMPILER: clang-19 + CXXCOMPILER: clang++-19 + ENABLE_LTO: OFF + + - name: clang-18-release + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Release + CCOMPILER: clang-18 + CXXCOMPILER: clang++-18 + ENABLE_LTO: OFF + + - name: clang-18-debug + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Debug + CCOMPILER: clang-18 + CXXCOMPILER: clang++-18 + ENABLE_LTO: OFF + + - name: clang-18-debug-clang-tidy + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Debug + CCOMPILER: clang-18 + CXXCOMPILER: clang++-18 + ENABLE_CLANG_TIDY: ON + NODE_PACKAGE_TESTS_ONLY: ON + ENABLE_LTO: OFF + + - name: clang-18-debug-asan-ubsan + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Debug + CCOMPILER: clang-18 + CXXCOMPILER: clang++-18 + ENABLE_SANITIZER: ON + TARGET_ARCH: x86_64-asan-ubsan + OSRM_CONNECTION_RETRIES: 10 + OSRM_CONNECTION_EXP_BACKOFF_COEF: 1.5 + + - name: clang-17-release + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Release + CCOMPILER: clang-17 + CXXCOMPILER: clang++-17 + ENABLE_LTO: OFF + + - name: clang-16-release + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Release + CCOMPILER: clang-16 + CXXCOMPILER: clang++-16 + ENABLE_LTO: OFF + + - name: gcc-14-release + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Release + CCOMPILER: gcc-14 + CXXCOMPILER: g++-14 + CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' + + - name: gcc-13-release + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Release + CCOMPILER: gcc-13 + CXXCOMPILER: g++-13 + CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' + + - name: gcc-13-debug-cov + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Debug + CCOMPILER: gcc-13 + CXXCOMPILER: g++-13 + ENABLE_COVERAGE: ON + + - name: gcc-12-release + continue-on-error: false + node: 24 + runs-on: ubuntu-22.04 + BUILD_TYPE: Release + CCOMPILER: gcc-12 + CXXCOMPILER: g++-12 + CXXFLAGS: '-Wno-array-bounds -Wno-uninitialized' + + - name: conan-linux-release + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Release + CCOMPILER: clang-18 + CXXCOMPILER: clang++-18 + ENABLE_CONAN: ON + ENABLE_LTO: OFF + + - name: conan-linux-debug-asan-ubsan + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Release + CCOMPILER: clang-18 + CXXCOMPILER: clang++-18 + ENABLE_CONAN: ON + ENABLE_SANITIZER: ON + ENABLE_LTO: OFF - name: conan-linux-release-node build_bindings: true @@ -353,25 +355,25 @@ jobs: ENABLE_CONAN: ON NODE_PACKAGE_TESTS_ONLY: ON - # - name: conan-linux-debug-node - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04 - # BUILD_TYPE: Debug - # CCOMPILER: clang-16 - # CXXCOMPILER: clang++-16 - # ENABLE_CONAN: ON - # NODE_PACKAGE_TESTS_ONLY: ON - - # - name: conan-linux-arm64-release - # continue-on-error: false - # node: 24 - # runs-on: ubuntu-24.04-arm - # BUILD_TYPE: Release - # CCOMPILER: clang-18 - # CXXCOMPILER: clang++-18 - # ENABLE_CONAN: ON - # ENABLE_LTO: OFF + - name: conan-linux-debug-node + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04 + BUILD_TYPE: Debug + CCOMPILER: clang-16 + CXXCOMPILER: clang++-16 + ENABLE_CONAN: ON + NODE_PACKAGE_TESTS_ONLY: ON + + - name: conan-linux-arm64-release + continue-on-error: false + node: 24 + runs-on: ubuntu-24.04-arm + BUILD_TYPE: Release + CCOMPILER: clang-18 + CXXCOMPILER: clang++-18 + ENABLE_CONAN: ON + ENABLE_LTO: OFF - name: conan-macos-x64-release-node build_bindings: true @@ -384,16 +386,16 @@ jobs: ENABLE_ASSERTIONS: ON ENABLE_CONAN: ON - # - name: conan-macos-arm64-release-node - # build_bindings: true - # continue-on-error: true - # node: 24 - # runs-on: macos-15 # arm64 - # BUILD_TYPE: Release - # CCOMPILER: clang - # CXXCOMPILER: clang++ - # ENABLE_ASSERTIONS: ON - # ENABLE_CONAN: ON + - name: conan-macos-arm64-release-node + build_bindings: true + continue-on-error: true + node: 24 + runs-on: macos-15 # arm64 + BUILD_TYPE: Release + CCOMPILER: clang + CXXCOMPILER: clang++ + ENABLE_ASSERTIONS: ON + ENABLE_CONAN: ON name: ${{ matrix.name}} continue-on-error: ${{ matrix.continue-on-error }} @@ -596,77 +598,77 @@ jobs: with: path: ~/.conan2 key: v11-conan-${{ matrix.name }}-${{ hashFiles('conanfile.py') }} - # - name: Run all tests - # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY != 'ON' }} - # run: | - # make -C test/data benchmark - - # # macOS SIP strips the linker path. Reset this inside the running shell - # export LD_LIBRARY_PATH=${{ env.LD_LIBRARY_PATH }} - - # # All tests assume to be run from the build directory - # pushd ${OSRM_BUILD_DIR} - # for i in ./unit_tests/*-tests ; do echo Running $i ; $i ; done - # if [ -z "${ENABLE_SANITIZER}" ]; then - # npm run nodejs-tests - # fi - # popd - # npm test -- --parallel $JOBS - - # - name: Use Node 22 - # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - # uses: actions/setup-node@v6 - # with: - # node-version: 22 - # - name: Run Node package tests on Node 22 - # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - # run: | - # node --version - # npm run nodejs-tests - # - name: Use Node 24 - # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - # uses: actions/setup-node@v6 - # with: - # node-version: 24 - # - name: Run Node package tests on Node 24 - # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - # run: | - # node --version - # npm run nodejs-tests - # - name: Use Node latest - # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - # uses: actions/setup-node@v6 - # with: - # node-version: latest - # - name: Run Node package tests on Node-latest - # if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} - # run: | - # node --version - # npm run nodejs-tests + - name: Run all tests + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY != 'ON' }} + run: | + make -C test/data benchmark - # - name: Upload test logs - # uses: actions/upload-artifact@v6 - # if: failure() - # with: - # name: logs - # path: test/logs/ + # macOS SIP strips the linker path. Reset this inside the running shell + export LD_LIBRARY_PATH=${{ env.LD_LIBRARY_PATH }} - # - name: Build Node package - # if: ${{ env.BUILD_BINDINGS }} - # run: ./scripts/ci/node_package.sh - # - name: Publish Node package - # if: ${{ env.BUILD_BINDINGS && env.PUBLISH == 'On' }} - # uses: ncipollo/release-action@v1 - # with: - # allowUpdates: true - # artifactErrorsFailBuild: true - # artifacts: build/stage/**/*.tar.gz - # omitBody: true - # omitBodyDuringUpdate: true - # omitName: true - # omitNameDuringUpdate: true - # replacesArtifacts: true - # token: ${{ secrets.GITHUB_TOKEN }} + # All tests assume to be run from the build directory + pushd ${OSRM_BUILD_DIR} + for i in ./unit_tests/*-tests ; do echo Running $i ; $i ; done + if [ -z "${ENABLE_SANITIZER}" ]; then + npm run nodejs-tests + fi + popd + npm test -- --parallel $JOBS + + - name: Use Node 22 + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + uses: actions/setup-node@v6 + with: + node-version: 22 + - name: Run Node package tests on Node 22 + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + run: | + node --version + npm run nodejs-tests + - name: Use Node 24 + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + uses: actions/setup-node@v6 + with: + node-version: 24 + - name: Run Node package tests on Node 24 + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + run: | + node --version + npm run nodejs-tests + - name: Use Node latest + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + uses: actions/setup-node@v6 + with: + node-version: latest + - name: Run Node package tests on Node-latest + if: ${{ matrix.NODE_PACKAGE_TESTS_ONLY == 'ON' }} + run: | + node --version + npm run nodejs-tests + + - name: Upload test logs + uses: actions/upload-artifact@v6 + if: failure() + with: + name: logs + path: test/logs/ + + - name: Build Node package + if: ${{ env.BUILD_BINDINGS }} + run: ./scripts/ci/node_package.sh + - name: Publish Node package + if: ${{ env.BUILD_BINDINGS && env.PUBLISH == 'On' }} + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + artifactErrorsFailBuild: true + artifacts: build/stage/**/*.tar.gz + omitBody: true + omitBodyDuringUpdate: true + omitName: true + omitNameDuringUpdate: true + replacesArtifacts: true + token: ${{ secrets.GITHUB_TOKEN }} # Python bindings @@ -703,7 +705,7 @@ jobs: CIBW_CONFIG_SETTINGS_MACOS: "cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache" - name: Upload wheels and sdist - if: ${{ env.BUILD_BINDINGS && github.event_name != 'pull_request' }} + if: ${{ env.BUILD_BINDINGS && startsWith(github.ref, 'refs/tags/v') }} uses: actions/upload-artifact@v4 with: name: wheels-${{ matrix.name }} diff --git a/.github/workflows/release-monthly.yml b/.github/workflows/release-monthly.yml index 6e95a53909..9047ae371e 100644 --- a/.github/workflows/release-monthly.yml +++ b/.github/workflows/release-monthly.yml @@ -1,9 +1,6 @@ name: Monthly Release on: - push: - branches: - - nn-py-bindings schedule: # 1st of each month at 08:00 UTC - cron: '0 8 1 * *' @@ -14,9 +11,10 @@ on: required: false type: string branch: - description: 'Branch to release from (defaults to the ref that triggered this workflow)' + description: 'Branch to release from (defaults to master)' required: false type: string + default: 'master' concurrency: group: release-monthly-${{ github.ref }} @@ -27,12 +25,15 @@ jobs: runs-on: ubuntu-latest permissions: contents: write - id-token: write actions: write + outputs: + tag: ${{ steps.version.outputs.tag }} + version: ${{ steps.version.outputs.version }} + run_id: ${{ steps.wait_ci.outputs.run_id }} steps: - uses: actions/checkout@v6 with: - ref: ${{ inputs.branch || github.ref_name }} + ref: ${{ inputs.branch || 'master' }} fetch-depth: 0 - uses: actions/setup-node@v6 @@ -52,7 +53,7 @@ jobs: fi # Validate branch input (prevent shell injection) - BRANCH="${{ inputs.branch || github.ref_name }}" + BRANCH="${{ inputs.branch || 'master' }}" if ! echo "$BRANCH" | grep -E '^[a-zA-Z0-9._/-]+$' > /dev/null; then echo "Error: branch name contains invalid characters" exit 1 @@ -134,7 +135,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.BACKEND_RELEASE_TOKEN }} run: | - BRANCH="${{ inputs.branch || github.ref_name }}" + BRANCH="${{ inputs.branch || 'master' }}" git push origin "$BRANCH" git push origin "${{ steps.version.outputs.tag }}" @@ -232,13 +233,36 @@ jobs: echo "✗ Timed out waiting for CI workflow to complete for tag $TAG" exit 1 + + publish: + needs: release + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ needs.release.outputs.tag }} + + - uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies (skip native build scripts) + run: npm ci --ignore-scripts + + - name: Publish to npm + run: npm publish + - name: Download wheels from CI run uses: actions/download-artifact@v4 with: pattern: wheels-* path: dist merge-multiple: true - run-id: ${{ steps.wait_ci.outputs.run_id }} + run-id: ${{ needs.release.outputs.run_id }} github-token: ${{ github.token }} - name: Publish to PyPI @@ -248,9 +272,3 @@ jobs: verbose: true repository-url: https://test.pypi.org/legacy/ - # - name: Install dependencies (skip native build scripts) - # run: npm ci --ignore-scripts - - # - name: Publish to npm - # run: npm publish - From 6bacda31dd830ff4bbc5f3d1b0e41a82db7a0887 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 14:02:11 +0200 Subject: [PATCH 41/50] revert js package*.json --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2191dd965a..46b77bf296 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@project-osrm/osrm", - "version": "26.4.5", + "version": "26.4.0", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 81562f8e97..01baeccfda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@project-osrm/osrm", - "version": "26.4.5", + "version": "26.4.0", "private": false, "type": "module", "description": "The Open Source Routing Machine is a high performance routing engine written in C++ designed to run on OpenStreetMap data.", From b08cf08f613f2d5d7db4c23af33f9d6b525be981 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 14:03:23 +0200 Subject: [PATCH 42/50] add other yml --- .../workflows-disabled/python-bindings.yml | 137 ------------------ .../osrm-backend-docker.yml | 0 .../stale.yml | 0 3 files changed, 137 deletions(-) delete mode 100644 .github/workflows-disabled/python-bindings.yml rename .github/{workflows-disabled => workflows}/osrm-backend-docker.yml (100%) rename .github/{workflows-disabled => workflows}/stale.yml (100%) diff --git a/.github/workflows-disabled/python-bindings.yml b/.github/workflows-disabled/python-bindings.yml deleted file mode 100644 index 1cb46f9844..0000000000 --- a/.github/workflows-disabled/python-bindings.yml +++ /dev/null @@ -1,137 +0,0 @@ -name: Python Bindings - -on: - push: - branches: - - nn-py-bindings - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - build_sdist: - name: Build sdist - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - fetch-tags: true - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Build sdist - run: | - python -m pip install build - python -m build --sdist - echo "=== sdist produced ===" - ls -la dist/ - - - name: Upload sdist - uses: actions/upload-artifact@v4 - with: - name: sdist - path: dist/*.tar.gz - - build_wheels: - name: Build - cp312, ${{ matrix.name }} - needs: [build_sdist] - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: - include: - - name: Linux x86_64 - runner: ubuntu-latest - - name: Linux aarch64 - runner: ubuntu-24.04-arm - - name: macOS arm64 - runner: macos-14 - - name: Windows amd64 - runner: windows-latest - - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - fetch-tags: true - - - name: Download sdist - if: runner.os == 'Linux' - uses: actions/download-artifact@v4 - with: - name: sdist - path: dist - - - name: Locate sdist - if: runner.os == 'Linux' - id: sdist - shell: bash - run: echo "path=$(ls dist/*.tar.gz | head -n1)" >> "$GITHUB_OUTPUT" - - - name: Restore Conan cache - id: conan-cache - if: runner.os == 'Windows' - uses: actions/cache/restore@v5 - with: - path: ~/.conan2 - key: v4-conan-${{ runner.os }}-${{ hashFiles('pyproject.toml') }} - restore-keys: | - v4-conan-${{ runner.os }}- - - - name: Run cibuildwheel - uses: pypa/cibuildwheel@v3.4.0 - with: - # for linux build the wheel from the sdist - package-dir: ${{ runner.os == 'Linux' && steps.sdist.outputs.path || '.' }} - env: - CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /tmp/ccache:/ccache" - CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/usr/local/lib64:${LD_LIBRARY_PATH} CCACHE_DIR=/ccache" - CIBW_CONFIG_SETTINGS_MACOS: "cmake.define.CMAKE_CXX_COMPILER_LAUNCHER=ccache cmake.define.CMAKE_C_COMPILER_LAUNCHER=ccache" - CIBW_CONFIG_SETTINGS_WINDOWS: "cmake.define.ENABLE_CONAN=ON" - - - name: Save Conan cache - uses: actions/cache/save@v5 - if: "!cancelled() && runner.os == 'Windows' && steps.conan-cache.outputs.cache-hit != 'true'" - with: - path: ~/.conan2 - key: v4-conan-${{ runner.os }}-${{ hashFiles('pyproject.toml') }} - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-${{ matrix.name }} - path: wheelhouse/*.whl - - publish_test_pypi: - name: Publish to TestPyPI - needs: [build_wheels] - runs-on: ubuntu-latest - permissions: - id-token: write - steps: - - name: Download wheels - uses: actions/download-artifact@v4 - with: - path: dist - pattern: wheels-* - merge-multiple: true - - - name: Download sdist - uses: actions/download-artifact@v4 - with: - name: sdist - path: dist - - - name: Publish to TestPyPI - uses: pypa/gh-action-pypi-publish@v1.13.0 - with: - repository-url: https://test.pypi.org/legacy/ - verbose: true diff --git a/.github/workflows-disabled/osrm-backend-docker.yml b/.github/workflows/osrm-backend-docker.yml similarity index 100% rename from .github/workflows-disabled/osrm-backend-docker.yml rename to .github/workflows/osrm-backend-docker.yml diff --git a/.github/workflows-disabled/stale.yml b/.github/workflows/stale.yml similarity index 100% rename from .github/workflows-disabled/stale.yml rename to .github/workflows/stale.yml From d58f1464134d4cfa77dacafe505203b5db032e67 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 14:06:11 +0200 Subject: [PATCH 43/50] more uncommenting --- .github/workflows/osrm-backend.yml | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index 913f463f54..edc8c59572 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -93,22 +93,22 @@ jobs: # ./lib/binding_napi_v8/osrm-datastore.exe test/data/ch/monaco.osrm # node test/nodejs/index.js - # - name: Build Node package - # shell: bash - # run: ./scripts/ci/node_package.sh - # - name: Publish Node package - # if: ${{ env.PUBLISH == 'On' }} - # uses: ncipollo/release-action@v1 - # with: - # allowUpdates: true - # artifactErrorsFailBuild: true - # artifacts: build/stage/**/*.tar.gz - # omitBody: true - # omitBodyDuringUpdate: true - # omitName: true - # omitNameDuringUpdate: true - # replacesArtifacts: true - # token: ${{ secrets.GITHUB_TOKEN }} + - name: Build Node package + shell: bash + run: ./scripts/ci/node_package.sh + - name: Publish Node package + if: ${{ env.PUBLISH == 'On' }} + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + artifactErrorsFailBuild: true + artifacts: build/stage/**/*.tar.gz + omitBody: true + omitBodyDuringUpdate: true + omitName: true + omitNameDuringUpdate: true + replacesArtifacts: true + token: ${{ secrets.GITHUB_TOKEN }} - name: Run CIBuildWheel if: github.event_name != 'pull_request' uses: pypa/cibuildwheel@v3.4.0 From f86e06269b234d867f42a82703e98ad5699dfbcc Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 14:20:17 +0200 Subject: [PATCH 44/50] last fixes --- .github/workflows/osrm-backend.yml | 2 +- .github/workflows/release-monthly.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index edc8c59572..c860c243ae 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -721,6 +721,6 @@ jobs: ci-complete: runs-on: ubuntu-latest - needs: [build-matrix] + needs: [build-matrix, conan-windows-release-node, docker-image-matrix] steps: - run: echo "CI complete" diff --git a/.github/workflows/release-monthly.yml b/.github/workflows/release-monthly.yml index 9047ae371e..ee1ba23809 100644 --- a/.github/workflows/release-monthly.yml +++ b/.github/workflows/release-monthly.yml @@ -25,6 +25,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + id-token: write actions: write outputs: tag: ${{ steps.version.outputs.tag }} From b361809472be5e4cf95af077d39be0b204a414db Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 14:24:25 +0200 Subject: [PATCH 45/50] add maintenancen commnet --- AGENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENT.md b/AGENT.md index fa6ff0179c..ade3a61fee 100644 --- a/AGENT.md +++ b/AGENT.md @@ -114,6 +114,7 @@ Python bindings live under `src/python/` using nanobind + scikit-build-core. - `__main__.py` finds executables: `osrm/bin/` (wheel) → `build/*/` (editable) → PATH - Type stubs (`osrm_ext.pyi`) auto-generated by `nanobind_add_stub()`; rebuild + commit after C++ changes - C++ formatted by project clang-format; Python by ruff (both via `.pre-commit-config.yaml`) +- When changing the Python binding API (function signatures, classes, exposed attributes, parameter semantics), update [docs/python/api.md](docs/python/api.md) in the same change ### Building & testing From 9ab861db7d95d5e3de71a24b7073e57eec330353 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 14:43:22 +0200 Subject: [PATCH 46/50] update development.md --- AGENT.md | 1 + docs/python/development.md | 73 +++++++++++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/AGENT.md b/AGENT.md index ade3a61fee..40d27fe9a8 100644 --- a/AGENT.md +++ b/AGENT.md @@ -115,6 +115,7 @@ Python bindings live under `src/python/` using nanobind + scikit-build-core. - Type stubs (`osrm_ext.pyi`) auto-generated by `nanobind_add_stub()`; rebuild + commit after C++ changes - C++ formatted by project clang-format; Python by ruff (both via `.pre-commit-config.yaml`) - When changing the Python binding API (function signatures, classes, exposed attributes, parameter semantics), update [docs/python/api.md](docs/python/api.md) in the same change +- When changing the build, packaging, or release flow (pyproject.toml, CMake options, cibuildwheel config, `.github/workflows/release-monthly.yml`, `.github/workflows/osrm-backend.yml` Python steps), update [docs/python/development.md](docs/python/development.md) in the same change ### Building & testing diff --git a/docs/python/development.md b/docs/python/development.md index 00226534a7..8522eb3a8a 100644 --- a/docs/python/development.md +++ b/docs/python/development.md @@ -2,13 +2,17 @@ ## Installing for production -Pre-built wheels are published to PyPI for Linux (x86\_64, aarch64), macOS (arm64), and Windows (amd64), requiring Python 3.12+: +Pre-built wheels are published to PyPI for Linux (x86\_64), macOS (x86\_64), +and Windows (amd64). They use the CPython 3.12 stable ABI (`cp312-abi3`) and +therefore install on Python 3.12+: ```bash pip install osrm-bindings ``` -To build from source (e.g. unsupported platform): +The package itself supports Python 3.10+ when built from source — needed for +3.10/3.11, aarch64 Linux, arm64 macOS, or any platform without a pre-built +wheel: ```bash pip install osrm-bindings --no-binary osrm-bindings @@ -244,18 +248,53 @@ ruff format src/python/osrm/osrm_ext.pyi ## Releasing -Releases are driven by git tags. `setuptools-scm` reads the tag to set the -package version — no manual version bumps needed. - -1. Ensure CI is green on `main`. -2. Create and push an annotated tag: - ```bash - git tag -a v1.2.3 -m "v1.2.3" - git push origin v1.2.3 - ``` -3. The publish workflow triggers on tag push, builds wheels for all platforms, - and uploads to PyPI via trusted publisher. -4. Verify the release at [pypi.org/project/osrm-bindings](https://pypi.org/project/osrm-bindings/). - -**Test release without tagging:** trigger the publish workflow manually via -`workflow_dispatch` with the `upload` input set to `true`. +Releases are driven by the monthly release workflow +([.github/workflows/release-monthly.yml](../../.github/workflows/release-monthly.yml)), +not by pushing a tag by hand. The workflow bumps the version, creates the tag, +drives CI, downloads the built wheels, and publishes to both PyPI and npm in +one shot. + +### Scheduled monthly release + +A cron on the 1st of each month at 08:00 UTC runs the workflow against +`master`: + +1. Compute the next version as `(YYYY-2000).M.patchlevel` (e.g. `26.4.0`). +2. Bump `package.json` + `package-lock.json`, commit, create annotated tag + `v`, push branch and tag. +3. Dispatch `osrm-backend.yml` on the tag. That run builds wheels + sdist + via `cibuildwheel` and uploads them as `wheels-*` artifacts. +4. Wait for the dispatched CI run to finish with conclusion `success`. +5. Run the `publish` job: download every `wheels-*` artifact into `dist/`, + publish to PyPI via trusted publisher (OIDC), then `npm publish`. + +If PyPI fails, the npm publish still runs (the npm steps have +`if: ${{ !cancelled() }}`), and the overall job is marked failed so the PyPI +problem stays visible. + +### Manual release + +Trigger the workflow from the Actions UI or `gh workflow run release-monthly.yml` +with optional inputs: + +- `version_override` — set the version explicitly (e.g. `26.4.1`) instead of + using the `(YYYY-2000).M.patchlevel` calculation. +- `branch` — release from a branch other than `master`. + +### Verification + +After the run finishes, check: + +- Tag `v` exists and the GitHub Release is published. +- [pypi.org/project/osrm-bindings](https://pypi.org/project/osrm-bindings/) shows + the new version with an sdist and three platform wheels (manylinux x86_64, + macOS x86_64, win_amd64). +- [npmjs.com/package/@project-osrm/osrm](https://www.npmjs.com/package/@project-osrm/osrm) + shows the matching version. + +### Version mechanics + +`pyproject.toml` uses `setuptools-scm` with `local_scheme = "no-local-version"`. +On a tag checkout (e.g. `v26.4.0`), the Python version resolves cleanly to +`26.4.0`, matching the `package.json` version that `release-monthly.yml` +committed when creating the tag. From 5b8412d5e04fcebb4e0de68311326d8fc9fa4994 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 14:58:28 +0200 Subject: [PATCH 47/50] run pre-commit and add job to check python .pyi stub --- .github/workflows/osrm-backend.yml | 60 +++++++++++++++++++++++++++++- .pre-commit-config.yaml | 8 ---- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/.github/workflows/osrm-backend.yml b/.github/workflows/osrm-backend.yml index c860c243ae..1380bc5bb5 100644 --- a/.github/workflows/osrm-backend.yml +++ b/.github/workflows/osrm-backend.yml @@ -151,6 +151,11 @@ jobs: PR_TITLE: ${{ github.event.pull_request.title }} run: | node ./scripts/check_pr_title.js + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - name: Run `pre-commit` + uses: pre-commit/action@v3.0.1 - name: Run checks run: | ./scripts/check_taginfo.py taginfo.json profiles/car.lua @@ -714,13 +719,66 @@ jobs: dist/*.tar.gz if-no-files-found: error + - name: Upload Linux wheel for stub check + if: ${{ env.BUILD_BINDINGS && runner.os == 'Linux' && github.event_name == 'pull_request' }} + uses: actions/upload-artifact@v4 + with: + name: wheel-linux-stub-check + path: dist/*.whl + if-no-files-found: error + retention-days: 1 + - name: Show CCache statistics run: | ccache -p ccache -s + # Verify that committed .pyi stubs match the actual C++ bindings when Python + # C++ sources changed. Installs the Linux wheel built by build-matrix, + # regenerates stubs via nanobind, ruff-formats them, and diffs against the repo. + check-python-stubs: + name: Check Python stubs are up to date + needs: build-matrix + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + python_cpp: + - 'src/python/src/**' + - 'src/python/include/**' + - 'src/python/CMakeLists.txt' + - uses: actions/setup-python@v6 + if: steps.changes.outputs.python_cpp == 'true' + with: + python-version: '3.12' + - uses: actions/download-artifact@v4 + if: steps.changes.outputs.python_cpp == 'true' + with: + name: wheel-linux-stub-check + path: wheelhouse + - name: Install wheel and regenerate stubs + if: steps.changes.outputs.python_cpp == 'true' + run: | + pip install nanobind ruff + pip install wheelhouse/*.whl + python -m nanobind.stubgen -m osrm.osrm_ext -o src/python/osrm/osrm_ext.pyi + ruff format src/python/osrm/osrm_ext.pyi + - name: Check for differences + if: steps.changes.outputs.python_cpp == 'true' + run: | + git diff --exit-code src/python/osrm/osrm_ext.pyi \ + || (echo "::error::Stubs are out of date. Rebuild locally and commit the updated .pyi file." && exit 1) + ci-complete: runs-on: ubuntu-latest - needs: [build-matrix, conan-windows-release-node, docker-image-matrix] + needs: [build-matrix, conan-windows-release-node, docker-image-matrix, check-python-stubs] + if: ${{ !cancelled() }} steps: + - name: Fail if any dependency failed + if: ${{ contains(needs.*.result, 'failure') }} + run: exit 1 - run: echo "CI complete" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0bd24ab61a..9e69ffd466 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,4 @@ repos: - - repo: https://github.com/pre-commit/mirrors-clang-format - # matches (more or less) the current clang-format on OSRM CI - # TODO(nils): we should change to pypi's clang tools for reproducibility - rev: v18.1.8 - hooks: - - id: clang-format - types_or: [c, c++] - files: ^src/python/ - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.5 hooks: From 2bcb0035a0e78f4bc0bdf379dc83a7f2add2cce0 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 15:18:46 +0200 Subject: [PATCH 48/50] revert version update --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46b77bf296..e95a8f3cc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@project-osrm/osrm", - "version": "26.4.0", + "version": "26.4.1", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 01baeccfda..22f88fe929 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@project-osrm/osrm", - "version": "26.4.0", + "version": "26.4.1", "private": false, "type": "module", "description": "The Open Source Routing Machine is a high performance routing engine written in C++ designed to run on OpenStreetMap data.", From b65c287b81f14b66d29801ff3b6fba70c45cc523 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 15:23:04 +0200 Subject: [PATCH 49/50] docs update --- README.md | 2 +- src/python/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d468d646d8..e138bf12a2 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ You can install the Python bindings from PyPI via pip install osrm-bindings -We distribute `abi3` wheels for CPython 3.12+ on Linux (x86_64, aarch64), macOS (arm64) and Windows (x86_64). On other platforms `pip` will fall back to building from source, which requires CPython 3.10+ and the OSRM build dependencies. +We distribute `abi3` wheels for CPython 3.12+ on Linux (x86_64), macOS (arm64, x86_64) and Windows (x86_64). On other platforms `pip` will fall back to building from source, which requires CPython 3.10+ and the OSRM build dependencies. To build from source from this repository: diff --git a/src/python/README.md b/src/python/README.md index f2ee4b4395..d0ff27fdde 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -14,7 +14,7 @@ pip install osrm-bindings Platform | Arch ---|--- Linux | x86_64 -Linux | aarch64 +MacOS | aarch64 MacOS | arm64 Windows | x86_64 From 760b656d076c373c8f8f1d1bf8cd75601239ddf6 Mon Sep 17 00:00:00 2001 From: nilsnolde Date: Wed, 22 Apr 2026 16:01:43 +0200 Subject: [PATCH 50/50] change to real pypi --- .github/workflows/release-monthly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-monthly.yml b/.github/workflows/release-monthly.yml index ee1ba23809..56f269d137 100644 --- a/.github/workflows/release-monthly.yml +++ b/.github/workflows/release-monthly.yml @@ -271,5 +271,5 @@ jobs: with: skip-existing: true verbose: true - repository-url: https://test.pypi.org/legacy/ + # repository-url: https://test.pypi.org/legacy/