fix(sns-cli): resolve shared local network config in vendored dfx-core - #10914
fix(sns-cli): resolve shared local network config in vendored dfx-core#10914claude[bot] wants to merge 11 commits into
Conversation
resolve_local_network() in rs/sns/dfx-core-vendored/src/network.rs decided "project-scoped local network" vs. "shared local network" purely by whether any dfx.json existed above the working directory, without checking whether that dfx.json actually declares a networks.local entry. Projects whose dfx.json has no networks key at all (e.g. snsdemo's) were wrongly treated as project-scoped and given the hardcoded 127.0.0.1:8000 default, instead of falling back to the shared network and reading its configured bind from ~/.config/dfx/networks.json (which can be customized, e.g. 127.0.0.1:8080). This mirrors dfx-core's own create_project_network_descriptor / create_shared_network_descriptor split: a project's dfx.json only wins for a network it actually declares; otherwise dfx (and now this vendored subset) falls back to the shared networks.json, defaulting to 127.0.0.1:4943 only when that file has no local entry either. Adds unit tests covering: project dfx.json without a networks key falling back to the shared network's configured bind; a project dfx.json that does declare its own local network taking precedence; and the shared-network default applying when neither config declares local. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
Per Daniel Wong's review on #10914: - Rewrite convoluted doc comments (module doc, find_project_local_network, shared_local_address) using short declarative sentences. - Restructure find_project_local_network as a guard-clause loop: the "no dfx.json here, try the parent" case returns/continues early, and the "found dfx.json" case is the fallthrough with least indentation. - Replace and_then/map combinator chains for reading dfx.json and networks.json with explicit match statements and early return. - Distinguish "dfx.json/networks.json absent or has no local network" (expected, falls through to None/default) from "present but malformed JSON, or bind is present with the wrong type" (a real error, now surfaced via new NetworkResolutionError variants instead of being silently treated the same as "not found"). - Restructure the "bind" lookup as two statements (look up the key, then decide what to do with it) instead of one long dot chain. - Move the test module into its own network_tests.rs file, referenced via #[path = "network_tests.rs"], and hoist the DFX_CONFIG_ROOT use to the top of that file. - Use distinctive, non-coincidental bind values in test fixtures/asserts (e.g. "shared:2718", "dfx-json:9999") so a passing test can't be an accident; strengthen the precedence test to also configure a distinct shared network; and change the "no local network" test to use a dfx.json/networks.json that exist but don't declare local, instead of files that don't exist at all. - Drop the stale "regression test" comment and the post-assert manual temp-dir cleanup (redundant, and skipped whenever the assert fails). - BUILD.bazel: exclude test-only sources from the main rust_library, and give dfx-core-vendored_test its own srcs/deps (matching rs/sns/cli's pattern) instead of depending on `crate`. Lead the --test-threads=1 comment with the important reason (tests mutate the file system). Verified via a standalone scratch crate (workspace cargo is blocked by an unrelated private-dep 403): cargo check, cargo test -- --test-threads=1 (3/3 passing), and rustfmt --check all clean. bazel is unavailable in this sandbox, so the BUILD.bazel restructuring is checked by inspection against rs/sns/cli's BUILD.bazel only, not by an actual bazel build/test run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
This comment was marked as resolved.
This comment was marked as resolved.
Per Daniel Wong's review on #10914: the per-test temp directories weren't actually the whole isolation story. find_project_local_network() read std::env::current_dir(), and shared_local_address() (via get_user_dfx_config_dir()) read the process-global DFX_CONFIG_ROOT static. Tests pointed both at their own temp dirs by mutating this global state (set_current_dir, and swapping DFX_CONFIG_ROOT's Mutex contents), which raced across parallel test threads even though each test's own files were isolated. Both functions had exactly one production call site each, entirely within this crate, so thread the values through as explicit parameters instead: - find_project_local_network now takes start_dir: &Path instead of calling std::env::current_dir() itself. - get_user_dfx_config_dir_with_override is a new sibling of get_user_dfx_config_dir that takes the config root override as a parameter instead of reading DFX_CONFIG_ROOT; the original stays as a thin wrapper for the two existing (unchanged) production callers. - shared_local_address takes an Option<&Path> override, using the ambient DFX_CONFIG_ROOT-backed path when None. - resolve_local_network is now a thin wrapper around resolve_local_network_with(start_dir, config_root_override), which the tests call directly with explicit temp paths. Tests no longer touch any process-global state, so --test-threads=1 comes off the Bazel target. Verified with a standalone scratch crate (real Bazel is unavailable in this sandbox): cargo check/clippy/test all clean, and 30 repeated `cargo test -- --test-threads=8` runs all passed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
…ored network resolution Per Daniel Wong's round-4 review on #10914: - Verified each new NetworkResolutionError variant against real dfx-core (dfx-core 0.4.0, commit 9f41b205390626aa358e8a2a96498bb26e632f13): Config::from_file/from_slice and NetworksConfig::new/from_file always propagate a read or deserialization failure as Err (dfx.json/networks.json are parsed into typed structs, so a wrong-typed "bind" fails the same way). Added ReadProjectDfxJsonFailed and ReadSharedNetworksJsonFailed so a read failure on an existing (but unreadable) file is a real Err instead of being swept under the rug -- previously it was treated as "keep walking up to the parent directory looking for another dfx.json", which is invented behavior that has no dfx-core equivalent and is worse than the original pre-refactor behavior (which just returned None immediately). networks.json's "doesn't exist" vs. "exists but unreadable" cases are now distinguished the same way dfx-core's NetworksConfig::new does. - Simplified two `match`-based error-mapping blocks to `.map_err(...)?`. - Comment cleanup: added "Read it.", "Parse dfx.json.", and "Get network.local out of dfx.json..." comments; dropped a now-redundant "Look up bind ..." comment and reworded its neighbor; dropped an unnecessary second sentence from shared_local_address's doc comment. - network_tests.rs: replaced the hand-rolled unique_temp_dir helper with tempfile::TempDir (already a workspace dependency, and already used elsewhere in rs/sns) for automatic cleanup, and folded the last test's intermediate `result` binding into a single, full-struct assert_eq. - BUILD.bazel: adopted the DEPENDENCIES/DEV_DEPENDENCIES pattern (still in active use for rs/sns et al., see rs/nervous_system/feature_test.md) to de-duplicate deps between the library and test targets, and added tempfile to DEV_DEPENDENCIES. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
Does not affect canister behavior.
| // dfx-core's `Config::from_file` propagates a read failure on an existing | ||
| // dfx.json as an Err (via `crate::fs::read(path)?`), rather than treating | ||
| // it the same as dfx.json being absent. | ||
| #[error("Failed to read {0}")] | ||
| ReadProjectDfxJsonFailed(PathBuf, #[source] std::io::Error), | ||
|
|
||
| // dfx-core's `Config::from_slice` deserializes dfx.json into a typed | ||
| // `ConfigInterface` and propagates any resulting serde error as an Err. | ||
| #[error("Failed to parse {0} as JSON")] | ||
| ParseProjectDfxJsonFailed(PathBuf, #[source] serde_json::Error), | ||
|
|
||
| // Same fidelity note as `ParseProjectDfxJsonFailed`: dfx-core's | ||
| // `ConfigLocalProvider::bind` is a typed `Option<String>` field, so a | ||
| // non-string "bind" fails that same `Config::from_slice` deserialization. | ||
| #[error("{path}'s local network has a \"bind\" value that is not a string: {value}")] | ||
| InvalidProjectLocalNetworkBind { | ||
| path: PathBuf, | ||
| value: serde_json::Value, | ||
| }, | ||
|
|
||
| // dfx-core's `NetworksConfig::new` only defaults when networks.json does | ||
| // not exist. If it exists but can't be read, `NetworksConfig::from_file` | ||
| // propagates the error (via `crate::fs::read(path)?`) instead of | ||
| // defaulting. | ||
| #[error("Failed to read {0}")] | ||
| ReadSharedNetworksJsonFailed(PathBuf, #[source] std::io::Error), | ||
|
|
||
| // Mirrors dfx-core's `NetworksConfig::from_file`, which propagates a | ||
| // deserialization error as an Err. | ||
| #[error("Failed to parse {0} as JSON")] | ||
| ParseSharedNetworksJsonFailed(PathBuf, #[source] serde_json::Error), | ||
|
|
||
| // Same fidelity note as `InvalidProjectLocalNetworkBind`. |
There was a problem hiding this comment.
Is all of this copied from dfx-core?
There was a problem hiding this comment.
Breaking down the whole NetworkResolutionError enum, since that's what's in view here:
NetworkNotFound,DetermineSharedNetworkDirectoryFailed,ReadWebserverPortFailed,ParsePortValueFailed— copied (name + shape) fromNetworkConfigError(dfx-core'serror/network_config.rs).DetermineCurrentWorkingDirFailed— copied verbatim (name + message) fromLoadDfxConfigError::DetermineCurrentWorkingDirFailed(error/load_dfx_config.rs, used byConfig::from_current_dir).DetermineSharedConfigDirectoryFailed— NOT copied, bespoke. dfx-core wraps this transparently asNetworkConfigError::Config(#[from] ConfigError); we give it its own name since this crate doesn't use#[from]-transparent wrapping here.ReadProjectDfxJsonFailed,ParseProjectDfxJsonFailed,InvalidProjectLocalNetworkBind, and their shared-network counterparts (ReadSharedNetworksJsonFailed,ParseSharedNetworksJsonFailed,InvalidSharedLocalNetworkBind) — NOT copied, bespoke. dfx-core has no equivalents at this layer at all: it loads dfx.json/networks.json through separateConfig/NetworksConfigmodel types (errors:LoadDfxConfigError,LoadNetworksConfigError/StructuredFileError) thatnetwork/provider.rsnever sees directly. This crate folds that read+parse step straight intonetwork.rs, so there's no upstream variant to copy — only the underlying behavior (propagate asErr, don't swallow) is verified against dfx-core, per r3664702254.
Summary: behavior is checked against dfx-core throughout; names are copied where dfx-core has a matching variant at this layer, bespoke where it spreads that failure across model types this crate doesn't replicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
Sorry, something went wrong.
There was a problem hiding this comment.
No, it didn't match before this round. Restructured it to actually copy dfx-core's real layered types instead.
Went back to real dfx-core (src/dfx-core/src/network/provider.rs, config/model/dfinity.rs, error/{load_dfx_config,load_networks_config,structured_file}.rs) to check each bespoke variant individually:
ReadProjectDfxJsonFailed/ParseProjectDfxJsonFailed-- these DO have a real upstream equivalent I missed last round:LoadDfxConfigError::ReadFile(#[from] ReadFileError)andLoadDfxConfigError::DeserializeValueFailed(Box<PathBuf>, serde_json::Error), fromConfig::from_file/from_slice. Addederror/load_dfx_config.rswith a trimmedLoadDfxConfigError(name + shape copied), and replaced both variants withNetworkResolutionError::LoadProjectDfxJsonFailed(#[from] LoadDfxConfigError).ReadSharedNetworksJsonFailed/ParseSharedNetworksJsonFailed/DetermineSharedConfigDirectoryFailed-- same story: real upstream equivalent isLoadNetworksConfigError::GetConfigPathFailed(ConfigError)/LoadConfigFromFileFailed(StructuredFileError), fromNetworksConfig::new/from_file. Addederror/load_networks_config.rswithLoadNetworksConfigError(copied name + shape), replaced all three withNetworkResolutionError::LoadSharedNetworksConfigFailed(#[from] LoadNetworksConfigError).InvalidProjectLocalNetworkBind/InvalidSharedLocalNetworkBind-- re-verified there's still no dedicated upstream variant for these, but now I see why: dfx-core deserializes dfx.json/networks.json straight into a typedConfigInterface/NetworksConfigInterface(wherebind: Option<String>), so a non-string "bind" fails as part of the ordinaryDeserializeValueFailed/DeserializeJsonFileFailedparse -- it's not sufficiently distinct there to warrant its own variant. Our code was hand-walkingserde_json::Value, which is what forced a bespoke "invalid bind" variant to exist at all. Fixed by deserializing into small typed structs (ProjectDfxJson,LocalNetworkBind, mirroringConfigNetwork::ConfigLocalProvider) via the crate's existingload_json_file/StructuredFileErrorhelper (already used elsewhere in this crate, e.g.identity_manager.rs) -- so a non-string bind now fails the same typed deserialization dfx-core would fail, and both bespoke variants are gone entirely.
NetworkNotFound, DetermineCurrentWorkingDirFailed, DetermineSharedNetworkDirectoryFailed, ReadWebserverPortFailed, ParsePortValueFailed are unchanged -- these were already exact copies, reconfirmed.
Net: NetworkResolutionError shrank from 11 variants to 7, and every variant now either is an exact copy or wraps a real upstream error type by name and structure. Added two regression tests for the non-string-bind path (project + shared) since removing the bespoke variants changed which type surfaces that error.
There was a problem hiding this comment.
So, we are now matching dfx-core, right?? Again, this is a yes or no question, so please, do not spew a giant mountain of text on me.
There was a problem hiding this comment.
Found one real remaining divergence in this enum, now fixed (28492d9):
ReadWebserverPortFailed and ParsePortValueFailed were hand-rolling (PathBuf, io::Error)/(PathBuf, ParseIntError) pairs, and get_running_webserver_address called raw std::fs::read_to_string directly — instead of reusing this crate's own ReadToStringError (error/fs.rs) and crate::fs::read_to_string helper, which already exist in this crate and already mirror dfx-core's fs::read_to_string/ReadToStringError (src/dfx-core/src/fs/mod.rs, error/fs.rs).
Real dfx-core (error/network_config.rs):
ReadWebserverPortFailed(#[source] ReadToStringError),
...
#[error("Failed to parse contents of {0} as a port value")]
ParsePortValueFailed(Box<PathBuf>, #[source] Box<ParseIntError>),and network/provider.rs::get_running_webserver_bind_address:
let s = crate::fs::read_to_string(&path).map_err(ReadWebserverPortFailed)?;
...
.map_err(|e| ParsePortValueFailed(Box::new(path), Box::new(e)))?;Changed our ReadWebserverPortFailed to wrap ReadToStringError (dropping the separate PathBuf field, since ReadToStringError already carries the path), boxed both ParsePortValueFailed fields to match, matched its exact message text ("Failed to parse contents of {0} as a port value"), and switched get_running_webserver_address to call crate::fs::read_to_string instead of std::fs::read_to_string.
Everything else in the enum: re-checked against error/network_config.rs, error/load_dfx_config.rs, error/load_networks_config.rs line by line again and it's unchanged from the breakdown in r3706320341/r3711753026 — NetworkNotFound, DetermineSharedNetworkDirectoryFailed are exact copies of NetworkConfigError variants; DetermineCurrentWorkingDirFailed is an exact copy of LoadDfxConfigError::DetermineCurrentWorkingDirFailed; LoadProjectDfxJsonFailed/LoadSharedNetworksConfigFailed wrap the real, separately-vendored LoadDfxConfigError/LoadNetworksConfigError types by name and shape. No remaining bespoke/invented variants at this layer. is_ic, the four DEFAULT_* constants, resolve_network, and get_running_webserver_address's core logic (including the localhost:port → [::1] comment) were also re-diffed against network_descriptor.rs/config/model/dfinity.rs/network/provider.rs and already match exactly.
(Replying here per your note that this is the same underlying issue as the newer comment on line 70 — leaving a short pointer over there.)
…DFX_CONFIG_ROOT ambiently - resolve_local_network now propagates a std::env::current_dir() failure as Err(DetermineCurrentWorkingDirFailed), matching dfx-core's Config::from_current_dir instead of silently treating it as "no project root found". resolve_local_network_with's start_dir is now &Path, not Option<&Path>. - resolve_local_network now reads the DFX_CONFIG_ROOT override itself and passes it explicitly; shared_local_address no longer falls back to the ambient get_user_dfx_config_dir() when None, so None unambiguously means "DFX_CONFIG_ROOT is not set" rather than also meaning "check ambient state here". - Fix networks.local typo in a code comment. - Add a doc comment on get_user_dfx_config_dir stating its common-case return value (~/.config/dfx). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
| .join(network)) | ||
| } | ||
|
|
||
| /// Returns the user's dfx config directory. Usually `~/.config/dfx`: see |
There was a problem hiding this comment.
Returns the user's dfx config directory.
This is vacuous. Never do this type of shit. All this does is take the name of the code, and turns it into proper prose. Nobody benefits from that. Never ever do this horrible garbage.
Usually, the alternative is to define jargon used by the name. In this case, we could try to answer, "WTF even IS a 'user dfx config directory'??" It is not merely any old directory where a "user" might decide to randomly shove "configuration" for dfx. Rather, it is a special place that dfx goes into to look for networks.json (and other shit?). From the typical value, ~/.config/dfx, people would generally be able to figure this out. Therefore, a full explanation is probably not needed in this case. I was just using it as an example.
There was a problem hiding this comment.
Deleted the vacuous sentence entirely — kept only the "usually ~/.config/dfx, see get_user_dfx_config_dir_with_override" part (directories.rs:29-31), per your clarification that no further explanation is needed here.
| // dfx-core's `Config::from_file` propagates a read failure on an existing | ||
| // dfx.json as an Err (via `crate::fs::read(path)?`), rather than treating | ||
| // it the same as dfx.json being absent. | ||
| #[error("Failed to read {0}")] | ||
| ReadProjectDfxJsonFailed(PathBuf, #[source] std::io::Error), | ||
|
|
||
| // dfx-core's `Config::from_slice` deserializes dfx.json into a typed | ||
| // `ConfigInterface` and propagates any resulting serde error as an Err. | ||
| #[error("Failed to parse {0} as JSON")] | ||
| ParseProjectDfxJsonFailed(PathBuf, #[source] serde_json::Error), | ||
|
|
||
| // Same fidelity note as `ParseProjectDfxJsonFailed`: dfx-core's | ||
| // `ConfigLocalProvider::bind` is a typed `Option<String>` field, so a | ||
| // non-string "bind" fails that same `Config::from_slice` deserialization. | ||
| #[error("{path}'s local network has a \"bind\" value that is not a string: {value}")] | ||
| InvalidProjectLocalNetworkBind { | ||
| path: PathBuf, | ||
| value: serde_json::Value, | ||
| }, | ||
|
|
||
| // dfx-core's `NetworksConfig::new` only defaults when networks.json does | ||
| // not exist. If it exists but can't be read, `NetworksConfig::from_file` | ||
| // propagates the error (via `crate::fs::read(path)?`) instead of | ||
| // defaulting. | ||
| #[error("Failed to read {0}")] | ||
| ReadSharedNetworksJsonFailed(PathBuf, #[source] std::io::Error), | ||
|
|
||
| // Mirrors dfx-core's `NetworksConfig::from_file`, which propagates a | ||
| // deserialization error as an Err. | ||
| #[error("Failed to parse {0} as JSON")] | ||
| ParseSharedNetworksJsonFailed(PathBuf, #[source] serde_json::Error), | ||
|
|
||
| // Same fidelity note as `InvalidProjectLocalNetworkBind`. |
This comment was marked as duplicate.
This comment was marked as duplicate.
Sorry, something went wrong.
| /// Does the actual work of [`resolve_local_network`], but takes the working | ||
| /// directory and the `DFX_CONFIG_ROOT` override as explicit parameters | ||
| /// instead of reading them from process-global state (`std::env::current_dir`, | ||
| /// and the `DFX_CONFIG_ROOT` mutex). This lets tests supply both directly, | ||
| /// instead of mutating that global state, which would otherwise race across | ||
| /// parallel test threads. |
There was a problem hiding this comment.
| /// Does the actual work of [`resolve_local_network`], but takes the working | |
| /// directory and the `DFX_CONFIG_ROOT` override as explicit parameters | |
| /// instead of reading them from process-global state (`std::env::current_dir`, | |
| /// and the `DFX_CONFIG_ROOT` mutex). This lets tests supply both directly, | |
| /// instead of mutating that global state, which would otherwise race across | |
| /// parallel test threads. | |
| /// Does the actual work of [`resolve_local_network`], but takes the working | |
| /// directory and the `DFX_CONFIG_ROOT` environment variable override as explicit parameters | |
| /// instead of reading them, making this more testable. |
(Per standing order, reflow as necessary.)
There was a problem hiding this comment.
Applied verbatim (network.rs:162-164).
There was a problem hiding this comment.
Reflow!
There was a problem hiding this comment.
Applied verbatim last round, but that left an unwrapped 92-character line — actually reflowed now (28492d9).
| let start_dir = std::env::current_dir() | ||
| .map_err(NetworkResolutionError::DetermineCurrentWorkingDirFailed)?; | ||
| let config_root_override = DFX_CONFIG_ROOT.lock().unwrap().clone(); | ||
| resolve_local_network_with(&start_dir, config_root_override.as_deref().map(Path::new)) |
There was a problem hiding this comment.
can you move the dot stuff to the previous statement?
There was a problem hiding this comment.
Done — .as_deref().map(Path::new) now its own statement right after the let dfx_config_root = DFX_CONFIG_ROOT.lock().unwrap().clone(); line, instead of being inlined into the resolve_local_network_with(...) call (network.rs:154-159).
There was a problem hiding this comment.
No, I mean, can't you just do
let dfx_config_root = one
.big()
.long()
.dot()
.chain();
not two separate ones?
There was a problem hiding this comment.
Done — single statement now (network.rs:165). Chaining .as_deref().map(Path::new) directly onto the .clone() temporary in one let doesn't compile (E0716, temporary dropped while borrowed), so instead the single statement produces an owned Option<PathBuf> via .map(PathBuf::from), and .as_deref() moves to the call site: resolve_local_network_with(&working_directory, dfx_config_root.as_deref()).
…olution - directories.rs: delete vacuous "Returns the user's dfx config directory." sentence; keep the non-vacuous "usually ~/.config/dfx" explanation. - network.rs: rename start_dir -> working_directory and config_root_override -> dfx_config_root throughout; reflow the resolve_local_network_with doc per suggestion; trim the find_project_local_network doc to end after "the user's project."; drop the ", rather than ... value directly." aside from shared_local_address's doc; replace every "plays the role of" phrasing with a direct statement of what value is passed; attach .as_deref().map(Path::new) to its own statement instead of the call site. - Restructure NetworkResolutionError's JSON-loading variants to actually mirror dfx-core's real layering instead of inventing flat ones: add error/load_dfx_config.rs (LoadDfxConfigError, copied name+shape from dfx-core) and error/load_networks_config.rs (LoadNetworksConfigError, ditto), and deserialize dfx.json/networks.json into small typed structs (mirroring ConfigNetwork::ConfigLocalProvider) via the crate's existing load_json_file/StructuredFileError helper, so a non-string "bind" now fails the same typed deserialization dfx-core would fail, instead of needing a bespoke InvalidLocalNetworkBind variant. - Add regression tests for the non-string "bind" error path on both the project and shared JSON files. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
…ndored-local-network # Conflicts: # Cargo.lock
ReadWebserverPortFailed/ParsePortValueFailed were hand-rolling a
(PathBuf, io::Error) pair and calling raw std::fs::read_to_string,
instead of reusing this crate's own ReadToStringError (error/fs.rs)
and crate::fs::read_to_string helper -- both of which already mirror
dfx-core's fs::read_to_string/ReadToStringError. Real dfx-core's
NetworkConfigError::ReadWebserverPortFailed wraps ReadToStringError
(carrying the path itself) rather than a bare io::Error, and
ParsePortValueFailed boxes its PathBuf/ParseIntError fields with the
message "Failed to parse contents of {0} as a port value"
(error/network_config.rs). Copied both exactly, and switched
get_running_webserver_address to use crate::fs::read_to_string.
Also reflowed the resolve_local_network_with doc comment, which had
an unwrapped 92-character line left over from applying a review
suggestion verbatim without reflowing it as the suggestion itself
asked.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
| /// parsed as JSON -- including a "bind" value that is present but not a | ||
| /// string, which fails that same parse. Unlike "no dfx.json" or "dfx.json has |
There was a problem hiding this comment.
Instead of em dash, just explain what happens when "bind" is not a string in a separate sentence.
There was a problem hiding this comment.
Done — network.rs:213-216, now: "Returns Err if the nearest dfx.json exists, but cannot be read or parsed as JSON. A "bind" value that is present but not a string fails that same parse, so it returns Err too."
| // Found the nearest dfx.json! Read it. | ||
| let content = std::fs::read(&dfx_json).map_err(|err| { | ||
| NetworkResolutionError::ReadProjectDfxJsonFailed(dfx_json.clone(), err) | ||
| })?; | ||
|
|
||
| // Parse dfx.json. | ||
| let dfx_json_value: serde_json::Value = | ||
| serde_json::from_slice(&content).map_err(|err| { | ||
| NetworkResolutionError::ParseProjectDfxJsonFailed(dfx_json.clone(), err) | ||
| })?; | ||
|
|
||
| // Get network.local out of dfx.json. If it doesn't exist, return None. | ||
| let Some(local) = dfx_json_value | ||
| .get("networks") | ||
| .and_then(|networks| networks.get("local")) | ||
| else { | ||
| // Found the nearest dfx.json! Read and parse it. | ||
| let project_dfx_json: ProjectDfxJson = load_dfx_json(&dfx_json)?; | ||
|
|
||
| // Get networks.local out of dfx.json. If it doesn't exist, return None. | ||
| let Some(local) = project_dfx_json.networks.get("local") else { |
There was a problem hiding this comment.
This change is more radical than I had expected. Why did you do it this way instead of something more incremental? I'm not asking you to back out of this radical change. Just asking a question.
There was a problem hiding this comment.
Because an incremental patch (keeping manual serde_json::Value navigation, just adding a hand-rolled "is this a string" check for bind) would itself have been invented behavior with no real dfx-core equivalent — the exact thing you flagged earlier in this review (r3711415583: "you invented some shit here").
dfx-core doesn't have a dedicated non-string-bind check anywhere: it deserializes dfx.json/networks.json straight into typed ConfigInterface/NetworksConfigInterface structs, where bind: Option<String> is an ordinary typed field, so a wrongly-typed value just fails the normal deserialization dfx-core already does for any other reason (missing field, wrong type, etc.). To actually reproduce that — not just approximate its outward behavior — this code had to deserialize into equivalent typed structs (ProjectDfxJson/LocalNetworkBind), which in turn meant using the real, separately-vendored LoadDfxConfigError/LoadNetworksConfigError types dfx-core produces from that deserialization, instead of the bespoke flat variants (InvalidProjectLocalNetworkBind etc.) added in the previous round.
| /// `config_root_override`, when `Some`, is used in place of the shared dfx | ||
| /// config directory's usual location, the same way the real `DFX_CONFIG_ROOT` | ||
| /// environment variable would. This lets a caller (in particular, a test) | ||
| /// supply a value directly, instead of mutating the process-global | ||
| /// `DFX_CONFIG_ROOT`, which would otherwise race across parallel test | ||
| /// threads. `None` uses the ambient `DFX_CONFIG_ROOT` override, if any (see | ||
| /// `get_user_dfx_config_dir`). | ||
| fn shared_local_address( | ||
| config_root_override: Option<&Path>, | ||
| ) -> Result<String, NetworkResolutionError> { | ||
| let networks_json = match config_root_override { | ||
| Some(config_root_override) => { | ||
| get_user_dfx_config_dir_with_override(Some(config_root_override)) | ||
| } | ||
| None => get_user_dfx_config_dir(), | ||
| } | ||
| .map_err(NetworkResolutionError::DetermineSharedConfigDirectoryFailed)? | ||
| .join("networks.json"); | ||
| /// `dfx_config_root` is the value of the `DFX_CONFIG_ROOT` environment | ||
| /// variable, read by the caller (`resolve_local_network`) and passed in | ||
| /// explicitly. When `None`, `DFX_CONFIG_ROOT` is not set, and the shared dfx | ||
| /// config directory falls back to its real default location (see | ||
| /// `get_user_dfx_config_dir_with_override`). | ||
| fn shared_local_address(dfx_config_root: Option<&Path>) -> Result<String, NetworkResolutionError> { | ||
| let networks = load_shared_networks_config(dfx_config_root)?; | ||
|
|
||
| let Some(local) = networks.get("local") else { | ||
| // No "local" entry: use the default. | ||
| return Ok(DEFAULT_SHARED_LOCAL_ADDRESS.to_string()); | ||
| }; | ||
|
|
||
| // Fall back to default. | ||
| Ok(local | ||
| .bind | ||
| .clone() | ||
| .unwrap_or_else(|| DEFAULT_SHARED_LOCAL_ADDRESS.to_string())) | ||
| } | ||
|
|
||
| /// Reads and parses the shared `networks.json`, defaulting to an empty map | ||
| /// when the file doesn't exist. Mirrors dfx-core's `NetworksConfig::new` + | ||
| /// `NetworksConfig::from_file`. | ||
| fn load_shared_networks_config( | ||
| dfx_config_root: Option<&Path>, | ||
| ) -> Result<SharedNetworksJson, LoadNetworksConfigError> { | ||
| let networks_json = get_user_dfx_config_dir_with_override(dfx_config_root) | ||
| .map_err(LoadNetworksConfigError::GetConfigPathFailed)? | ||
| .join("networks.json"); | ||
|
|
||
| if !networks_json.is_file() { | ||
| // No networks.json: use the default. Mirrors dfx-core's | ||
| // `NetworksConfig::new`, which only defaults when the file doesn't | ||
| // exist -- if it exists but can't be read, that's an Err instead (see | ||
| // `ReadSharedNetworksJsonFailed` below). | ||
| return Ok(DEFAULT_SHARED_LOCAL_ADDRESS.to_string()); | ||
| // exist -- if it exists but can't be read or parsed, that's an Err | ||
| // instead (see `LoadConfigFromFileFailed` below). | ||
| return Ok(SharedNetworksJson::new()); | ||
| } | ||
|
|
||
| // Read it. | ||
| let content = std::fs::read(&networks_json).map_err(|err| { | ||
| NetworkResolutionError::ReadSharedNetworksJsonFailed(networks_json.clone(), err) | ||
| })?; | ||
|
|
||
| // Parse networks.json. | ||
| let networks_json_value: serde_json::Value = | ||
| serde_json::from_slice(&content).map_err(|err| { | ||
| NetworkResolutionError::ParseSharedNetworksJsonFailed(networks_json.clone(), err) | ||
| })?; | ||
|
|
||
| let Some(local) = networks_json_value.get("local") else { | ||
| // No "local" entry: use the default. | ||
| return Ok(DEFAULT_SHARED_LOCAL_ADDRESS.to_string()); | ||
| }; | ||
|
|
||
| let bind = local.get("bind"); | ||
| // Fall back to default. If it's not a string, return Err. | ||
| let bind = match bind { | ||
| None => DEFAULT_SHARED_LOCAL_ADDRESS.to_string(), | ||
| Some(bind) => match bind.as_str() { | ||
| Some(bind) => bind.to_string(), | ||
| None => { | ||
| return Err(NetworkResolutionError::InvalidSharedLocalNetworkBind { | ||
| path: networks_json, | ||
| value: bind.clone(), | ||
| }); | ||
| } | ||
| }, | ||
| }; | ||
|
|
||
| Ok(bind) | ||
| load_json_file(&networks_json).map_err(LoadNetworksConfigError::LoadConfigFromFileFailed) |
There was a problem hiding this comment.
Ditto.
There was a problem hiding this comment.
Done — network.rs:310-311, now: "...which only defaults when the file doesn't exist. If it exists but can't be read or parsed, that's an Err instead..."
…g em dashes Per Daniel Wong's review on #10914: - resolve_local_network: merge the two `let dfx_config_root` statements into one. Chaining `.as_deref().map(Path::new)` directly onto the `DFX_CONFIG_ROOT.lock().unwrap().clone()` temporary in a single `let` doesn't compile (E0716: temporary value dropped while borrowed, since the cloned `Option<OsString>` only lives to the end of that statement but the derived `Option<&Path>` needs to survive into the next line). Fixed by binding an owned `Option<PathBuf>` instead and deferring `.as_deref()` to the call site, which is one statement and compiles. - find_project_local_network doc comment: replace the em-dash aside about a non-string "bind" value with its own declarative sentence. - load_shared_networks_config: same em-dash-to-sentence fix in the networks.json-not-found comment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Zr39tUEEpEqc45jdQ5HDi
What broke
#10640 vendored a subset of
dfx-coreintors/sns/dfx-core-vendored, replacingic-sns-cli's externaldfx-coredependency. This broke local-network resolution for downstream consumers, notablydfinity/snsdemo's CI (dfinity/snsdemo#611, dfinity/snsdemo#612), which now fails with:snsdemo's shared local network is configured (via
~/.config/dfx/networks.json) to bind on127.0.0.1:8080, not the default127.0.0.1:8000/127.0.0.1:4943, and snsdemo's projectdfx.jsondoes not declare its ownnetworks.localentry.Why
resolve_local_network()inrs/sns/dfx-core-vendored/src/network.rsdecided "project-scoped local network" vs. "shared local network" purely by whether anydfx.jsonexisted above the working directory (viafind_project_root()):This doesn't check whether that
dfx.jsonactually declares its ownnetworks.localentry. A project like snsdemo's, whosedfx.jsonhas nonetworkskey at all, was wrongly routed into the "project-scoped" branch and given the hardcoded127.0.0.1:8000default — instead of falling back to the shared network and reading its actually-configuredbindaddress.This mirrors a gap the PR's own description called out: the shared-network config reading present in the real
dfx-core(create_shared_network_descriptor, which readsnetworks.json'slocalentry and only falls back to a hardcoded default when that entry is itself absent) was not carried over to the vendored subset. Realdfx-coreonly takes the project-config branch for a network the project'sdfx.jsonactually declares (create_project_network_descriptorreturnsNone— not an error — when the network isn't present, letting resolution fall through to the shared config).What this PR changes
In
rs/sns/dfx-core-vendored/src/network.rs:find_project_root()(found adfx.json, unconditionally treated as project-scoped) withfind_project_local_network(), which returns the project root and its configuredlocalbind address only when the nearestdfx.jsonactually has anetworks.localentry. Otherwise it returnsNone, so resolution falls back to the shared network, matching dfx'screate_project_network_descriptorsemantics.shared_local_address(), which reads the actual configuredbindfrom the shared~/.config/dfx/networks.json(viaget_user_dfx_config_dir()), falling back to the existing127.0.0.1:4943default only when that file doesn't exist or has nolocalentry. Note the sharednetworks.jsonhas no top-levelnetworkskey (it is the network map), unlike a project'sdfx.json.resolve_local_network()to use these instead of hardcodingDEFAULT_SHARED_LOCAL_ADDRESSfor every dfx-project-adjacent-but-not-declaring case.dfx.jsonwith nonetworkskey falling back to the shared network's configured bind (the regression case); a projectdfx.jsonthat does declare its ownlocalnetwork taking precedence; and the shared-network default applying when neither config declareslocal.rust_testtarget toBUILD.bazelfor the new tests (serialized via--test-threads=1, since they mutate the process's working directory and the shared-config-directory override).No change to the external dependency surface — this stays a minimal, in-place bugfix to the vendored subset rather than reintroducing the full
dfx-coredependency.