Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/scripts/check_sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

# Paths below the dimos-<version>/ root that every release sdist must carry.
REQUIRED = (
"dimos/native_packages.json",
"dimos/_native_revision.json",
"web/cockpit/dist/index.html",
"web/deno.lock",
"web/relay/main.ts",
Expand Down
97 changes: 97 additions & 0 deletions .github/scripts/native_wheel_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
# Copyright 2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Record and read native artifacts from a core-only wheel installation.

Run with the installed environment's Python, from outside the checkout. Nix
is required; preparation may download or build the pinned native package.
"""

from dataclasses import replace
from pathlib import Path
import socket
import subprocess
import sys
import tempfile
import time
import uuid

from dimos.constants import DIMOS_PROJECT_ROOT
from dimos.core.global_config import global_config
from dimos.core.native_package import ensure_native_package, source_revision
from dimos.core.transport import LCMTransport
from dimos.experimental.memory.rust_cli_recorder import RustRecordingSession, make_plan
from dimos.msgs.geometry_msgs.Vector3 import Vector3
from dimos.msgs.sensor_msgs.Imu import Imu


def main() -> None:
assert not (DIMOS_PROJECT_ROOT / ".git").exists(), "must import the installed wheel"
assert not (DIMOS_PROJECT_ROOT / "dimos/experimental/memory/rust").exists()
executable = ensure_native_package("dimos-memory-recorder")
assert executable.is_file()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
url = f"udpm://239.255.76.67:{port}?ttl=0"
transport = LCMTransport(f"/native-wheel-{uuid.uuid4().hex}", Imu, url=url)
with tempfile.TemporaryDirectory(prefix="dimos-native-recording-") as directory:
try:
transport.start()
for kind, suffix in (("sqlite", "db"), ("mcap", "mcap")):
global_config.update(
record_engine="rust", record=kind, record_topics="*", replay=False
)
plan = make_plan({("imu", Imu): transport})
# Artifact location is explicit; executable preparation stays unmodified.
plan = replace(plan, path=Path(directory) / f"memory.{suffix}")
session = RustRecordingSession(plan)
try:
session.start()
for _ in range(3):
transport.broadcast(
None, Imu(ts=22.5, frame_id="imu", angular_velocity=Vector3(1, 2, 3))
)
time.sleep(0.05)
finally:
session.stop()
result = subprocess.run(
[sys.executable, "-m", "dimos.cli.dimos", "mem", "summary", str(plan.path)],
check=True,
capture_output=True,
text=True,
)
assert "imu" in result.stdout, result.stdout
if kind == "mcap":
subprocess.run(
[
sys.executable,
"-m",
"dimos.cli.dimos",
"mem",
"rerun",
str(plan.path),
"--no-gui",
],
check=True,
)
assert plan.path.with_suffix(".rrd").is_file()
finally:
transport.stop()
print(f"Native wheel smoke passed: {source_revision()} -> {executable}")


if __name__ == "__main__":
main()
10 changes: 10 additions & 0 deletions .github/scripts/wheel_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@

import json
from pathlib import Path
import subprocess
import sys
import urllib.request

from dimos.core.native_package import native_packages, package_reference, source_revision
from dimos.navigation.replanning_a_star.min_cost_astar_ext import min_cost_astar_cpp # noqa: F401
from dimos.web.relay_bridge import locate
from dimos.web.relay_bridge.relay_process import RelayProcess
Expand All @@ -44,6 +47,13 @@


def main() -> None:
for command in ("native", "mem"):
subprocess.run([sys.executable, "-m", "dimos.cli.dimos", command, "--help"], check=True)
revision = source_revision()
for package in native_packages().values():
reference = package_reference(package)
if not reference.startswith(f"github:dimensionalOS/dimos/{revision}?"):
raise SystemExit(f"installed native package resolved a checkout: {reference}")
dist = Path(locate.__file__).resolve().parent / "_relay_dist"
for rel in REQUIRED:
if not (dist / rel).is_file():
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/release-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ on:
- web/deno.json
- web/deno.lock
- dimos/utils/deno.py
- dimos/native_packages.json
- dimos/core/native_package.py

permissions: {}

Expand All @@ -39,6 +41,8 @@ jobs:
uses: ./.github/actions/build-cockpit
- name: Build one wheel
uses: pypa/cibuildwheel@v4.2.0
env:
CIBW_ENVIRONMENT: DIMOS_BUILD_REVISION=${{ github.sha }}
with:
only: cp312-manylinux_x86_64

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ jobs:
attestations: write # For uploading the SLSA build-provenance attestation
env:
CIBW_ARCHS: ${{ matrix.arch }}
CIBW_ENVIRONMENT: DIMOS_BUILD_REVISION=${{ github.sha }}
# Skip musllinux (Alpine) — Pinocchio's build ecosystem (pin/coal/cmeel-*)
# is glibc-only; cmeel-assimp >= 6.0.5 has no musllinux wheels.
CIBW_SKIP: "*-musllinux_*"
Expand Down
25 changes: 24 additions & 1 deletion bin/build-native-modules
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import ast
from concurrent.futures import ProcessPoolExecutor, as_completed
from dataclasses import dataclass
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
Expand All @@ -54,7 +55,7 @@ import time
import urllib.error
import urllib.request

MARKER_SALT = 1
MARKER_SALT = 2

REPO_ROOT = Path(__file__).resolve().parent.parent
PACKAGE_DIR = REPO_ROOT / "dimos"
Expand Down Expand Up @@ -137,7 +138,9 @@ def is_nix_build(command: str | None) -> bool:


def discover() -> tuple[DiscoveredModule, ...]:
catalog = json.loads((PACKAGE_DIR / "native_packages.json").read_text())
found = []
packages_seen = set()
provisioned_seen = set()
for dirpath, dirnames, filenames in os.walk(PACKAGE_DIR):
dirnames[:] = sorted(d for d in dirnames if d not in IGNORED_DIRS)
Expand All @@ -159,6 +162,21 @@ def discover() -> tuple[DiscoveredModule, ...]:
provisioned_seen.add(qualname)
continue
build_command = _literal_default(node, "build_command", source)
package_id = _literal_default(node, "native_package", source)
if package_id:
if package_id not in catalog:
raise SystemExit(f"{source}: unknown native_package {package_id!r}")
package = catalog[package_id]
packages_seen.add(package_id)
found.append(
DiscoveredModule(
qualname=qualname,
source=source,
build_command=f"nix build -L .#{package['attribute']}",
build_dir=package["flake_dir"],
)
)
continue
if not is_nix_build(build_command):
continue
cwd = _literal_default(node, "cwd", source)
Expand All @@ -180,6 +198,10 @@ def discover() -> tuple[DiscoveredModule, ...]:
f"EXTERNALLY_PROVISIONED entries not found in the tree: {sorted(stale)}"
" — remove them from bin/build-native-modules"
)
if packages_seen != set(catalog):
raise SystemExit(
f"Native packages without a module: {sorted(set(catalog) - packages_seen)}"
)
return tuple(sorted(found))


Expand Down Expand Up @@ -377,6 +399,7 @@ def build_manifest(modules: tuple[DiscoveredModule, ...]) -> str:
f"salt {MARKER_SALT}",
f"script {_git_object_hash('bin/build-native-modules')}",
f"cachix {os.environ.get('CACHIX_CACHE_NAME', '')}",
f"catalog {(PACKAGE_DIR / 'native_packages.json').read_text().strip()}",
*(f"module {m.qualname} dir={m.build_dir} cmd={m.build_command}" for m in modules),
*(f"path {path} {_git_object_hash(path)}" for path in top),
]
Expand Down
3 changes: 2 additions & 1 deletion dimos/cli/can.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@

from __future__ import annotations

import importlib
import os
import shlex
import subprocess
import sys

import can_motor_control
import typer

app = typer.Typer(help="Discover and configure CAN interfaces", no_args_is_help=True)
Expand Down Expand Up @@ -64,6 +64,7 @@ def list_devices() -> None:
typer.echo(output or "No SocketCAN interfaces found")
return
if sys.platform == "darwin":
can_motor_control = importlib.import_module("can_motor_control")
try:
devices = can_motor_control.list_gs_usb_devices(
vendor_id=GS_USB_VENDOR_ID,
Expand Down
31 changes: 31 additions & 0 deletions dimos/cli/commands/native.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright 2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Prepare native executables without starting a robot."""

import typer

from dimos.core.native_package import ensure_native_package

native_app = typer.Typer(help="Prepare native packages through Nix and Cachix.")


@native_app.command()
def prepare(package_id: str) -> None:
"""Download or build PACKAGE_ID using the same inputs as automatic startup."""
try:
executable = ensure_native_package(package_id)
except (ValueError, RuntimeError) as error:
raise typer.BadParameter(str(error)) from error
typer.echo(str(executable))
2 changes: 2 additions & 0 deletions dimos/cli/dimos.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
from dimos.cli.commands.lifecycle import log_cmd, restart, run, status, stop
from dimos.cli.commands.map import map_app
from dimos.cli.commands.mcp import agent_send_cmd, mcp_app
from dimos.cli.commands.native import native_app
from dimos.cli.commands.rerun_bridge import rerun_bridge_cmd
from dimos.cli.commands.topic import topic_app
from dimos.cli.commands.tuis import agentspy, humancli, lcmspy, spy, top
Expand Down Expand Up @@ -146,6 +147,7 @@ def cli_main() -> None:
from dimos.memory.cli.app import mem_app

main.add_typer(mem_app, name="mem")
main.add_typer(native_app, name="native")

from dimos.evals.cli import app as evals_app

Expand Down
8 changes: 5 additions & 3 deletions dimos/cli/hardware/g1.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from __future__ import annotations

import importlib
import time
from typing import Any, NoReturn, Protocol, TypeGuard

Expand All @@ -25,7 +26,6 @@
from dimos.msgs.sensor_msgs.JointState import JointState
from dimos.porcelain.dimos import Dimos
from dimos.porcelain.module_handle import ModuleHandle
from dimos.robot.unitree.g1.manip_config import G1_READY_JOINTS, G1_READY_SPEED_SCALE

app = typer.Typer(help="Operate a running Unitree G1 stack safely")

Expand Down Expand Up @@ -162,10 +162,12 @@ def _execute_ready_pose(
) -> None:
_require_armed_and_enabled(coordinator)
_require_teleop_disengaged(coordinator)
manip_config = importlib.import_module("dimos.robot.unitree.g1.manip_config")
targets = {
group: JointState(position=list(positions)) for group, positions in G1_READY_JOINTS.items()
group: JointState(position=list(positions))
for group, positions in manip_config.G1_READY_JOINTS.items()
}
planned = manipulation.plan_to_joints(targets, speed_scale=G1_READY_SPEED_SCALE)
planned = manipulation.plan_to_joints(targets, speed_scale=manip_config.G1_READY_SPEED_SCALE)
if not planned.succeeded:
_abort(f"ready-pose planning failed: {planned}")
executed = manipulation.execute(blocking=True)
Expand Down
5 changes: 5 additions & 0 deletions dimos/cli/test_can.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
from dimos.cli.dimos import main


@pytest.fixture(autouse=True)
def can_backend(mocker: MockerFixture) -> None:
mocker.patch.dict(sys.modules, {"can_motor_control": Mock(TransportError=RuntimeError)})


def _subprocess_argv(run: Mock) -> list[list[str]]:
return [call.args[0] for call in run.call_args_list]

Expand Down
Loading
Loading