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] dependencies with version specifiers
  • CMakeLists.txt find_package(), pkg_check_modules(), FetchContent_Declare(), and ExternalProject_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:

  • edition becomes rust_edition (it's language-specific, since horus.toml is multi-language)
  • type = "lib" or type = "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
  • path and git deps don't need source — 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.tomlhorus.tomlNotes
version = "1.0"version = "1.0"Same syntax
features = ["derive"]features = ["derive"]Same syntax
optional = trueoptional = trueSame 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 = falseNot yet supportedUse 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:

  1. Use the enable field for capability flags that HORUS understands (cuda, headless, etc.)
  2. 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 version and source fields
  • 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.txthorus.tomlNotes
"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.tomlKeep in pyproject.toml or use [scripts]
[tool.ruff]Not in horus.tomlKeep 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 stylehorus.tomlMeaning
>=1.24version = ">=1.24"At least 1.24
>=1.24,<2.0version = ">=1.24,<2.0"Range
==1.24.0version = "==1.24.0"Exact
~=1.24version = "~=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 patternhorus.tomlNotes
find_package(Pkg)source = "system" + cmake_package + aptSystem library
FetchContent_Declare(... GIT_REPOSITORY ...)git = "..." + tag or branchGit dependency
ExternalProject_Add(...)git = "..."External project
pkg_check_modules(...)source = "system" + aptpkg-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:

  1. Is it from crates.io? Add source = "crates.io"
  2. Is it from PyPI? Add source = "pypi"
  3. Is it a system library? Add source = "system" with apt and cmake_package
  4. Is it a local path? Use path = "..." (source auto-inferred)
  5. Is it from git? Use git = "..." (source auto-inferred)
  6. 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 wantCargo.tomlpyproject.tomlhorus.toml
Add a Rust crateserde = "1.0"N/Aserde = { version = "1.0", source = "crates.io" }
Add a Python packageN/A"numpy>=1.24"numpy = { version = ">=1.24", source = "pypi" }
Add with featuresfeatures = ["derive"]"pkg[extra]"features = ["derive"]
Dev-only dep[dev-dependencies][project.optional-dependencies][dev-dependencies]
Local path deppath = "../lib"N/Apath = "../lib"
Git depgit = "https://..."N/Agit = "https://..."
System libraryN/AN/Asource = "system", apt = "libfoo-dev"
Add via CLIcargo add serdepip install numpyhorus add serde --source crates.io

Next Steps