Migrating to horus.toml
If you already have a Rust, Python, or C++ project and want to move it to HORUS, this page shows you exactly how your existing config maps to horus.toml. No guessing — just side-by-side translations.
New project? Skip this page. Run horus new my-project -r (Rust) or horus new my-project -p (Python) and you'll get a ready-to-go horus.toml. This page is for existing projects you want to bring into the HORUS ecosystem.
Quick Path: Automatic Migration
HORUS can read your existing build files and generate horus.toml for you:
# Step 1: Initialize a horus.toml in your project
cd my-existing-project
horus init
# Step 2: Auto-import dependencies from existing configs
horus migrate --dry-run # Preview what will change
horus migrate # Do it
horus migrate reads Cargo.toml, pyproject.toml, and CMakeLists.txt from your project root, extracts all dependencies, and writes them into horus.toml. It backs up old files to .horus/backup/ so nothing is lost.
What it handles:
- Cargo.toml
[dependencies]and[dev-dependencies]with versions, features, git sources, and path deps - pyproject.toml PEP 621
[project] dependencieswith version specifiers - CMakeLists.txt
find_package(),pkg_check_modules(),FetchContent_Declare(), andExternalProject_Add()
What you'll still do manually: [hardware], [scripts], [hooks], [robot] — these are HORUS-specific and have no equivalent in native configs.
If you prefer to understand the mapping and do it yourself, read on.
Coming from Rust (Cargo.toml)
If you've used Cargo, horus.toml will feel familiar. The structure is intentionally similar.
Package Metadata
Key differences:
editionbecomesrust_edition(it's language-specific, since horus.toml is multi-language)type = "lib"ortype = "bin"works the same way- No
[lib],[[bin]], or[profile]sections — HORUS handles these in.horus/Cargo.toml
Dependencies
This is where the biggest shift happens. In Cargo, source is implicit — everything comes from crates.io unless you specify path or git. In horus.toml, you declare the source explicitly because deps can come from six different places.
Before — Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
my-lib = { path = "../my-lib" }
my-driver = { git = "https://github.com/team/driver.git", branch = "main" }
[dev-dependencies]
criterion = "0.5"
After — horus.toml:
[dependencies]
serde = { version = "1.0", source = "crates.io", features = ["derive"] }
tokio = { version = "1", source = "crates.io", features = ["full"] }
my-lib = { path = "../my-lib" }
my-driver = { git = "https://github.com/team/driver.git", branch = "main" }
[dev-dependencies]
criterion = { version = "0.5", source = "crates.io" }
What changed:
- Added
source = "crates.io"to crates.io deps. Without it, HORUS defaults to the HORUS registry pathandgitdeps don't needsource— HORUS infers it automatically- Everything else is identical
The most common mistake: Forgetting source = "crates.io". Without it, HORUS looks for the package in the HORUS registry, not crates.io. If horus build says "package not found", check your sources first.
Field-by-Field Mapping
| Cargo.toml | horus.toml | Notes |
|---|---|---|
version = "1.0" | version = "1.0" | Same syntax |
features = ["derive"] | features = ["derive"] | Same syntax |
optional = true | optional = true | Same syntax |
path = "../lib" | path = "../lib" | Source auto-inferred |
git = "https://..." | git = "https://..." | Source auto-inferred |
branch = "main" | branch = "main" | Same syntax |
tag = "v1.0" | tag = "v1.0" | Same syntax |
rev = "abc123" | rev = "abc123" | Same syntax |
| (implicit crates.io) | source = "crates.io" | Must be explicit |
default-features = false | Not yet supported | Use features = [] |
Cargo Features
Cargo [features] don't have a direct equivalent in horus.toml. Feature flags for your own crate are defined in the generated .horus/Cargo.toml. For now, if your project defines custom features, you have two options:
- Use the
enablefield for capability flags that HORUS understands (cuda,headless, etc.) - For custom Cargo features, use
horus cargo build --features my-feature— the native tool proxy passes flags through
Workspaces
Before — Cargo.toml (root):
[workspace]
members = ["crates/*"]
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
After — horus.toml (root):
[workspace]
members = ["crates/*"]
[workspace.dependencies]
serde = { version = "1.0", source = "crates.io", features = ["derive"] }
Each workspace member gets its own horus.toml:
# crates/my-node/horus.toml
[package]
name = "my-node"
version = "0.1.0"
[dependencies]
serde = { workspace = true } # Inherits from root
This works exactly like Cargo workspaces — workspace = true pulls version, features, and source from the root.
Coming from Python (pyproject.toml)
Python's packaging landscape has pyproject.toml, setup.py, requirements.txt, and setup.cfg. HORUS replaces all of them with one section in horus.toml.
Package Metadata
Key differences:
[project]becomes[package]— same fields, simpler syntax- Authors are strings, not tables
- License is a string, not a table
- No
requires-python— HORUS manages the Python environment
Dependencies
Before — pyproject.toml:
[project]
dependencies = [
"numpy>=1.24",
"opencv-python>=4.8",
"torch>=2.0",
]
[project.optional-dependencies]
dev = ["pytest>=7.0", "ruff>=0.1"]
After — horus.toml:
[dependencies]
numpy = { version = ">=1.24", source = "pypi" }
opencv-python = { version = ">=4.8", source = "pypi" }
torch = { version = ">=2.0", source = "pypi" }
[dev-dependencies]
pytest = { version = ">=7.0", source = "pypi" }
ruff = { version = ">=0.1", source = "pypi" }
What changed:
- PEP 508 inline strings become TOML tables with
versionandsourcefields source = "pypi"is required — without it, HORUS looks in its own registry- Optional dependency groups become
[dev-dependencies]
If you're using requirements.txt instead:
# requirements.txt
numpy>=1.24
opencv-python>=4.8
torch>=2.0
The mapping is the same — each line becomes an entry in [dependencies] with source = "pypi".
Field-by-Field Mapping
| pyproject.toml / requirements.txt | horus.toml | Notes |
|---|---|---|
"numpy>=1.24" | version = ">=1.24", source = "pypi" | Version syntax is the same |
"torch==2.0.1" | version = "==2.0.1", source = "pypi" | Exact pinning works |
"pkg[extra1,extra2]" | features = ["extra1", "extra2"] | Extras become features |
[project.optional-dependencies] | [dev-dependencies] | Dev-only scope |
[tool.pytest.ini_options] | Not in horus.toml | Keep in pyproject.toml or use [scripts] |
[tool.ruff] | Not in horus.toml | Keep in pyproject.toml or ruff.toml |
Tool configuration ([tool.pytest], [tool.ruff], [tool.mypy]) stays in pyproject.toml or its own config file. horus.toml only handles dependencies and project metadata — it doesn't replace tool-specific configuration.
Version Specifiers
Python version specifiers work directly in horus.toml:
| Python style | horus.toml | Meaning |
|---|---|---|
>=1.24 | version = ">=1.24" | At least 1.24 |
>=1.24,<2.0 | version = ">=1.24,<2.0" | Range |
==1.24.0 | version = "==1.24.0" | Exact |
~=1.24 | version = "~=1.24" | Compatible release |
Coming from C++ (CMakeLists.txt)
C++ migration is more involved because CMake configs are imperative scripts, not declarative manifests. HORUS extracts what it can and puts it in [dependencies] and [cpp].
Package Metadata
Before — CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(my_robot VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
After — horus.toml:
[package]
name = "my-robot"
version = "0.1.0"
standard = "c++20"
[cpp]
compiler = "clang++" # Optional
cmake_args = ["-DCMAKE_BUILD_TYPE=Release"] # Optional
Dependencies
Before — CMakeLists.txt:
find_package(Eigen3 REQUIRED)
find_package(OpenCV REQUIRED)
find_package(Boost REQUIRED COMPONENTS filesystem system)
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.1.1
)
FetchContent_MakeAvailable(fmt)
After — horus.toml:
[dependencies]
eigen3 = { source = "system", apt = "libeigen3-dev", cmake_package = "Eigen3" }
opencv = { source = "system", apt = "libopencv-dev", cmake_package = "OpenCV" }
boost = { source = "system", apt = "libboost-all-dev", cmake_package = "Boost" }
fmt = { git = "https://github.com/fmtlib/fmt.git", tag = "10.1.1" }
Field-by-Field Mapping
| CMake pattern | horus.toml | Notes |
|---|---|---|
find_package(Pkg) | source = "system" + cmake_package + apt | System library |
FetchContent_Declare(... GIT_REPOSITORY ...) | git = "..." + tag or branch | Git dependency |
ExternalProject_Add(...) | git = "..." | External project |
pkg_check_modules(...) | source = "system" + apt | pkg-config library |
set(CMAKE_CXX_STANDARD 20) | standard = "c++20" in [package] | Language standard |
target_compile_options(...) | cmake_args = [...] in [cpp] | Build flags |
CMake migration has limits. Custom CMake logic (conditionals, generator expressions, install rules) can't be auto-converted. horus migrate extracts dependencies and basic settings. Complex build logic may need cmake_args in [cpp] or a wrapper script in [scripts].
Mixed-Language Projects
This is where horus.toml pays off. A project using Rust for control and Python for ML traditionally needs separate config files. With horus.toml, it's one file:
Before — two files:
# Cargo.toml
[package]
name = "my-robot"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
horus_library = "0.1.9"
# pyproject.toml
[project]
name = "my-robot"
version = "0.1.0"
dependencies = ["numpy>=1.24", "torch>=2.0"]
After — one file:
# horus.toml
[package]
name = "my-robot"
version = "0.1.0"
[dependencies]
# Rust
serde = { version = "1.0", source = "crates.io", features = ["derive"] }
horus_library = "0.1.9"
# Python
numpy = { version = ">=1.24", source = "pypi" }
torch = { version = ">=2.0", source = "pypi" }
When you run horus build, HORUS generates both .horus/Cargo.toml (with serde and horus_library) and .horus/pyproject.toml (with numpy and torch). Each build tool only sees its own deps.
Using Native Tools After Migration
You don't have to abandon cargo or pip after switching to horus.toml. HORUS provides transparent proxying:
# Set up the proxy (add to your shell profile)
eval "$(horus env --init)"
# Now these commands auto-sync with horus.toml
cargo add rand # Adds to horus.toml with source = "crates.io"
pip install scipy # Adds to horus.toml with source = "pypi"
cargo build # Builds from .horus/Cargo.toml (generated from horus.toml)
The proxy intercepts cargo and pip commands, updates horus.toml, regenerates the native build files, and runs the real tool. Your muscle memory still works.
To bypass the proxy for a one-off command:
HORUS_NO_PROXY=1 cargo build # Uses Cargo.toml directly, skips horus.toml
See Native Tool Integration for details on how the proxy works.
Step-by-Step: Manual Migration
If horus migrate doesn't cover your case, or you want to understand every step:
Step 1: Initialize
cd my-existing-project
horus init
This creates a minimal horus.toml and .horus/ directory. It does not touch your existing files.
Step 2: Move Package Metadata
Copy name, version, description, authors, license from your existing config into [package].
Step 3: Convert Dependencies
For each dependency in your existing config:
- Is it from crates.io? Add
source = "crates.io" - Is it from PyPI? Add
source = "pypi" - Is it a system library? Add
source = "system"withaptandcmake_package - Is it a local path? Use
path = "..."(source auto-inferred) - Is it from git? Use
git = "..."(source auto-inferred) - Is it from the HORUS registry? Just use
name = "version"(default source)
Step 4: Add HORUS-Specific Config
These sections have no equivalent in native build files — add them as needed:
[robot]
name = "turtlebot"
description = "robot.urdf"
simulator = "sim3d"
[hardware]
lidar = { use = "rplidar", port = "/dev/ttyUSB0", sim = true }
imu = { use = "bno055", bus = "i2c-1", sim = true }
[scripts]
sim = "horus sim start --world warehouse"
deploy = "horus deploy pi@robot --release"
[hooks]
pre_run = ["fmt", "lint"]
Step 5: Verify
horus check # Validates horus.toml syntax and fields
horus build # Generates .horus/ and builds
horus test # Runs tests through the new config
Step 6: Clean Up
Once everything works, you can remove the old config files. horus migrate backs them up to .horus/backup/ automatically. If you migrated manually:
# Only after confirming horus build and horus test pass
mkdir -p .horus/backup
mv Cargo.toml .horus/backup/ # If Rust
mv pyproject.toml .horus/backup/ # If Python
mv CMakeLists.txt .horus/backup/ # If C++
Keep tool configs. Files like ruff.toml, clippy.toml, rustfmt.toml, and pytest.ini are NOT replaced by horus.toml. They configure tools, not dependencies. Keep them where they are.
Troubleshooting
"Package not found" after migration
Most likely a missing source field. Check that every crates.io dep has source = "crates.io" and every PyPI dep has source = "pypi". Without an explicit source, HORUS looks in its own registry.
"Invalid version format"
horus.toml requires full semver for your project version: "0.1.0", not "0.1". Dependency versions can use ranges (">=1.24", "^1.0").
Build fails but old config worked
Compare the generated .horus/Cargo.toml or .horus/pyproject.toml against your original. If a feature flag or version constraint is missing, update horus.toml and run horus build again.
diff Cargo.toml.bak .horus/Cargo.toml # Spot the difference
Cargo features I defined are missing
Custom [features] sections aren't in horus.toml yet. Use the native tool proxy:
horus cargo build --features my-custom-feature
Cheat Sheet
A quick reference for the most common translations:
| What you want | Cargo.toml | pyproject.toml | horus.toml |
|---|---|---|---|
| Add a Rust crate | serde = "1.0" | N/A | serde = { version = "1.0", source = "crates.io" } |
| Add a Python package | N/A | "numpy>=1.24" | numpy = { version = ">=1.24", source = "pypi" } |
| Add with features | features = ["derive"] | "pkg[extra]" | features = ["derive"] |
| Dev-only dep | [dev-dependencies] | [project.optional-dependencies] | [dev-dependencies] |
| Local path dep | path = "../lib" | N/A | path = "../lib" |
| Git dep | git = "https://..." | N/A | git = "https://..." |
| System library | N/A | N/A | source = "system", apt = "libfoo-dev" |
| Add via CLI | cargo add serde | pip install numpy | horus add serde --source crates.io |
Next Steps
- horus.toml Reference — Why it's the single source of truth
- Configuration Reference — Every field documented
- Native Tool Integration — Keep using
cargoandpip - Multi-Crate Workspaces — Workspace migration
- CLI Reference — All
horuscommands