Repository navigation
refactor: one runtime manifest writer, one Python-environment-root rule, one single-ready-runtime rule and one install-root payload rule - #934
Conversation
…terpreter Three private helpers turn an interpreter path into the environment folder that contains it, and they do not agree. ComfyUI's runtime environment returns a root only when the interpreter's folder is bin or Scripts (any case) and otherwise leaves VIRTUAL_ENV unset. The adopt command and the vLLM launch both take the grandparent whatever the folder is called: the bin/Scripts check in the adopt helper returns the same value on both of its branches, and vLLM never had one. So an interpreter in an unrelated folder is guessed at by two callers and refused by the third. Add tests that pass on the unmodified code and pin each caller's behaviour for bin, Scripts, SCRIPTS, an unrelated folder, a bare file name and a root-level interpreter, including those differences, so the change that makes the callers agree shows exactly what moves. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…rule The adopt command, the ComfyUI runtime environment and the vLLM launch each kept a private copy of "which environment folder holds this interpreter", and the copies disagreed. ComfyUI gave a root only for an interpreter in a bin or Scripts folder; adopt and vLLM took the grandparent whatever the folder was called (adopt's bin/Scripts check returned the same value on both branches). An interpreter in an unrelated folder was refused by one caller and guessed at by the other two. Add runtime_python_env_root_for_executable to rocm-core, beside the helpers it inverts, and use it from all three. An interpreter belongs to an environment only when it sits directly in a bin or Scripts folder, in any case; everything else is None, because a guessed folder is not an environment. The engine crate cannot reach apps/rocm, so rocm-core is the shared place. Valid venv layouts resolve exactly as before. Two things change for an interpreter outside that layout: `rocm runtimes adopt --python` without --root now stops and asks for --root instead of adopting the interpreter's grandparent, and the vLLM launch fails with its existing "not inside a venv" error instead of searching the wrong folder. Each caller's test goes through the shared function, scenario runtime-23 covers the adopt refusal end to end, and the --root help text, README and docs/testing.md now say how the folder is found. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Two private helpers each decide whether an install root holds a payload: has_nontrivial_directory_contents in therock.rs, which refuses to extract a tarball into a non-empty target and decides whether a repair's replacement is already installed, and runtime_install_root_has_payload in runtime_registry.rs, which activation validation applies to a tarball runtime. Both skip dot-entries (the runtime marker is one) and count any other entry, a dangling symlink included. They differ only for a path that does not exist, or that is a dangling symlink: the first answers empty, the second returns an error. Add one table per helper over the same input kinds (missing path, empty directory, dotfile-only, dot-directory-only, regular file, subdirectory, a file as the root, dangling symlink entry, symlinked and dangling roots, unreadable directory) so the next commit, which collapses them into one, can be seen to change nothing but those two rows. Tests only. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…ready runtime With no runtime chosen, `rocm comfyui install` and the engine paths (`serve`, `engines install`) each pick "the one ready runtime" with their own copy of the rule. Both count a runtime as ready by `validate_runtime_manifest_for_activation`, but they differ in what happens around it: the engine path loads config and runs `recover_setup_runtime_registration` first and answers no selection on zero or many (its callers print their own advice), while the ComfyUI path never recovers and refuses on zero or many with its own text. Pin both as they are today, for zero, one and several ready runtimes and for a setup runtime the registry has lost. Each test that checks a refusal also checks the state it describes: that nothing was written, and that the remedy the message names (`rocm runtimes activate`, `--runtime-id`, repairing the runtime) clears it. No production code changes. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…nv-root-rule Signed-off-by: Jussi Elo <jussi.elo@amd.com> # Conflicts: # docs/architecture.md
…nv-root-rule Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…nv-root-rule Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…ngine paths With no runtime chosen, `rocm comfyui install` and the engine paths (`rocm serve`, `rocm engines install`) each kept their own copy of "use the one ready runtime". They agreed on what ready means but not on the steps around it: the engine path recovered the runtime `rocm setup` created before counting, ComfyUI never did, so a runtime whose registry entry had been lost made the install refuse with "Install ROCm first" (or silently pick a different runtime registered beside it) while the same machine served fine. Move the rule to `runtime_adopt::select_single_ready_runtime`, beside `recover_setup_runtime_registration`: recover setup, load the registry, count the runtimes `validate_runtime_manifest_for_activation` accepts, and return one, none (with everything registered) or several (the ready ones). Both callers use it and delete their copy. Each keeps its own words: the engine path still treats none and several as "no selection" and leaves the advice to its callers, and ComfyUI still refuses with its own `/runtimes`, `rocm runtimes activate` and `--runtime-id` text. What changes for a ComfyUI user, only when no `--runtime-id`, active runtime or configured default says which runtime to use: the runtime setup created is found even if its registry entry was lost, and finding it writes the entry back (also under `--dry-run`); with another ready runtime registered beside it the install now refuses as ambiguous instead of picking the other one; and an unusable setup manifest now fails the install with the recovery error instead of being skipped. The engine paths behave as before. The ComfyUI characterisation test for recovery flips to the new behaviour, and a test now runs both callers against the same registry states and fails if they disagree. The e2e scenario comfyui-15 covers the recovery through the dry run. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…election The previous commit makes the ComfyUI install recover the runtime `rocm setup` created before counting ready runtimes, as the engine paths do. A recovery that fails (the setup folder's runtime no longer passes the activation check) is an error there too; pin it for ComfyUI, with the registry left untouched. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
has_nontrivial_directory_contents (therock.rs) and runtime_install_root_has_payload (runtime_registry.rs) were two copies of the same rule: an install root has a payload when it holds an entry whose name does not start with a dot, so the runtime's own .rocm-cli-runtime.json marker does not count. They answered every input identically except a path that does not exist (or a dangling symlink root): the first said "empty", the second failed with "failed to read". No caller can reach that row. Activation validation rejects a root that is not a directory ("install root is missing") before it asks, tarball extraction asks right after create_dir_all, and the update check maps any failure to "not installed" (unwrap_or(false)) anyway.
Keep the first one's answer (a root that is not there holds nothing, so asking never needs an error path for it) and move it to rocm-core as install_root_has_payload next to the other install-root helpers; a root that exists but cannot be listed is still an error. Tarball extraction, the update path's check that a repair's replacement is installed, and activation validation of a tarball runtime now all ask it. The extraction refusal moves into ensure_tarball_install_target_is_clean so the refusal can be tested with its message and the files it leaves alone.
The two per-copy tables from the previous commit are replaced by one table over the shared function, plus a test per caller that goes through it: a marker-only root has no payload for validation (message and unusable status asserted together), for the extraction guard, and for the replacement check. Re-introducing a private copy in a caller, or changing the shared predicate, turns these red. No user-visible change.
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…s os-release The OpenMPI, libatomic and libnuma install hints name package-manager commands for the machine the process runs on, so a simulated host root must not choose their distro. rocm-core's private real_os_release (openmpi.rs) and the apps/rocm one (system_packages.rs) are the same one-line call, read_os_release with the identity resolver; the apps copy already has a test for it, the rocm-core copy had none. Add one that plants a root whose os-release names another distro and checks all three hints are unchanged by it. It lives with the apps copy's test, in the rocm binary's test process, so the environment variable it sets cannot overlap the rocm-core tests that read host paths. Swapping the identity resolver in openmpi.rs for host_path_for_str turns it red. Tests only. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
The OpenMPI, libatomic and libnuma install hints (rocm-core/openmpi.rs), the system-package plans and the driver plan (apps/rocm via system_packages.rs) each need the os-release of the machine the process runs on, not a simulated host root. openmpi.rs and system_packages.rs each carried their own real_os_release, identical line for line: read_os_release with the identity resolver. The e2e driver step spelled the same call a third time. They read the same two files in the same order with the same fallback and error rules, so nothing a user can see differs; the risk was one copy being edited and the others not. Move the reader to rocm-core as hardware_root::real_os_release, delete both private copies, and make the identity resolver private so a caller outside hardware_root.rs has no shared way to build a second reader. The apps copy's test (real_os_release_ignores_a_simulated_host_root) stays where it was and now calls the shared function; the hint test from the previous commit holds the three hints to the same rule, and a scan test in the rocm binary fails if any of its source files names read_os_release or the resolver. The shared function is no longer cfg(linux)-gated as openmpi's was; off Linux it reads the same paths and finds nothing, as the apps copy always did. Doc comments, docs/architecture.md and docs/testing.md that named the two copies are updated. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
A runtime has up to two manifest files: its registry entry and the .rocm-cli-runtime.json that ROCm CLI keeps in the install root. Installing writes both and always overwrites. Importing and adopting write the registry entry only and refuse to replace an existing entry unless asked, and recovering the runtime that setup created restores a missing registry entry from the manifest already in its folder. Nothing asserted that file set per path. These tests assert, for install, import, adopt and setup recovery, which of the two files exist afterwards, and for import and adopt that a duplicate is refused (message and untouched entry together) until replacement is requested. They pass on the current code, so they pin its behaviour before the writers are merged. The import scenario gains an assertion that the registry holds the entry and that the imported folder gains no manifest file, after the first import and after a replacing one. The adopt test's environment fixture moves into a helper so the new adopt test reuses it. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Two functions wrote a runtime's manifests. The SDK install path wrote the registry entry and the .rocm-cli-runtime.json in the install root and always overwrote; the import, adopt and setup-recovery path wrote only the registry entry and refused to overwrite unless --replace was given. Which files a runtime got depended on which function its caller happened to use, and nothing tied the choice to the thing it stands for. write_runtime_manifest in runtime_registry.rs is now the only writer. It takes the overwrite policy (ManifestOverwrite::Replace or ::Refuse) as its one parameter and decides the install-root file from InstalledRuntimeManifest::owns_install_root: a runtime ROCm CLI installed gets it, an imported or adopted one, which is someone else's folder that uninstall and prune never delete, does not. The private writer in runtime_adopt.rs is gone. The file is left alone when it already holds what would be written, so recovering a runtime from its own manifest does not rewrite it. No user-visible file set changes: install still writes both files and replaces, import and adopt still write the registry entry only and refuse a duplicate without --replace, recovery still only fills a missing entry. The test that pins this per path stays green across the change, and new tests tie the writer's ownership rule to uninstall's, check each policy and the skipped rewrite, and fail if production code outside runtime_registry.rs opens a registry entry for writing itself. Neither file is written atomically; that is left to the shared atomic writer work. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
There was a problem hiding this comment.
🟡 Changes recommended
Byte comparison misclassifies valid legacy manifests as changed, causing setup recovery to fail when it attempts to rewrite a read-only sidecar after restoring the missing registry entry.
1 open issue
What changed in this PR
This PR centralizes runtime registration in one manifest writer, making overwrite policy explicit and using runtime ownership to decide whether to write an install-root manifest.
Changes:
- Routes SDK installation, import, adoption, and setup recovery through
write_runtime_manifest. - Preserves registry-only writes for imported and adopted runtimes and skips byte-identical install-root rewrites.
- Adds regression coverage and documents the shared writer’s behavior.
Validation was limited to source inspection; project tests were not run.
| File | Description |
|---|---|
tests/e2e-cucumber/tests/e2e/runtime_lifecycle_steps.rs |
Adds assertions for registry-only import writes. |
tests/e2e-cucumber/features/runtime_lifecycle.feature |
Checks file sets after initial and replacement imports. |
docs/testing.md |
Documents registration behavior and focused tests. |
docs/architecture.md |
Describes the shared writer and ownership policy. |
apps/rocm/src/therock.rs |
Routes both SDK installation paths through the shared writer. |
apps/rocm/src/runtimes_cmd.rs |
Adds registration, replacement, recovery, and ownership tests. |
apps/rocm/src/runtime_registry.rs |
Implements the shared writer, overwrite policy, and ownership predicate. |
apps/rocm/src/runtime_adopt.rs |
Removes the duplicate writer and migrates registration callers. |
Review effort: Balanced
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Only docs/architecture.md conflicted: this branch reworded the runtime_registry.rs entry and the clap-tree move edited the neighbouring runtime_services.rs entry. Keeps this branch's runtime_registry.rs entry and main's runtime_services.rs entry. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…nv-root-rule The only conflict was apps/rocm/src/main.rs: main moved the clap command tree into cli.rs, so the `rocm runtimes adopt` --root doc-comment edit from this branch is re-applied there and main.rs is taken as on main. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…eady-runtime-rule The only conflict was docs/architecture.md, in the engine_ipc.rs and engine_runtime_select.rs entries. Both sides edited them: main added the note that the WatcherModeArg conversion moved to cli.rs, and this branch added the sentence saying single_ready_runtime_key asks select_single_ready_runtime. Both are kept. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…root-payload-and-os-release-rule The only conflict was docs/architecture.md, in the entries for runtime_registry.rs, runtime_services.rs, system_packages.rs and therock.rs. This branch edits the registry, system_packages and therock entries; main re-pointed the runtime_services and therock entries from main.rs to cli.rs. The therock entry carries both edits; the other three take whichever side changed them. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…istry entry With no runtime chosen, `rocm comfyui install` now recovers the runtime `rocm setup` created before counting the ready ones, and recovering writes a lost registry entry back. That ran before the install looked at `--dry-run`, so a preview changed a file even though the flag is documented as "Show what would happen without changing files". The recovery is split into finding the setup runtime (read-only) and writing its entry. `select_single_ready_runtime` takes a `SetupRecovery`: `Persist` writes the entry as before, and `PreviewOnly` counts the setup runtime from its own manifest, normalised and ordered as the registry would return it once restored, and writes nothing, so the preview names the runtime the real run will use. The engine paths keep `Persist`; the install derives the mode from `--dry-run`. The flag help stays true, so no wording changes. comfyui-15 now asserts that the preview selects the runtime and the registry still has no entry, and comfyui-16 runs the install against an existing checkout and asserts the entry is written back. Unit tests pin both modes, that preview and real run list the same ready runtimes in the same order, that an unusable setup runtime is reported alike, and the dry-run mapping. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…ecodes to The shared manifest writer skips rewriting the `.rocm-cli-runtime.json` in an install root when it already holds what would be written, so recovering the runtime `rocm setup` created does not fail on a folder the user cannot write to. It compared bytes. A manifest an older binary wrote omits keys that now read back as defaults (`wheel_composition` and other optional fields) and serialize as explicit values, so the bytes differ although the manifest is the same. Recovery then wrote the missing registry entry and failed rewriting the otherwise usable, read-only file. The writer now also treats a file as current when it decodes to the manifest that would be written once normalised for storage (`install_root_manifest_is_current`). A different manifest, or a file that does not decode, is still rewritten. The regression test recovers a setup runtime whose read-only install root manifest is in the older shape and asserts the registry entry is restored and the file's bytes are untouched; a pure test covers the equal, same-manifest, different-manifest and not-a-manifest cases on every platform. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Signed-off-by: Jussi Elo <jussi.elo@amd.com> # Conflicts: # docs/testing.md
Signed-off-by: Jussi Elo <jussi.elo@amd.com> # Conflicts: # apps/rocm/src/runtime_adopt.rs # apps/rocm/src/runtime_registry.rs
There was a problem hiding this comment.
🟡 Changes recommended
The runtime-selection match suppresses persistence errors for usable setup runtimes, misleadingly reporting “Install ROCm first” instead of propagating the actual failure.
1 open issue
1 resolved since last review
Review effort: Balanced
…ck instead of refusing A choice that names a runtime (`--runtime-id`, the active runtime, the configured default) finds the runtime `rocm setup` created when its registry entry was lost. Looking it up discarded every error from that second look, the failure to write the entry back included. With the registry readable but not writable, `rocm comfyui install --dry-run` succeeded, and running the command it printed answered "Install ROCm first" (or "runtime not found") about a runtime that was there, instead of saying the entry could not be restored. The setup runtime is now found and picked without writing, whichever recovery was asked for, and a real run writes the entry back only for a pick it is going to use, propagating that error as it is. Not finding a usable setup runtime still leaves the registry's own refusal, so a choice naming something else and an unusable setup runtime answer as before. The engine paths already propagated the same failure (`rocm serve` and `rocm engines install` through `resolve_runtime_selector_to_exact_key` and the one-ready-runtime rule); nothing pinned it, so a unit test now covers the explicit and the default selection there. Covered by unit tests for each way of naming the runtime, with the preview succeeding and the real run reporting the error and leaving the registry as it was, and for a read-only registry directory holding other runtimes; and by scenario comfyui-18, which runs the printed command against a registry that cannot be written. The fixture is a plain file where the registry directory should be, because a read-only directory does not stop root. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
juhovainio
left a comment
There was a problem hiding this comment.
Thanks for this. I read through the full diff and CI is green. I found nothing I'd call blocking, but a few things are worth a look.
Worth addressing or confirming
- Scope: the PR itself says the four changes (877/878/880/882) are independent, and each ticket asks for one PR. At ~4.7k lines and 27 commits it is a lot to review at once. Splitting would help, unless the maintainers are fine landing it together.
rocm comfyui installwith no runtime chosen now fails on an unusable setup runtime even when another ready runtime is registered, while the named-choice path keeps the registry's answer. Is that asymmetry intended? No test covers that row (see inline comment).- In recovery, the registry entry is written before the install-root file. If the second write fails, the error blames the registry although the entry now exists, and the next run succeeds silently (see inline comment).
only_the_shared_routine_writes_a_registry_entryis a fairly loose guard: a 200-char window, substring matching, and exemption by file name. It could miss a writer that builds its path another way.vllm_venv_rootnow refuses an interpreter that is not directly in bin/Scripts. I couldn't confirm from the diff that only hand-edited manifests reach it, so it would be good to confirm that.
Questions
- The PR text says setup recovery "needs no file written", but recovery can now rewrite the install-root manifest when it differs. Maybe "normally" is the accurate word.
- For 877, the ticket framed the missing install-root file on imported/adopted runtimes as the defect, while the PR keeps it absent and explains why. That direction is worth confirming with the ticket owner.
adopt --pythonwithout--rootnow refuses interpreters outside bin/Scripts. The e2e scenario (runtime-23) is Linux-only, so other platforms rely on unit tests.
Approving on the strength of the diff read and green CI; the points above are non-blocking.
| }), | ||
| // Nothing says which runtime to use: the rule the engine paths share. | ||
| None => { | ||
| single_ready_runtime_or_refusal(select_single_ready_runtime(paths, config, recovery)?) |
There was a problem hiding this comment.
With no choice made, select_single_ready_runtime(.., Persist)? propagates the setup recovery error, so a stale or unusable setup folder now fails rocm comfyui install even when another ready runtime is registered. The named-choice path (pick_from_registry) deliberately keeps the registry's answer in that case. Is the asymmetry intended? No test covers "no choice + one ready registered runtime + unusable setup runtime".
There was a problem hiding this comment.
Not intended, and the asymmetry was real: a choice that names a runtime let the registry answer, while with nothing chosen the setup recovery error was propagated, and rocm serve and rocm engines install reach the same function, so they failed the same way. A setup runtime that cannot be used is not a ready runtime and cannot change which runtimes are ready, so failing on it while another runtime was ready told the user about a folder that had no say in the answer. fa5b41e fixes it in the shared rule (select_single_ready_runtime in runtime_adopt.rs): when the setup runtime is unusable, the registry alone is counted, so one ready runtime is picked, two are refused as ambiguous with exactly those two named, and nothing is written. The setup runtime's own error is still the answer when the registry holds no ready runtime (it was the only candidate), and a usable setup runtime whose entry cannot be written back is still reported as that failure, because it is one of the runtimes being counted. One consequence to be aware of: since the rule is shared, rocm serve and rocm engines install now pick the registered runtime in that corner too; their other recovery call sites are unchanged.
The missing row is covered by comfyui_runtime_selection_state_matrix, which runs every combination of the choice (none, a registered runtime by name, the setup runtime by name), 0, 1 or 2 ready registered runtimes, a setup folder that is absent, usable or unusable, a registry that takes writes or not, and preview or real run (96 rows), asserting the message and the registry entries together. The row you named fails without the change (it reports the setup error instead of picking the registered runtime) and passes with it; reverting only the "keep the error when nothing else is ready" half fails the row with no registered runtime, and swallowing the write failure fails the unwritable-registry rows. Also added: an_unusable_setup_runtime_does_not_hide_the_registrys_ready_runtimes for the rule itself, a new row in comfyui_and_engine_paths_agree_on_the_single_ready_runtime for the engine side, and scenario comfyui-19 for the install preview.
| let already_current = fs::read(&local_manifest_path) | ||
| .is_ok_and(|existing| install_root_manifest_is_current(&existing, &encoded)); | ||
| if !already_current { | ||
| fs::write(&local_manifest_path, &encoded) |
There was a problem hiding this comment.
The registry entry is written before this install-root file. If this write fails during setup recovery, the caller reports "failed to restore setup runtime ... into " although the entry now exists, and the next run succeeds silently. Consider writing the install-root file first, or asserting and documenting this state; no test pins message plus state for this branch.
There was a problem hiding this comment.
Agreed on the ordering, and f82b274 changes it: the install-root file is now written first and the registry entry last, so a failed install-root write leaves the registry exactly as it was and a failed registry write leaves no new entry. An error that says a runtime could not be registered or restored therefore never describes one whose entry exists (a failed registry write can leave the install-root file behind, which holds what a retry writes, and the doc comment says so). a_failed_manifest_write_never_leaves_a_registered_runtime_behind pins the error and the registry together for each failing write under both overwrite policies, and fails with the old order.
On reachability, I also checked the branch you described and it cannot be reached from setup recovery: the manifest recovery restores is decoded from that very install-root file, so the file is always what would be written and the skip applies, however hand-edited its shape. setup_recovery_restores_the_entry_and_leaves_a_hand_edited_install_root_file_alone pins that on the bytes (so it holds for an administrator too) and fails if the skip is removed. The PR text said recovery rewrites the file when it decodes to a different manifest; that was wrong and is corrected in the description. The second write is only reachable from an SDK install, where the error names the file that failed.
…runtime is ready
With no runtime chosen, `rocm comfyui install` failed with the setup recovery error ("setup runtime ... is not usable") whenever the folder `rocm setup` named held a runtime that no longer passed the readiness check, even when exactly one other runtime was registered and ready. A choice that names a runtime already keeps the registry's answer in that case, so the two ways of asking disagreed, and the engine commands reached the same rule through the same function.
A setup runtime that cannot be used is not a ready runtime and cannot change which runtimes are ready, so `select_single_ready_runtime` now counts the registry alone when the setup runtime is unusable: one ready runtime is picked, several are refused as ambiguous, and nothing is written. When the registry holds no ready runtime the setup runtime was the only candidate and its error is still the answer. A usable setup runtime whose registry entry cannot be written back is unchanged and still reported as that failure, because it is one of the runtimes being counted.
The rule is shared, so `rocm serve` and `rocm engines install` pick the registered runtime in this corner too; their other recovery call sites are untouched.
The state matrix is a test of its own: the choice (none, a registered runtime, the setup runtime) against 0, 1 or 2 ready registered runtimes, a setup folder that is absent, usable or unusable, a registry that takes writes or not, and preview or real run, each row asserting the message and the registry entries together. Scenario comfyui-19 covers the install preview with an unusable setup runtime beside one ready registered runtime.
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
`write_runtime_manifest` wrote the registry entry first and the install-root `.rocm-cli-runtime.json` second. When the second write failed, the caller's error (for setup recovery, "failed to restore setup runtime ... into <registry>") described a runtime that was in fact registered, and the next run succeeded without saying anything. The registry entry is what registering a runtime means, so it is now written last: a failed install-root write leaves the registry exactly as it was, and a failed registry write leaves no new entry, so an error that says a runtime could not be registered or restored matches the registry. A failed registry write can leave the install-root file behind, which holds what a retry writes. Reaching the install-root write from setup recovery is not possible in practice: the manifest recovery restores is the one decoded from that very file, so the file is always what would be written and is skipped. A test pins that with a hand-edited file, asserted on the bytes so it holds for an administrator as well. A second test pins the error and the registry together for each failing write, under both overwrite policies. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…s, not a window `only_the_shared_routine_writes_a_registry_entry` looked 200 characters past each `fs::write(` for three names and exempted any file called `test_support.rs`. A writer that built its path another way (the `runtimes` and `registry` segments joined into a variable, one literal, a path held away from the write) passed it; a storage-module function doing exactly that stayed green. The guard now treats a production file as a writer when it both calls a write primitive and names the registry directory by any of its spellings (the two path helpers, the two path segments, or one literal holding both), wherever the two sit in the file. Comments are removed and test modules cut before the scan. It reads the `rocm` app, `rocmd`, `rocm-core` and the engines. The files that call the two path helpers are an explicit reviewed list that is compared both ways, and the two fixture modules are exempt because their declarations are `#[cfg(test)]`, which the test checks, instead of by file name. `registry_writer_detector_sees_every_way_of_building_the_path` pins the detector on nine cases, including the blind spots above. What the guard still does not catch is written in its doc comment: a writer handed the registry path by a function in another file, a write primitive outside its list, and code outside the scanned roots. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
The new state-matrix test tripped three pedantic lints that the workspace denies: constants declared after statements, a two-armed match that is an `if let`/`let else`, and a missing semicolon after an assertion. No change to what the test checks. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
|
Thanks for the careful read. Going through the points in order.
Questions:
|
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…on is chained on it The single-registry-writer guard treated every `OpenOptions::new(` as a write primitive. `amd_smi.rs` builds the registry directory path only to read it and opens device files with `OpenOptions::new().read(true).open(..)`, so the guard flagged it as a second writer and failed on the merged tree. The detector now reads the method chain after `OpenOptions::new(` or `File::options(` across lines and whitespace: the builder is a read-only probe when the chain reaches `.open(` and calls none of `write`, `append`, `create`, `create_new` or `truncate`. A builder kept in a variable, or a chain that never reaches `.open(`, cannot be judged and still counts as a writer. Every other write primitive is unchanged. `registry_writer_detector_sees_every_way_of_building_the_path` pins eight more cases: the amd_smi shape, read-only probes on one line, over several lines and through `File::options`, and the writing forms (`.write(true)`, a write option on a later line, `.append(true)`, a stored builder, an unreadable chain). The only file under the scanned roots that names the registry directory and uses a file builder is `amd_smi.rs`, so no other file changes verdict. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
One conflict, in tests/e2e-cucumber/features/runtime_setup.feature: main added runtime-23 to runtime-25 (the SDK install preview scenarios) at the end of the file, where this branch added its adopt-outside-a-venv scenario as runtime-23. Both are kept; this branch's scenario is renumbered runtime-26, with its `@id` unchanged, and the references to it in the step definitions and docs/testing.md follow. comfyui.feature, comfyui_steps.rs and docs/testing.md merged without conflict. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Cross-platform filesystem changes to runtime persistence and setup recovery require maintainer confirmation of native Windows testing and full-workspace validation on the combined tree.
No open issues 🎉
Review effort: Balanced

Closes ROCMAI-877, ROCMAI-878, ROCMAI-880 and ROCMAI-882.
Summary
This PR collapses four pairs of duplicated rules in the runtime-install code into one definition each, so that the copies can no longer drift apart. Each rule had two or three private implementations that agreed on the common case and disagreed on the edges, and the edges are what users hit: which manifest files a registered runtime ends up with, which folder a Python interpreter belongs to, which runtime "the one ready runtime" is, and what counts as a non-empty install root or the real host's os-release. The four changes are independent of each other and are delivered here in reading order, one section each below; they are bundled in one PR on purpose, to cut review overhead, and each ticket is named on the first line. Each one was written as a tests-first pair of commits (tests that pin current behaviour and pass on the unmodified code, then the change), and the history keeps those commits so every behaviour change can be read against the test that moves with it. The branch also carries three signed merge commits that join the four lines of work; the only conflicts were in
docs/testing.md,docs/architecture.mdand, for the ComfyUI and registry work,runtime_adopt.rsandruntime_registry.rs, and the resolution keeps both sides' content.Two of the four change what a user can observe, in narrow cases:
rocm runtimes adopt --python <file>without--rootnow refuses an interpreter that is not in abinorScriptsfolder (and the vLLM launch fails up front for the same layout), androcm comfyui installnow finds the runtime thatrocm setupcreated even when its registry entry was lost, the wayrocm servealready did. A setup folder whose runtime is no longer usable no longer blocks that pick while a registered runtime is ready, for the ComfyUI install and, because the rule is shared, forrocm serveandrocm engines installtoo. The other two are behaviour-preserving deduplications with a source-reading or shared-table test that fails if a second copy comes back.One manifest writer (ROCMAI-877)
The two functions that wrote a runtime's manifests are now one. A registered runtime can have two manifest files, its registry entry under the data folder and a
.rocm-cli-runtime.jsonin its install root. The SDK install path (save_runtime_manifest) wrote both and always overwrote; the import, adopt and setup-recovery path (write_runtime_registry_manifestinruntime_adopt.rs) wrote only the registry entry and refused to overwrite unless--replacewas given. Which files a runtime ended up with depended on which of the two a caller happened to pick, and nothing tied that choice to what it was meant to express.write_runtime_manifestinruntime_registry.rsis now the only writer, and every way of registering a runtime goes through it: an SDK install (both the wheel and the tarball path),rocm runtimes import,rocm runtimes adopt, and the recovery of the runtimerocm setupcreated. Its one parameter is the overwrite policy,ManifestOverwrite::Replaceor::Refuse. Whether the install-root file is written is no longer a property of the caller: it follows fromInstalledRuntimeManifest::owns_install_root(not read-only and not imported), the same rule uninstall and prune apply when they decide whether they may delete a folder. The private writer inruntime_adopt.rsis deleted.runtime_registry_dirandruntime_manifest_pathwere already one definition each, so there was nothing left to deduplicate there.The ticket framed the missing install-root file on imported, adopted and recovered runtimes as the defect. Reading what consumes that file, the install path is the odd one out only in the sense that it is the one runtime source ROCm CLI owns, and the file is not meant to appear elsewhere. Uninstall and prune delete an install folder only when it carries a
.rocm-cli-runtime.jsonmatching the registry entry, and both checkread_onlyandimported_fromfirst, so for an imported or adopted runtime the file is never consulted. Activation requires the file only when the runtime is not read-only. Import and adopt register a folder somebody else owns, androcm runtimes adoptis documented as not writing into the environment; an existing test and the adopt test already assert the folder gains no manifest file, and an import's source can itself be another install's.rocm-cli-runtime.json, which an unconditional write would overwrite. Setup recovery needs no file written either: it reads the runtime's manifest from the.rocm-cli-runtime.jsonalready in the setup folder and only restores the missing registry entry, and it never rewrites that file. So the converged behaviour keeps both file sets as they were and makes the rule that selects between them explicit and shared. The overwrite policy is kept as it was too: an install replaces its own entry, import and adopt refuse an existing one unless--replaceis given, and recovery refuses so that it only ever fills a missing entry. If you would rather have the file on every registered runtime, that is a one-line change inwrite_runtime_manifest, and the tests that pin the current file sets say exactly what flips.runtime_registry::save_runtime_manifest(paths, manifest)(registry entry plus install-root file, always overwrites)runtime_registry::write_runtime_manifest(paths, manifest, ManifestOverwrite::Replace)runtime_adopt::write_runtime_registry_manifest(paths, manifest, replace)(registry entry only, refuses unlessreplace)write_runtime_manifest(paths, manifest, ManifestOverwrite::for_replace_flag(replace))for import and adopt,::Refusefor recovery; the private function is deletedshould_remove_runtime_install_root) and prune (HoldReason::NotOwned)InstalledRuntimeManifest::owns_install_rootstates the same rule for the writer, and a test ties the writer's decision toshould_remove_runtime_install_rootOne small behaviour addition: the install-root file is not rewritten when it already holds what would be written, judged on the decoded manifest and not on the bytes. Recovery restores the registry entry from that file, so without this it would rewrite the file it just read, and a setup folder the user cannot write to would turn a recovery that used to succeed into an error. A manifest written by an older binary omits optional keys that now read back as defaults and are written out as explicit values, so a byte comparison would have read such a file as changed;
install_root_manifest_is_currenttreats a file that decodes to the manifest being written, once normalised for storage, as current. For recovery this means it never rewrites the file, whatever shape the file has: the manifest it restores is decoded from that very file, so the file always holds what would be written (a test pins this on the bytes with a hand-edited file, so it also holds for an administrator). The file is rewritten only when a different manifest is written over it, which is an SDK install replacing its own runtime. A file written byrocm install sdkround-trips and is left alone, and so is an older file that differs only in keys that hold no value.The registry entry is written last, after the install-root file, because the entry is what registering a runtime means. A failed install-root write therefore leaves the registry exactly as it was, and a failed registry write leaves no new entry, so an error that says a runtime could not be registered or restored never describes one whose entry exists; before, the entry was written first and a failing second write reported a failure for a runtime that was in fact registered. A failed registry write can leave the install-root file behind, which holds what a retry writes. Neither file is written atomically; that stays with the shared atomic-writer work (#610, #811).
A source-reading test,
only_the_shared_routine_writes_a_registry_entry, fails if a second writer comes back: production code in therocmapp,rocmd,rocm-coreor the engines that both names the registry directory (through the two path helpers, or by spelling theruntimesandregistrysegments or one literal holding both) and calls a write primitive (a file builder such asOpenOptionscounts only when a write option is chained on it, so a read-only probe in a file that builds the registry path, asamd_smi.rsdoes, is not a writer), wherever the two sit in the file; and a file outside a short reviewed list calling the two path helpers.registry_writer_detector_sees_every_way_of_building_the_pathpins what the detector sees. It does not catch a writer that is handed the registry path by a function in another file, a write call that is not in its list of write primitives, or code outside those source roots; its doc comment says so.One rule for finding a Python environment root (ROCMAI-878)
Three places turned a Python interpreter's path into "the environment folder this interpreter belongs to", and they gave different answers for the same path. The ComfyUI runtime environment returned a folder only when the interpreter sat in a
binorScriptsfolder (any case) and otherwise leftVIRTUAL_ENVunset.rocm runtimes adoptand the vLLM launch both took the interpreter's grandparent whatever its own folder was called: the adopt helper had abin/Scriptscheck, but both of its branches returned the same value, so it was dead code, and the vLLM copy never had a check at all. An interpreter in some unrelated folder was therefore refused by one caller and guessed at by the other two, and a guess names a folder that is not an environment.The three private copies are replaced by one rule in
rocm-core,runtime_python_env_root_for_executable, next to the existingruntime_python_env_bin_dirandruntime_python_executable_in_envthat it inverts. An interpreter belongs to an environment only when it sits directly in abinorScriptsfolder, in any letter case and on any host, and the answer is that folder's parent. Anything else returnsNone: another folder, a bare file name, a file at the filesystem root. The engine crate cannot depend onapps/rocmandrocmdmust never depend onrocm, sorocm-coreis the one place all three callers can reach, andcargo xtask check-crate-edgespasses with no new edge. The ComfyUI behaviour wins because refusing is the only answer that cannot be wrong: the grandparent of an interpreter in/opt/tools/pythonis/opt, whichadoptwould have registered as a runtime and the vLLM launch would have searched forlib/python3.*/site-packages.infer_python_env_rootinruntime_adopt.rs(grandparent, deadbin/Scriptsbranch)resolve_adopt_python_inputcallsrocm_core::runtime_python_env_root_for_executableinfer_python_env_rootincomfyui/runtime_env.rs(bin/ScriptsorNone)venv_rootsites call the shared functionpython.parent().and_then(Path::parent)invllm_venv_root,engines/vllm/src/process.rsruntime_python_env_root_for_executableincrates/rocm-core/src/runtime.rs, re-exported at the crate rootOnly the two callers that guessed change, and only for an interpreter that is not directly inside a
binorScriptsfolder. Forrocm runtimes adopt --python <file>without--root, where the file's folder is neither, the command used to adopt the interpreter's grandparent as the install root; now it stops with the existing messagecould not infer the Python environment folder from <path>; pass --rootand registers nothing, and passing--rootstill adopts that same interpreter. The--roothelp text and the README'sadoptparagraph say this (the help text used to call--root"Optional SDK root path"). For the vLLM launch on a ROCm 10.x discovery install whose recorded interpreter is not in abinorScriptsfolder, the grandparent used to be taken as the venv; now it fails up front with the error that was already written for this case,python executable ... is not inside a venv (bin/Scripts) layout. An SDK install never records such an interpreter (it records its own environment'sbinorScriptsinterpreter), so the refusal is reachable only through a registered manifest that does: a hand-edited or imported one, orrocm runtimes adopt --python <file> --root <dir>with an interpreter outside such a folder. ComfyUI is unchanged:VIRTUAL_ENVis set forbin,Scripts,SCRIPTSandBIN, and left unset otherwise. A relative interpreter path such asbin/pythonstill yields an empty folder from all three callers, as before; adopt makes its input absolute first and manifests store absolute paths.One rule for the single ready runtime, and the setup-recovery preview (ROCMAI-880)
With no runtime chosen,
rocm comfyui installand the engine commands (rocm serve,rocm engines install) each carried their own copy of "use the one ready runtime". The copies agreed on what ready means (both end invalidate_runtime_manifest_for_activation) but not on the steps around it. The engine copy first recovered the runtimerocm setupcreated (recover_setup_runtime_registration) and only then counted; the ComfyUI copy never recovered. So when a setup runtime's registry entry had been lost,rocm servefound the runtime and wrote the entry back, whilerocm comfyui installon the same machine refused with "Install ROCm first", or, if another runtime was registered next to it, quietly installed into that one.The rule is now one function,
runtime_adopt::select_single_ready_runtime, next torecover_setup_runtime_registration: find the setup runtime, load the registry, count the runtimes the activation check accepts, and return one, none (with everything registered, so a caller can say why each one does not count) or several (the ready ones). Both callers use it and the duplicate is deleted. What each caller does with "none" and "several" stays its own: the engine path still treats both as "no selection" and leaves the advice to its callers, and ComfyUI still refuses with its own/runtimes,rocm runtimes activate <key>and--runtime-id <key>text. ComfyUI recovers setup because the recovery only restores an entry the install tree itself still carries, the engine commands already do it, and a user who just ran setup should not see a different answer fromserveand fromcomfyui install.The rule takes a
SetupRecovery.Persistis what real installs and the engine paths use: the lost registry entry is written back, through the shared writer from the first section withManifestOverwrite::Refuse.PreviewOnlyis whatrocm comfyui install --dry-runuses (SetupRecovery::for_dry_run): the setup runtime is counted from its own manifest, sorted newest-install-first as the registry will once the entry is back, and nothing is written, so the preview names the runtime the real run will use and a--dry-rundoes not restore the registry entry. Both modes report an unusable setup runtime the same way. One thing a dry run still does, because it already did before this change, ispaths.ensure()at the top ofinstall(), which creates the app directories; it does not touch the registry.engine_runtime_select.rs(single_ready_runtime_key) and private copy incomfyui/runtime_env.rs(select_single_ready_runtime)runtime_adopt::select_single_ready_runtime(paths, config, SetupRecovery), result typeSingleReadyRuntime, pure halfclassify_single_ready_runtime, plus the privatefind_setup_runtimethat recovery and the preview shareready.as_slice()single_ready_runtime_keymapsOneto the key andZero/Manyto no selection, withPersistruntime_usability_status == "ready", worded the three casessingle_ready_runtime_or_refusalwords the answer only;select_runtimetakes theSetupRecovery, whichinstallderives from--dry-runwrite_test_setup_runtime_missing_from_registry,runtime_registry_entry_namesintest_support.rsEvery difference is on the path where nothing else says which runtime to use (no
--runtime-id, no active runtime, no configured default), and the engine commands behave as before except in the one corner described at the end of this section. On the ComfyUI side: a ready runtime that only setup recovery can make visible is now found byrocm comfyui install, and a real install writes its registry entry back, asrocm serveandrocm examinealready do; before, it refused with "Install ROCm first". A--dry-runfinds the same runtime, prints the sameinstall command:line naming it, and leaves the registry empty. That printed command works from the state the preview left: a choice that names a runtime (--runtime-id, the active runtime or the configured default) is answered by the registry first, and when the registry alone cannot answer it is looked up again among the registry plus the setup runtime (pick_from_registryincomfyui/runtime_env.rs, overruntime_adopt::registered_runtimes_with_setup, the list the single-ready rule also counts from), the way the engine paths already resolve an explicit--runtime-id.SetupRecoverystill decides the write, so previewing the printed command writes nothing and running it writes the entry back; an unusable setup runtime leaves the registry's own refusal in place instead of an error about a runtime the user did not name. A failure to write the entry back is a different case and is not hidden behind that refusal: the setup runtime is found and picked without writing, whichever recovery was asked for, and a real run writes the entry back only for a pick it is going to use and reports a failed write as it is (failed to restore setup runtime <key> into <registry>), where it used to answer "Install ROCm first" or "runtime not found" about a runtime that was there. The engine commands already reported that failure on their explicit and default paths, and a unit test now pins both. When such a recovered runtime sits beside another ready runtime, the install now refuses as ambiguous ("Multiple ROCm runtimes are ready ...") instead of silently picking the other one.If the setup folder's runtime no longer passes the activation check, or its manifest is unreadable or points elsewhere, it is not a ready runtime and cannot change which runtimes are ready, so it is set aside while the registry holds a ready runtime: that one is picked, several are refused as ambiguous with exactly those named, and nothing is written. That is the answer a choice that names a runtime already got from the registry, and the two ways of asking no longer disagree. Only when the registry holds no ready runtime is the recovery error returned ("setup runtime
<key>... is not usable"), because the setup runtime was then the only candidate; that is what the engine commands already did and ComfyUI now does too. A usable setup runtime whose entry cannot be written back is unchanged: it is one of the runtimes being counted, so the failed write is reported as it is, whatever the count would have said. Because the rule is shared,rocm serveandrocm engines installnow also pick the registered runtime when the setup folder's runtime is unusable and one other runtime is ready, where they used to fail with the recovery error; their other recovery call sites (for example the environment lookup for the lemonade engine) are unchanged and still report an unusable setup runtime. Unchanged and pinned: zero registered still says "Install ROCm first: ...", registered-but-unusable still lists each runtime with its reason under "No ROCm runtime is ready", and several ready still names/runtimes,rocm runtimes activate <key>and--runtime-id <key>and lists the ready keys. The tests assert each message together with the state it describes and run the remedy it names;comfyui_runtime_selection_state_matrixcovers the choice (none, a registered runtime, the setup runtime) against 0, 1 or 2 ready registered runtimes, a setup folder that is absent, usable or unusable, a registry that takes writes or not, and preview or real run, 96 rows, each asserting the message and the registry entries together.One definition of a non-empty install root, and of the real host's os-release (ROCMAI-882)
Two pairs of helpers had each been written twice and could drift apart; they are collapsed to one definition apiece, with a test that fails if a second copy is reintroduced. Neither pair differed in anything a user can observe, so this is a deduplication and not a behaviour change, and both pairs were characterised first in tests-only commits so the equivalence is shown by tests rather than asserted.
The first pair answers "does this install root hold anything?".
has_nontrivial_directory_contentsintherock.rs(the tarball extraction guard, and the update path's check that a repair's replacement is already installed) andruntime_install_root_has_payloadinruntime_registry.rs(activation validation of a tarball runtime) both ignore dot-entries (the runtime's own.rocm-cli-runtime.jsonmarker is one) and count every other entry, a dangling symlink included. They differ in exactly one input: a path that does not exist, or a dangling-symlink root. The first says "empty", the second fails with "failed to read". No caller can reach that row: activation validation rejects a root that is not a directory ("install root is missing") before it asks, tarball extraction asks right aftercreate_dir_all, and the update check maps any failure to "not installed" anyway. The surviving definition is the first one's answer, nowrocm_core::install_root_has_payloadinruntime.rs, sorocmdcan use it without depending onrocm. A missing path holds nothing; a root that exists but cannot be listed is still an error.The second pair reads the real host's os-release for package-manager plans and install hints (a simulated host root must not choose the distro those commands are written for). The reader in
rocm-core/openmpi.rsand the one inapps/rocm/src/system_packages.rswere the same line,read_os_release(identity_path_for_str), with the same two paths, the same fallback and the same error rules, and there was a third spelling of the call in the e2e driver step. The reader is nowrocm_core::hardware_root::real_os_release, the other copies are deleted, and the identity resolver is private tohardware_root.rs, so a caller outside it has no shared way to build a second reader.real_os_releaseis no longer Linux-gated inrocm-core; off Linux it reads the same two paths and finds neither, as therocmcopy always did.therock::has_nontrivial_directory_contentsrocm_core::install_root_has_payload(runtime.rs, re-exported at the crate root)runtime_registry::runtime_install_root_has_payloadrocm_core::install_root_has_payloadinstall_tarball_runtimetherock::ensure_tarball_install_target_is_clean(same message, now testable)openmpi::real_os_release(Linux only),system_packages::real_os_releaseand the inlineread_os_release(identity_path_for_str)in the e2e driver steprocm_core::hardware_root::real_os_releasehardware_root::identity_path_for_str(pub)How the two install-root checks answered each input before this change: every row is identical in both except the first two, which were "empty" versus "error" and are now "empty". Empty directory and a root holding only dot-files or a dot-directory are empty; any other file or directory, a dangling symlink entry inside, and a symlink to a populated directory as the root are a payload; a root that is a regular file, and a directory that cannot be listed, are errors.
Verification
Run on this combined tree on Linux (WSL), debug build, one private target directory, targeted only. The branch's earlier verification is in the first list; the second list is what ran after the last three commits, which answer the approving review.
cargo fmt --all --checkandcargo clippy -p rocm -p e2e-cucumber --all-targets -- -D warnings: clean.cargo test -p rocm --bin rocm -- runtime setup_recovery install_root os_release python_env comfyui single_ready preview payload adopt only_the_shared engine_selection real_os_release: 367 passed, 0 failed, 4 ignored.cargo test -p rocm-core --lib -- install_root runtime_python os_release payload: 34 passed.cargo test -p rocm-engine-vllm -- venv_root: 3 passed.cargo xtask manifest --checkandcargo xtask check-crate-edges: clean.cargo test -p e2e-cucumber --test feature_naming: 4 passed.cargo xtask e2e -- -n "(comfyui-1[56]|runtime-23|runtime-lifecycle-06)": 4 scenarios, 23 steps passed (before the last review-fix commit); after it,cargo xtask e2e -- -n "comfyui-1[5678]"passes 4 scenarios, 24 steps,comfyui-17fails with "Install ROCm first" when the lookup that fix adds is disabled, andcomfyui-18fails when the error from writing the entry back is discarded; after the last commitcargo test -p rocm --bin rocmwith the filterscomfyui::(101 passed, 4 ignored),runtime_adopt::(15),runtime_registry(13) andengine_runtime_select::(15) all pass;-n "(comfyui-03|serve-29)": 2 scenarios passed. lychee in offline mode ondocs/architecture.md,docs/testing.mdandREADME.md: no errors.cargo clippy -p rocm -p rocm-core -p rocm-engine-vllm --bins --tests -- -D warningsagainst--target x86_64-pc-windows-msvc, with stub C compilers for the build scripts (check and clippy never link): clean.mainis clean (git merge-tree --write-tree origin/main HEADexits 0), and the leak scan againstorigin/mainhas only hits on the Rust word "private" in visibility wording and an unrelated function name.After the last four commits, on Linux (WSL), debug build, targeted:
cargo fmt --all --check,cargo clippy --locked -p rocm --all-targets -- -D warningsandcargo clippy --locked -p e2e-cucumber --test e2e -- -D warningsare clean.cargo test -p rocm --bin rocmwith the filterscomfyui::(102 passed, 4 ignored),runtime_registry(15),runtime_adopt::(17) andengine_runtime_select::(15) all pass. The scenarioscomfyui-1[5-9],comfyui-03,runtime-26(numberedruntime-23when it ran),runtime-lifecycle-06andserve-29, run with-nagainst one debug binary built withrocm/e2e-test-hooks: 9 scenarios, 57 steps passed.cargo test -p e2e-cucumber --test feature_naming(4 passed),cargo xtask manifest --checkandcargo xtask check-crate-edgesare clean, and lychee in offline mode ondocs/architecture.mdanddocs/testing.mdreports no errors.cargo clippy -p rocm -p rocm-core -p rocm-engine-vllm --bins --tests -- -D warningsagainst--target x86_64-pc-windows-msvc(stub C compilers, never linked) is clean; the new state matrix hascfg(unix)andcfg(not(unix))helpers, which is why it was run.git merge-tree --write-tree origin/main HEADexits 0 on the currentmain, and the leak scan of the new commits has only the Rust word "private" in visibility wording.Each new test was shown to fail when its target is broken, by a targeted edit that was then reverted. Letting an unusable setup runtime block the pick again fails the state matrix row
Nothing, 1 ready, setup Unusable,an_unusable_setup_runtime_does_not_hide_the_registrys_ready_runtimes, the new row ofcomfyui_and_engine_paths_agree_on_the_single_ready_runtimeand scenariocomfyui-19; swallowing the unusable-setup error even when nothing else is ready fails the row with no registered runtime andcomfyui_selection_reports_a_setup_runtime_that_is_no_longer_usable; discarding the registry write failure fails the unwritable-registry rows anda_choice_naming_the_setup_runtime_reports_a_registry_that_cannot_take_it_back. Writing the registry entry before the install-root file again failsa_failed_manifest_write_never_leaves_a_registered_runtime_behind, and rewriting the install-root file unconditionally failssetup_recovery_restores_the_entry_and_leaves_a_hand_edited_install_root_file_alone. A function in the storage module that joins theruntimesandregistrysegments and callsfs::writepassed the previous single-writer guard and fails the new one. The state-matrix rows that need a registry holding runtimes that then refuses a write skip themselves where the process can write anyway (an administrator), and the rest of the 96 rows run there.Carried over from the four branches before they were combined, not re-run here: each branch ran
cargo test -p rocm --no-fail-fastand the changed crates' suites, with no test lost by leaf name, and each new test was shown to fail when its target is broken by a targeted edit that was then reverted. For the manifest writer that means dropping the install-root write, writing it unconditionally, flipping the overwrite policy, ignoring--replacein adopt, rewriting the file unconditionally and putting a privatefs::writeof a registry entry back each fail a named test; for the environment-root rule each caller's test and the shared table go red when the rule re-diverges; for the ready-runtime rule changing the readiness test, dropping the recovery call and re-introducing either private copy each fail the unit tests and the corresponding scenario; for the install-root payload rule dropping the dot filter or the existence check, or putting a privateread_dircopy back in either caller, fails the table or the caller's test, and swapping the identity resolver in the os-release reader fails the install-hint test. Not run anywhere: the Windows tests themselves (only the cross-check compiles for that target), the full workspace gate,scripts/smoke_local.py, coverage and the GPU-only scenarios, which CI carries. The install-root manifest and the registry entry are still written with a plainfs::write; moving both onto an atomic writer belongs to the shared atomic-writer work (#610, #811) and is left alone here.User-visible behaviour
Scenarios, all on the default no-GPU lane that runs on every PR unless noted:
runtime-lifecycle-06(@id:runtime-lifecycle-import-rejects-duplicate-unless-replacing) imports a manifest, is refused on the duplicate and succeeds with--replace; it gained an assertion that the registry entry exists and that the imported folder gained no.rocm-cli-runtime.json. This is the end-to-end half of the manifest-writer change, which creates no new file. The SDK install scenarios that would exercise the install-root file need a GPU and a download and run on the gated lanes; adopt and setup recovery need a Python environment to probe and a completed setup, so unit tests assert each path's file set instead.runtime-26(@id:runtime-adopt-outside-a-venv-asks-for-root, Linux) is new: it plants a stand-in interpreter in atoolsfolder, adopts it without--root, and asserts the refusal asks for--rootand thatrocm runtimes liststill shows no runtime.runtime-07(adopting/usr/bin/python3with--root /opt/rocm) is unaffected. The scenario is Linux-only because the stand-in interpreter is a POSIX shell script; other platforms rely on unit tests that also run on the Windows CI job:adopt_request_without_an_inferred_root_asks_for_rootand therocm-coretableenv_root_for_executable_needs_a_bin_or_scripts_folder. The vLLM launch half is internal to a path that needs a real vLLM install and is covered by the unit testvllm_venv_root_needs_a_bin_or_scripts_folder; the "--rootclears the refusal" half is covered byadopt_request_without_an_inferred_root_asks_for_root.comfyui-15(@id:comfyui-install-finds-the-runtime-setup-created) is new: it plants a ready runtime and a setup folder naming it but no registry entry, previewsrocm comfyui install --dry-run, and asserts theinstall command:line names that runtime and that the runtime registry still has no entry for it afterwards.comfyui-16(@id:comfyui-install-restores-the-runtime-setup-created) is new: the real install of the same state, with a ComfyUI checkout already in place that needs nothing beyond the runtime's torch stack so no network or package index is needed, uses the runtime setup created and asserts the registry entry is written back.comfyui-17(@id:comfyui-install-preview-command-works-as-printed) is new: the same state, previews without choosing a runtime, then runs theinstall command:line exactly as it was printed and asserts the install uses the runtime setup created and writes its registry entry back; it fails with "Install ROCm first" if an explicit--runtime-idonly looks in the registry.comfyui-18(@id:comfyui-install-reports-a-registry-it-cannot-write) is new: the same printed command when the registry cannot take the entry back, with a plain file standing where the registry directory should be (a read-only directory would not stoproot, who can run this suite). The preview still succeeds and names the runtime; running the printed command fails namingfailed to restore setup runtimeand the runtime, does not say "Install ROCm first", and leaves the registry as it was. The variant with a read-only registry directory that holds other runtimes is a unit test, because that arrangement does not bite asroot.comfyui-19(@id:comfyui-install-uses-the-ready-runtime-beside-an-unusable-setup-runtime) is new: one ready runtime is registered and the setup folder names a runtime whose interpreter is gone; previewing the install with no runtime chosen uses the registered one, does not report the setup runtime, and leaves the registry holding only the registered runtime. It fails (reportingsetup runtime ... is not usable) when the unusable setup runtime is allowed to block the pick.comfyui-03already covers the several-ready refusal and now reaches it through the shared function, andserve-29keeps the engine path's recovery below the no-GPU refusal; both pass unchanged and were not touched.serve-31is@requires-gpu, unchanged in behaviour, and was not run. The engine side of the unusable-setup-runtime change has no scenario of its own:comfyui_and_engine_paths_agree_on_the_single_ready_runtimehas the row, and the rule itself is unit tested inruntime_adopt.rs. The remaining ComfyUI differences (ambiguity with a recovered runtime, an unusable setup runtime with nothing else ready) are variations of the same recovery and are covered by unit tests and the state matrix.download-progress-01,runtime-lifecycle-01,runtime-lifecycle-02,runtime-lifecycle-07,update-07,update-08,driver-install-06todriver-install-08(a bare-metal lane) anddeps-guard-01. Theexaminescenarios that plant an os-release under a simulated root read a different, root-routed reader and are untouched; only a comment inexamine.featurethat named the old location ofreal_os_releasechanged.Notes for reviewers
Read the history in the order of the sections above; within each section the first commit only adds tests and passes on the unmodified code, which is what pins the behaviour before anything changes, and the next one makes the change. The manifest-writer section has a third commit that makes the skipped rewrite compare decoded manifests rather than bytes, and the ready-runtime section has a commit that pins the unusable-setup-runtime failure and one that keeps a
--dry-runpreview from restoring the registry entry; then a commit that makes an explicit choice report a registry that cannot take the setup runtime back. The last commits answer the approving review: one sets an unusable setup runtime aside while a registered runtime is ready (the engine commands share that rule), one writes the registry entry after the install-root file, one tightens the single-registry-writer guard, and one fixes lint findings in the new state-matrix test. Scenario idscomfyui-15tocomfyui-19andruntime-26are the next free numbers on this branch and will need renumbering if another scenario lands first.docs/architecture.mdis edited in the entries forruntime_registry.rs,runtime_adopt.rs,runtime.rs,hardware_root.rs,therock.rs,therock/update.rs,system_packages.rs,driver_install.rs,engine_runtime_select.rsandcomfyui/runtime_env.rs, which other open work also edits, so expect textual conflicts there. A test inrocm-corethat sets the simulated host root would add to a race that already exists onmain(examine::rocm_install_probe::tests::merge_live_repo_paths_scans_every_family_dir_including_zyppflakes when a test takes the host-root lock inconsistently), so the install-hint test sits in therocmbinary's tests next to the existingreal_os_release_ignores_a_simulated_host_root.