#!/usr/bin/env python3
#
# ISC License
#
# Copyright (c) 2026, Autonomous Vehicle Systems Lab, University of Colorado at Boulder
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
"""Generate Rust FFI bindings for selected Basilisk C ABI utilities.
The generated bindings intentionally cover only C ABI headers. C++/Eigen APIs
need a dedicated C shim and are excluded until that interface is designed.
Install the exact command-line generator version before regenerating the
committed bindings::
cargo install bindgen-cli --version '=0.72.1' --locked
The script rejects missing or mismatched versions rather than producing
tool-version-dependent changes to ``bsk_utilities/src/raw.rs``.
The script accepts two include-root layouts:
* Basilisk core (default here): ``--bsk-include`` points at (or defaults to)
this repo's own ``src/`` -- headers are included bare, e.g.
``architecture/utilities/astroConstants.h``.
* Vendored headers: an include root with a ``Basilisk/`` subdirectory. Headers
are included as ``Basilisk/architecture/utilities/astroConstants.h``.
This layout is detected when ``<include-root>/Basilisk`` exists.
"""
import argparse
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
RUST_SUPPORT_ROOT = Path(__file__).resolve().parent
REPO_ROOT = Path(__file__).resolve().parents[3]
# Keep this version synchronized with the ``bindgen`` library used by
# ``bsk_messages`` and the installation command in pull-request CI.
BINDGEN_CLI_VERSION = "0.72.1"
BINDGEN_INSTALL_COMMAND = (
f"cargo install bindgen-cli --version '={BINDGEN_CLI_VERSION}' --locked"
)
FILE_HEADER = """// ISC License
//
// Copyright (c) 2026, Autonomous Vehicle Systems Lab, University of Colorado at Boulder
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
// This file is AUTO-GENERATED by src/architecture/rust/gen_rust_utilities.py.
// DO NOT EDIT BY HAND.
//
// Source headers: astroConstants.h, macroDefinitions.h, orbitalMotion.h, and
// rigidBodyKinematics.h.
//
"""
WRAPPER_TEMPLATE = """\
#include "{prefix}architecture/utilities/astroConstants.h"
#include "{prefix}architecture/utilities/macroDefinitions.h"
#include "{prefix}architecture/utilities/orbitalMotion.h"
#include "{prefix}architecture/utilities/rigidBodyKinematics.h"
/* Bindgen ignores some guarded/expression macros. Re-expose the selected
* values as typed constants without duplicating their definitions in Rust. */
static const double BSK_RUST_G_UNIVERSIAL = G_UNIVERSIAL;
static const double BSK_RUST_SEC2DAY = SEC2DAY;
static const double BSK_RUST_J2_EARTH = J2_EARTH;
static const double BSK_RUST_J3_EARTH = J3_EARTH;
static const double BSK_RUST_J4_EARTH = J4_EARTH;
static const double BSK_RUST_J5_EARTH = J5_EARTH;
static const double BSK_RUST_J6_EARTH = J6_EARTH;
static const double BSK_RUST_J2_MARS = J2_MARS;
"""
[docs]
def resolve_layout(include_root: Path) -> tuple[Path, str]:
"""Return ``(basilisk_include_dir, header_prefix)`` for this include root.
A vendored root has a ``Basilisk/`` subdirectory containing
the actual headers, with the root itself on the include path so
``#include "Basilisk/architecture/..."`` resolves; Basilisk core's own
``src/`` *is* that directory already, with nothing to prefix.
"""
if (include_root / "Basilisk").is_dir():
return include_root / "Basilisk", "Basilisk/"
return include_root, ""
[docs]
def find_default_include_root() -> Path:
"""Default to this repo's own ``src/`` (Basilisk core layout)."""
src = REPO_ROOT / "src"
if (src / "architecture" / "utilities" / "astroConstants.h").exists():
return src
raise RuntimeError(
f"Cannot find architecture/utilities headers under {src}. "
"Pass --bsk-include explicitly when using another include root."
)
[docs]
def find_bindgen() -> str:
"""Return the pinned ``bindgen`` executable or stop with installation help."""
executable = shutil.which("bindgen")
if executable is None:
sys.exit(
f"ERROR: `bindgen` not found on $PATH; run `{BINDGEN_INSTALL_COMMAND}`."
)
version_result = subprocess.run(
[executable, "--version"],
capture_output=True,
text=True,
)
reported_version = version_result.stdout.strip()
expected_version = f"bindgen {BINDGEN_CLI_VERSION}"
if version_result.returncode != 0 or reported_version != expected_version:
if not reported_version:
reported_version = version_result.stderr.strip() or "unknown"
sys.exit(
f"ERROR: expected {expected_version}, but `bindgen --version` reported "
f"`{reported_version}`; run `{BINDGEN_INSTALL_COMMAND}`."
)
return executable
[docs]
def run_bindgen(wrapper_path: Path, include_root: Path, basilisk_dir: Path) -> str:
"""Run bindgen over the selected C ABI headers.
``include_root`` resolves the wrapper header's own ``#include`` lines
(written with ``header_prefix``); ``basilisk_dir`` resolves the
*internal* cross-includes Basilisk headers use among themselves, which
are always bare (e.g. ``"architecture/utilities/linearAlgebra.h"``)
regardless of ``header_prefix``. The two are the same directory in
Basilisk core's own layout (no prefix) and different directories for a
vendored root (see ``resolve_layout``).
"""
bindgen = find_bindgen()
allow_functions = (
"E2f|E2M|f2E|f2H|H2f|H2N|M2E|N2H|"
"elem2rv|rv2elem|clMeanOscMap|clElem2eqElem|"
"hillFrame|hill2rv|rv2hill|"
"atmosphericDensity|debyeLength|atmosphericDrag|solarRad|"
"addMRP|subMRP|MRP2C|C2MRP|MRP2EP|EP2MRP|"
"MRPshadow|MRPswitch|dMRP|dMRP2Omega|ddMRP|ddMRP2dOmega|"
"BmatMRP|BdotmatMRP|BinvMRP|wrapToPi|tilde|Mi"
)
allow_constants = (
"G_UNIVERSIAL|AU|AU2M|SPEED_LIGHT|SOLAR_FLUX_EARTH|"
"D2R|R2D|RPM|SEC2DAY|EARTH_GRAV|"
"MU_(SUN|MERCURY|VENUS|EARTH|MOON|MARS|JUPITER|SATURN|URANUS|NEPTUNE|PLUTO)|"
"REQ_(SUN|MERCURY|VENUS|EARTH|MOON|MARS|JUPITER|SATURN|URANUS|NEPTUNE|PLUTO)|"
"RP_(EARTH|MARS)|"
"NANO2SEC|SEC2NANO|SEC2HOUR"
)
command = [
bindgen,
str(wrapper_path),
"--use-core",
"--no-prepend-enum-name",
"--merge-extern-blocks",
"--with-derive-default",
"--allowlist-type",
"ClassicElements|equinoctialElements|CelestialObject_t",
"--allowlist-function",
f"^({allow_functions})$",
"--allowlist-var",
f"^({allow_constants}|BSK_RUST_.*)$",
"--opaque-type",
"BSKLogger",
"--",
f"-I{include_root}",
f"-I{basilisk_dir}",
"-std=c++17",
"-x",
"c++",
]
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode:
sys.exit(f"ERROR: bindgen failed:\n{result.stderr}")
return result.stdout
[docs]
def main() -> None:
"""Generate ``bsk_utilities/src/raw.rs``."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--bsk-include",
metavar="DIR",
help="Include root: this repository's src directory or a vendored include root.",
)
parser.add_argument(
"--out",
default=RUST_SUPPORT_ROOT / "bsk_utilities" / "src" / "raw.rs",
type=Path,
)
args = parser.parse_args()
include_root = (
Path(args.bsk_include).resolve() if args.bsk_include else find_default_include_root()
)
basilisk_dir, header_prefix = resolve_layout(include_root)
with tempfile.NamedTemporaryFile(suffix=".h", mode="w", delete=False) as file:
file.write(WRAPPER_TEMPLATE.format(prefix=header_prefix))
wrapper_path = Path(file.name)
try:
bindings = run_bindgen(wrapper_path, include_root, basilisk_dir)
finally:
wrapper_path.unlink(missing_ok=True)
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(FILE_HEADER + bindings)
print(f"Wrote {args.out} ({args.out.stat().st_size:,} bytes)")
if __name__ == "__main__":
main()