Skip to content
Closed
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 MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ prune dimos/web/command-center-extension

global-exclude test_*.py
global-exclude conftest.py

include dimos_build.py
36 changes: 36 additions & 0 deletions dimos_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 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.

"""Build-time helpers that do not import the DimOS runtime."""

from pathlib import Path
import shutil
import subprocess


def ensure_web_dist(root: Path) -> None:
"""Build the web assets for Git installs and source distributions."""

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we need the web stuff?

for project, artifact in (("sdk", "sdk.js"), ("cockpit", "index.html")):
source = root / "web" / project
if (source / "dist" / artifact).is_file():
continue
deno = shutil.which("deno")
if deno is None:
raise RuntimeError(
"Deno is required to build DimOS web assets from source. "
"Run dimup setup, then retry dependency installation."
)
subprocess.run([deno, "task", "--cwd", str(source), "build"], check=True)
if not (source / "dist" / artifact).is_file():
raise RuntimeError(f"Deno build did not produce {source / 'dist' / artifact}")
49 changes: 49 additions & 0 deletions installer/src/dimup/sdk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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.

"""Read the consumer dependency policy from the selected SDK revision."""

from copy import deepcopy
from typing import Any

from dimup.process import SetupError

SDK_URL = "https://github.com/dimensionalOS/dimos.git"


def consumer_policy(manifest: dict[str, Any]) -> tuple[list[str], dict[str, Any]]:
extras = sorted(set(manifest["project"]["optional-dependencies"]) - {"dds", "unitree-dds"})
if not extras:
raise SetupError("Selected SDK has no desktop extras.")
upstream = manifest.get("tool", {}).get("uv", {})
policy = {
key: deepcopy(upstream[key])
for key in (
"required-version",
"override-dependencies",
"constraint-dependencies",
"exclude-newer",
"exclude-newer-package",
"sources",
"index",
"extra-build-dependencies",
"extra-build-variables",
)
if key in upstream
}
for name, source in policy.get("sources", {}).items():
entries = source if isinstance(source, list) else [source]
if any("path" in entry or "workspace" in entry for entry in entries):
raise SetupError(f"Selected SDK source {name!r} requires a checkout-local dependency.")
return extras, policy
58 changes: 58 additions & 0 deletions installer/src/dimup/test_sdk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 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.

import pytest

from dimup.process import SetupError
from dimup.sdk import consumer_policy


def test_consumer_policy_keeps_sdk_extras_and_sources_without_dev_groups():
manifest = {
"project": {
"optional-dependencies": {
"all": [],
"spot": [],
"learning": [],
"dds": [],
"unitree-dds": [],
}
},
"dependency-groups": {"docs": ["sphinx"]},
"tool": {
"uv": {
"sources": {"graspgenx": {"git": "https://example.com/grasp", "rev": "abc"}},
"override-dependencies": ["numpy>=2"],
"default-groups": ["docs"],
}
},
}
extras, policy = consumer_policy(manifest)
assert extras == ["all", "learning", "spot"]
assert policy == {
"sources": {"graspgenx": {"git": "https://example.com/grasp", "rev": "abc"}},
"override-dependencies": ["numpy>=2"],
}
policy["sources"]["graspgenx"]["rev"] = "changed"
assert manifest["tool"]["uv"]["sources"]["graspgenx"]["rev"] == "abc"


def test_consumer_policy_rejects_nonportable_sources():
with pytest.raises(SetupError, match="checkout-local"):
consumer_policy(
{
"project": {"optional-dependencies": {"all": []}},
"tool": {"uv": {"sources": {"local": {"path": "../local"}}}},
}
)
36 changes: 14 additions & 22 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
from pybind11.setup_helpers import Pybind11Extension, build_ext
from setuptools import find_packages, setup
from setuptools.command.build_py import build_py as _build_py
from setuptools.command.sdist import sdist as _sdist

# PEP 517 executes this file without adding the source root to sys.path.
sys.path.insert(0, str(Path(__file__).parent))
from dimos_build import ensure_web_dist


def python_is_macos_universal_binary(executable: str | None = None) -> bool:
Expand Down Expand Up @@ -91,6 +96,8 @@ def find_package_modules(self, package, package_dir):
]

def run(self):
if not getattr(self, "editable_mode", False):
ensure_web_dist(Path(__file__).parent)
super().run()
if not getattr(self, "editable_mode", False):
self._copy_relay_dist()
Expand All @@ -99,27 +106,6 @@ def _copy_relay_dist(self):
src = Path(__file__).parent / "web"
if not (src / "relay" / "main.ts").is_file():
raise RuntimeError(f"relay sources missing at {src}; refusing to build the wheel")
missing = [
rel
for rel in ("cockpit/dist/index.html", "sdk/dist/sdk.js")
if not (src / rel).is_file()
]
if missing:
# Wheels must carry the Cockpit and the SDK bundle: there is no
# fallback debug page, so a dist-less wheel's relay serves no UI
# (and /sdk.js only a build hint). Building a deliberate
# python-only wheel (e.g. where deno is unavailable) requires the
# explicit env-var opt-out, which covers both products.
if os.environ.get("DIMOS_ALLOW_MISSING_COCKPIT") != "1":
raise RuntimeError(
f"web dist missing ({', '.join(missing)}); run `deno task build` "
"in web/sdk and web/cockpit (or `dimos run --local-relay` from a "
"checkout builds them), or set DIMOS_ALLOW_MISSING_COCKPIT=1 to "
"build a UI-less wheel anyway"
)
self.warn(
f"web dist missing ({', '.join(missing)}); this wheel's relay will have no UI"
)
dst = Path(self.build_lib) / RELAY_DIST_TARGET
for name in RELAY_DIST_SOURCES:
entry = src / name
Expand All @@ -139,6 +125,12 @@ def _copy_relay_dist(self):
raise RuntimeError(f"relay copy did not produce {dst / 'relay' / 'main.ts'}")


class sdist(_sdist):
def run(self):
ensure_web_dist(Path(__file__).parent)
super().run()


extra_compile_args = [
"-O3", # Maximum optimization
"-ffast-math", # Fast floating point
Expand Down Expand Up @@ -166,5 +158,5 @@ def _copy_relay_dist(self):
packages=find_packages(),
package_dir={"": "."},
ext_modules=ext_modules,
cmdclass={"build_ext": build_ext, "build_py": build_py},
cmdclass={"build_ext": build_ext, "build_py": build_py, "sdist": sdist},
)
51 changes: 51 additions & 0 deletions tests/packaging/test_source_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 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.

from pathlib import Path
import subprocess

import pytest

from dimos_build import ensure_web_dist


def test_bundled_assets_need_no_deno(tmp_path, monkeypatch):
for project, artifact in (("sdk", "sdk.js"), ("cockpit", "index.html")):
dist = tmp_path / "web" / project / "dist"
dist.mkdir(parents=True)
(dist / artifact).write_text("built")
monkeypatch.setattr("shutil.which", lambda name: None)
ensure_web_dist(tmp_path)


def test_missing_deno_explains_machine_setup(tmp_path, monkeypatch):
monkeypatch.setattr("shutil.which", lambda name: None)
with pytest.raises(RuntimeError, match="dimup setup"):
ensure_web_dist(tmp_path)


def test_source_build_produces_both_bundles(tmp_path, monkeypatch):
commands = []

def build(command, *, check):
commands.append(command)
project = Path(command[3])
dist = project / "dist"
dist.mkdir(parents=True)
(dist / ("sdk.js" if project.name == "sdk" else "index.html")).write_text("built")

monkeypatch.setattr("shutil.which", lambda name: "/bin/deno")
monkeypatch.setattr(subprocess, "run", build)
ensure_web_dist(tmp_path)
assert [Path(command[3]).name for command in commands] == ["sdk", "cockpit"]
Loading