horus.toml: Single Source of Truth

Every HORUS project has one config file: horus.toml. It replaces the need for separate Cargo.toml and pyproject.toml files. You declare your dependencies once, and HORUS generates everything else.


The Problem

Traditional robotics projects need different config files for each language:

LanguageConfig Files
RustCargo.toml + Cargo.lock
Pythonpyproject.toml + requirements.txt
MixedAll of the above

A robot using Python for ML and Rust for control needs 3+ config files. Adding a dependency means knowing which file to edit.

The HORUS Solution

One file. All languages. All dependencies.

# horus.toml — the only config file you edit
[package]
name = "my-robot"
version = "0.1.0"

[robot]
name = "turtlebot"
description = "robot.urdf"    # Path to URDF file
simulator = "sim3d"           # Simulator plugin (default)

[dependencies]
# Rust (auto-detected as crates.io)
serde = { version = "1.0", source = "crates.io", features = ["derive"] }

# Python (auto-detected as PyPI)
numpy = { version = ">=1.24", source = "pypi" }

[hardware]
lidar = { use = "rplidar", port = "/dev/ttyUSB0", sim = true }
imu = { use = "bno055", bus = 1, sim = true }

[scripts]
sim = "horus sim start --world warehouse"
deploy = "horus deploy pi@robot --release"

[hooks]
pre_run = ["fmt", "lint"]    # Auto-format and lint before every run

When you run horus build, HORUS reads horus.toml and generates native build files:

  • Rust deps.horus/Cargo.toml
  • Python deps.horus/pyproject.toml

You never see or edit these generated files.


The .horus/ Directory

Every HORUS project has a .horus/ directory containing generated files and build artifacts. It's gitignored and fully managed by horus.

my_project/
├── horus.toml              ← You edit this
├── src/
│   ├── main.rs
│   └── main.py
└── .horus/                 ← Generated (don't touch)
    ├── Cargo.toml          ← From horus.toml Rust deps
    ├── pyproject.toml      ← From horus.toml Python deps
    ├── target/             ← Rust build artifacts
    └── packages/           ← Cached registry packages

Never edit files inside .horus/. They are regenerated from horus.toml every time you build. If you need to change a dependency, edit horus.toml and run horus build.

If .horus/ gets corrupted, just delete it:

horus clean --all    # Remove everything, regenerated on next build

How It Works

horus.toml is the single source — native build files are generated into .horus/
CommandWhat happens
horus add serdeDetects crates.io → adds to horus.toml → regenerates .horus/Cargo.toml
horus add numpyDetects PyPI → adds to horus.toml → regenerates .horus/pyproject.toml
horus buildReads horus.toml → generates all build files → builds all languages
horus testRuns cargo test + pytest (all from horus.toml)
horus remove XRemoves from horus.toml → regenerates affected build files

Comparison

TraditionalHORUS
Config files3+ per language1 (horus.toml)
Add a depEdit the right file, know the syntaxhorus add NAME
Buildcargo build + pip install + ...horus build
Testcargo test + pytesthorus test
DeployManual cross-compile scriptshorus deploy pi@robot
OnboardingLearn 3 build systemsLearn 1 CLI

Workspace Projects

For projects that contain multiple crates (e.g., a driver library and a binary that uses it), horus.toml supports a [workspace] section. This lets you manage several crates under a single project root while keeping one unified config file.

# horus.toml — workspace with multiple crates
[package]
name = "my-robot"
version = "0.1.0"
type = "lib"          # "lib" for libraries, omit for binaries

[workspace]
members = [
  "crates/driver",
  "crates/controller",
  "crates/my-robot-bin",
]

[dependencies]
serde = { version = "1.0", source = "crates.io", features = ["derive"] }

Each workspace member directory contains its own horus.toml with a [package] section. The root horus.toml defines shared dependencies and the member list. When you run horus build, all members are compiled together, and you can target a specific member with horus build --package controller.

The type = "lib" field in [package] marks a crate as a library (no binary entry point). This is useful for shared code that other workspace members depend on but that is not run directly.

For a complete guide on setting up and working with multi-crate workspaces, see Multi-Crate Workspaces.


Next Steps


All Sections

SectionPurpose
[package]Project name, version, metadata
[robot]Robot name, URDF path, simulator selection
[dependencies]Project deps from any source (crates.io, PyPI, system, registry)
[dev-dependencies]Test/development-only dependencies
[hardware]Hardware node configuration (use field + params, sim for simulation swap)
[scripts]Custom project commands
[hooks]Pre/post action hooks (pre_run, pre_build, pre_test, post_test)
[ignore]File/directory exclusion patterns
enableCapability flags (cuda, gpio, etc.)
[cpp]C++ build configuration
[workspace]Multi-crate workspace

See Configuration Reference for complete field-level documentation.


Design Decisions

Why TOML Instead of YAML

YAML is common in robotics (ROS2 launch files, parameter files), but it has well-documented pitfalls for configuration: implicit type coercion (yes becomes true, 3.10 becomes 3.1), significant whitespace that causes silent errors, and multiple ways to express the same thing. TOML has an unambiguous grammar — every value has an explicit type, indentation is not significant, and there is one canonical way to write each structure. HORUS still uses YAML for launch configs and parameter files where its flexibility is useful, but the project manifest uses TOML because dependency versions and package metadata must be parsed unambiguously.

Why One File Instead of Per-Language Configs

A robot project using Rust for control and Python for ML traditionally needs Cargo.toml, pyproject.toml, and possibly requirements.txt — each with different syntax, different dependency resolution, and different tooling. When a team member adds a dependency, they need to know which file to edit. horus.toml is the single source of truth: all dependencies are declared once with an explicit source field (crates.io, pypi, system, git, path). HORUS generates the native build files (Cargo.toml, pyproject.toml) into .horus/ automatically. One file to learn, one file to review in PRs, one file to validate in CI.

Why Generated Build Files in .horus/ Directory

Native build tools (cargo, pip) need their own config files to function. Rather than forcing users to maintain both horus.toml and Cargo.toml in sync, HORUS generates the native files into a .horus/ directory that is gitignored and fully managed. Users never edit these files. This means cargo build and pip install still work under the hood with their standard tooling — HORUS does not replace build systems, it generates their input. If .horus/ gets corrupted, horus clean --all deletes it and the next build regenerates everything from horus.toml.

Trade-offs

AreaBenefitCost
TOML formatUnambiguous parsing; no implicit type coercion; explicit types for dependency versionsLess familiar to teams coming from YAML-heavy ROS2 workflows
Single manifestOne file to learn, edit, and review; horus add works for any languageMust specify source for non-default package registries (e.g., source = "pypi" for Python deps)
Generated build filesNative tooling (cargo, pip) works unchanged; no custom build system to learnCannot use Cargo.toml features not exposed by horus.toml without escape hatches; generated files must not be edited
.horus/ directoryClean project root; generated artifacts are gitignored and disposableExtra directory to understand; new contributors may be confused by the absence of Cargo.toml in the project root
horus add/remove CLINo need to manually edit TOML or know per-language syntaxDependency resolution depends on HORUS tooling — cargo add and pip install do not update horus.toml
Workspace supportMulti-crate projects with shared dependencies and a single root configWorkspace members still need their own horus.toml with [package] — not fully flat

See Also