diff --git a/bin/build-native-modules b/bin/build-native-modules index 686c356007..45c1146a27 100755 --- a/bin/build-native-modules +++ b/bin/build-native-modules @@ -308,6 +308,10 @@ def _flake_refs(flake: Path) -> tuple[set[str], set[str]]: f"{os.path.relpath(flake, REPO_ROOT)}: {what} reaches content the inputs hash" " cannot follow; use a relative path literal or a locked flake input instead" ) + # `pkgs.lib.fileset.toSource { root = ; fileset = ; }` anchors + # the enumerated members at `root`; the copied content is the members, not + # `root` itself (typically the repo root). Skip hashing the root itself. + uses_fileset = "fileset.toSource" in text literals: set[str] = set() path_inputs: set[str] = set() for match in _FLAKE_REF.finditer(text): @@ -323,6 +327,8 @@ def _flake_refs(flake: Path) -> tuple[set[str], set[str]]: f" {match.group('path')!r} — the inputs hash can only follow static path" " literals; reference a plain literal (a parent directory is fine) instead" ) + if uses_fileset and re.search(r"\broot\s*=\s*$", text[: match.start()]): + continue # fileset.toSource anchor, not an input rel = _resolve_ref(flake, match.group("path")) (path_inputs if match.group("scheme") == "path:" else literals).add(rel) return literals, path_inputs @@ -377,9 +383,10 @@ def build_manifest(modules: tuple[DiscoveredModule, ...]) -> str: for module in modules: paths |= _collect_input_paths(module) # Drop paths nested under another (e.g. a patch file inside its build dir). + # "." (repo root) sorts first and subsumes everything. top: list[str] = [] for path in sorted(paths): - if not any(path == kept or path.startswith(kept + "/") for kept in top): + if not any(kept == "." or path == kept or path.startswith(kept + "/") for kept in top): top.append(path) lines = [ f"salt {MARKER_SALT}", diff --git a/dimos/core/test_build_native_modules.py b/dimos/core/test_build_native_modules.py index 1cf0f44837..5914df1e8a 100644 --- a/dimos/core/test_build_native_modules.py +++ b/dimos/core/test_build_native_modules.py @@ -205,6 +205,38 @@ def test_ast_extraction_matches_runtime() -> None: assert runtime_dir == (DIMOS_PROJECT_ROOT / module.build_dir).resolve() +def test_no_module_hashes_the_repo_root() -> None: + """A collected input of "." puts the whole-repo tree SHA in the marker key, + so it changes on every commit and the marker never matches. A + fileset.toSource `root` anchor (usually the repo root) must be skipped, not + hashed — regression guard for the rust_recorder fileset flake.""" + for module in _SCRIPT.discover(): + assert "." not in _SCRIPT._collect_input_paths(module), ( + f"{module.qualname}: input set includes the repo root — a fileset root anchor " + "is being hashed, which busts the publish marker on every commit" + ) + + +def test_recorder_fileset_covers_every_workspace_member() -> None: + """Cargo resolves the workspace from the root manifest, so the recorder's + fileset src must carry every [workspace] member — as static path literals, + because the publish gate can only hash literals. Deriving the list from + Cargo.toml at eval time (fromTOML) is invisible to the flake parser, which + then hashes the fileset root instead: the repo-root tree SHA busts the + publish marker on every commit.""" + flake = DIMOS_PROJECT_ROOT / "dimos" / "experimental" / "memory" / "rust" / "flake.nix" + block = re.search(r"members\s*=\s*\[([^]]*)\]", (DIMOS_PROJECT_ROOT / "Cargo.toml").read_text()) + assert block is not None, "no [workspace] members array in Cargo.toml" + members = re.findall(r'"([^"]+)"', block.group(1)) + assert members, "no [workspace] members parsed from Cargo.toml" + literals, _path_inputs = _SCRIPT._flake_refs(flake) + for member in members: + assert any(member == lit or member.startswith(lit + "/") for lit in literals), ( + f"workspace member {member!r} has no covering path literal in {flake} — " + "list it in the fileset.unions so the publish gate hashes it" + ) + + @pytest.mark.skipif(not _IN_GIT_CHECKOUT, reason="needs git HEAD for object hashes") def test_manifest_is_deterministic() -> None: modules = _SCRIPT.discover() @@ -270,7 +302,8 @@ def test_flake_refs_resolve_and_are_covered() -> None: flake = DIMOS_PROJECT_ROOT / rel / "flake.nix" if not flake.is_file(): continue # plain source tree (e.g. native/cpp), nothing to sweep - for match in _RAW_REF.finditer(flake.read_text()): + raw = flake.read_text() + for match in _RAW_REF.finditer(raw): if match.group("scheme") == "git+file:": url = match.group("path").split("?", 1)[0] lock = json.loads((flake.parent / "flake.lock").read_text()) @@ -286,6 +319,8 @@ def test_flake_refs_resolve_and_are_covered() -> None: " derivation on every commit behind the publish gate's back" ) continue + if "fileset.toSource" in raw and re.search(r"\broot\s*=\s*$", raw[: match.start()]): + continue # fileset anchor, deliberately not an input (see _flake_refs) token = match.group("path").split("?", 1)[0] target = os.path.relpath(os.path.normpath(flake.parent / token), DIMOS_PROJECT_ROOT) if not (DIMOS_PROJECT_ROOT / target).exists(): diff --git a/dimos/experimental/memory/rust/flake.nix b/dimos/experimental/memory/rust/flake.nix index bb9e0318aa..6fa8e95610 100644 --- a/dimos/experimental/memory/rust/flake.nix +++ b/dimos/experimental/memory/rust/flake.nix @@ -17,22 +17,25 @@ "aarch64-darwin" ] (system: let pkgs = nixpkgs.legacyPackages.${system}; - workspaceRoot = ../../../..; - # resolve the members straight from the workspace - workspaceMembers = - (builtins.fromTOML (builtins.readFile (workspaceRoot + "/Cargo.toml"))).workspace.members; dimos-memory-recorder = pkgs.rustPlatform.buildRustPackage { pname = "dimos-memory-recorder"; version = "0.1.0"; src = pkgs.lib.fileset.toSource { - root = workspaceRoot; - fileset = pkgs.lib.fileset.unions ( - [ - (workspaceRoot + "/Cargo.lock") - (workspaceRoot + "/Cargo.toml") - ] - ++ map (member: workspaceRoot + "/${member}") workspaceMembers - ); + root = ../../../..; + fileset = pkgs.lib.fileset.unions [ + ../../../../Cargo.lock + ../../../../Cargo.toml + ../../../../dimos/experimental/memory/rust + ../../../../native/rust/dimos-module + ../../../../native/rust/dimos-module-macros + ../../../../dimos/mapping/ray_tracing/rust + ../../../../dimos/mapping/ray_tracing/rust/py + ../../../../dimos/navigation/nav_3d/mls_planner/rust + ../../../../dimos/navigation/nav_3d/mls_planner/rust/py + ../../../../dimos/hardware/sensors/lidar/livox/rust + ../../../../dimos/hardware/sensors/lidar/virtual_mid360 + ../../../../examples/native-modules/rust + ]; }; cargoLock = {