Source code for gen_rust_utilities

#!/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()