Skip to content

[V2:04] Add Workspace Configuration Discovery - #18

Open
ritorhymes wants to merge 1 commit into
eips-wg:masterfrom
ritovision:v2/04-workspace-config-discovery
Open

[V2:04] Add Workspace Configuration Discovery#18
ritorhymes wants to merge 1 commit into
eips-wg:masterfrom
ritovision:v2/04-workspace-config-discovery

Conversation

@ritorhymes

@ritorhymes ritorhymes commented May 5, 2026

Copy link
Copy Markdown
Contributor

Part of V2: Multi-repo local build system.

Summary

This PR adds the local workspace configuration layer that later init, doctor, runtime, and serve/preview PRs consume. It defines strict .build-eips.toml parsing, upward discovery, server/site defaults, and workspace path helpers; the [render].only surface is intentionally deferred to V2:15. LoadedWorkspaceConfig::discover is not called from main.rs yet, so review should focus on schema stability, discovery behavior, and derived path/default semantics rather than command integration.

  • Add .build-eips.toml workspace config parsing and upward discovery.
  • Add starter/default workspace config text.
  • Add server binding and site base URL settings.
  • Add workspace path helpers for build roots, the local theme, and local repos.
  • Add strict schema tests for defaults, invalid values, unsupported fields, and discovery behavior.

Review Notes

This is the workspace configuration foundation for the preprocessor stack. It defines how local workspaces are discovered and how workspace-local server/site/path defaults are represented, but it does not yet add user-facing workspace commands.

LoadedWorkspaceConfig::discover is defined here but not yet called from main.rs; init, doctor, and the runtime PRs wire it in.

Workspace init and doctor consume this in V2:06-V2:07. Execution policy consumes the loaded config in V2:08, and server binding is used by serve/preview in V2:11-V2:12.

[render].only is intentionally excluded here. Targeted rendering adds the render config surface in V2:15, when the build command can actually consume it.

The strict parse tests verify that unsupported workspace-config fields fail clearly instead of being silently ignored.

Review config.rs as a schema/discovery change: the important parts are strict parsing, stable starter output, upward discovery from active repo paths, and path helpers derived from the workspace root.

Verification

  • Review WorkspaceConfig, LoadedWorkspaceConfig, ServerSettings, ServerBinding, and SiteSettings in src/config.rs.
  • Review default_workspace_config_text() and the roundtrip/default tests for stable starter config output.
  • Review discover_path() and LoadedWorkspaceConfig::discover() for upward config discovery.
  • Review strict parse tests for unsupported fields and invalid site base URLs.

@ritorhymes
ritorhymes force-pushed the v2/04-workspace-config-discovery branch from 208ad72 to 16168f9 Compare May 5, 2026 07:53
@ritorhymes ritorhymes changed the title Add Workspace Configuration Discovery [V2:04] Add Workspace Configuration Discovery May 5, 2026
@ritorhymes
ritorhymes force-pushed the v2/04-workspace-config-discovery branch 2 times, most recently from 8d3c598 to e60a9b0 Compare May 9, 2026 20:10
@ritorhymes
ritorhymes force-pushed the v2/04-workspace-config-discovery branch from e60a9b0 to 8c9b119 Compare May 26, 2026 01:02

@SamWilsn SamWilsn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hm, so is the intended directory structure something like:

  • ~/Workspace/.build-eips.toml
  • ~/Workspace/.local-build/...
  • ~/Workspace/ERCs/...
  • ~/Workspace/EIPs/...

?

Comment thread src/config.rs Outdated
Comment thread src/config.rs Outdated
Comment thread src/config.rs
}

impl WorkspaceConfig {
fn starter() -> Self {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems like a good candidate for a Default::default impl instead, unless there's a reason to not have a default?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is a reason for keeping these separate. WorkspaceConfig already has Default: that is the omitted-user-config state, where site.base_url stays None so later code can distinguish “no override was configured” from “use this explicit base URL.” starter() is only for scaffolding a new .build-eips.toml, where it is useful to write an explicit local dev value like http://127.0.0.1:1111. Collapsing those would either make omitted config behave like an override, or make the generated starter file less useful.

Comment thread src/config.rs
Comment on lines +473 to +482
/// Workspace-local bind address defaults for local server commands.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ServerSettings {
/// Host or interface address used by `serve` and `preview`.
pub host: String,

/// TCP port used by `serve` and `preview`.
pub port: u16,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would a SocketAddr be more appropriate?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd keep host and port separate. SocketAddr is an IP address plus a port, so it has nowhere to store a hostname. Making it accept localhost would mean resolving the name during config parsing, which turns reading a file into fallible name resolution and discards the original hostname. Keeping host: String stores what the user typed and lets the bind call resolve it when the server starts. Separate keys are also easier to read and edit in TOML.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

$ zola serve --interface example.com
error: invalid value 'example.com' for '--interface <INTERFACE>': invalid IP address syntax

For more information, try '--help'.

I haven't traced the whole path, but this seems like where host ends up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Zola’s --interface argument only accepts an IP address, so I updated the server config and CLI from host: String to interface: IpAddr. Invalid values such as hostnames are now rejected during config or CLI parsing rather than being passed through to Zola.

I also updated the downstream consumers and added coverage for hostname rejection, IPv4, IPv6, and Zola argument construction. The full test suite passes locally.

@ritorhymes

Copy link
Copy Markdown
Contributor Author

Hm, so is the intended directory structure something like:

  • ~/Workspace/.build-eips.toml
  • ~/Workspace/.local-build/...
  • ~/Workspace/ERCs/...
  • ~/Workspace/EIPs/...

?

Exactly.

@ritorhymes

Copy link
Copy Markdown
Contributor Author

I need to rebase this to align with the repo manifest changes merged today and do some work adjusting the architecture downstream (which may cascade down to earlier branches, maybe here), especially regarding theme ownership.

@ritorhymes
ritorhymes force-pushed the v2/04-workspace-config-discovery branch from 8c9b119 to 322e2f9 Compare June 21, 2026 22:41
@ritorhymes

Copy link
Copy Markdown
Contributor Author

Okay I just finished a system-wide reworking to accommodate the foundational changes introduced from #43's new architecture. This PR is ready to review and everything else is good downstream for this series.

@SamWilsn SamWilsn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This PR, on its own, doesn't add any functionality. I understand that it's foundational to later PRs, but the way you've split this up makes it hard to review. I can't tell if, for example, WorkspaceCommandContext is the best approach to solving a problem if I can't see what that problem is.

I've reviewed this for Rust-isms and such, but I'll have to come back here when I get to the later PR that uses the stuff introduced here.

Comment thread src/context.rs Outdated
Comment on lines +34 to +36
if !find_root::is_root(path).whatever_context("invalid root directory")? {
snafu::whatever!("invalid root directory");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems to have duplicated the is_root check, unless there's some reason to do it before and again after canonicalizing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair, I updated this to canonicalize an explicit path first and then perform a single is_root check on the canonical path.

I also added coverage for a nonexistent explicit path, which now reports the canonicalization failure directly.

Comment thread src/context.rs Outdated
Comment on lines +21 to +28
pub(crate) fn resolve_input_path(path: &Path) -> Result<PathBuf, Whatever> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
let cwd = std::env::current_dir().whatever_context("unable to get current directory")?;
Ok(cwd.join(path))
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If you're just going to canonicalize it later, there's no reason to do this step. canonicalize will resolve against the current working directory for you.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right for this call site canonicalize() already resolves relative paths against the current working directory, so resolve_input_path() is redundant here.

The helper is used later in v2/06-workspace-init-baseline for destination paths that may not exist yet and therefore cannot be canonicalized first. I'll remove it from this canonicalization path and introduce or retain it where that behavior is actually needed.

Comment thread src/config.rs Outdated

#[snafu(display(
"unable to parse repo manifest `{}`",
"unable to parse Build.toml `{}`",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
"unable to parse Build.toml `{}`",
"unable to parse {MANIFEST_FILE} `{}`",

Comment thread src/config.rs Outdated

#[snafu(display(
"repo manifest `{}` is invalid: {}",
"Build.toml `{}` is invalid: {}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
"Build.toml `{}` is invalid: {}",
"{MANIFEST_FILE} `{}` is invalid: {}",

Comment thread src/config.rs Outdated

let error = ActiveRepo::load(tempdir.path()).unwrap_err().to_string();

assert!(error.contains("Build.toml"), "{error}");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
assert!(error.contains("Build.toml"), "{error}");
assert!(error.contains(BUILD_MANIFEST), "{error}");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied using the existing constant name MANIFEST_FILE; BUILD_MANIFEST isn't defined in this codebase.

Comment thread src/context.rs Outdated
}
}

pub(crate) fn load_workspace_command_context(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should be something like:

impl WorkspaceCommandContext {
    fn load(...) { ... }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated this to WorkspaceCommandContext::load(args).

I used pub(crate) rather than a private fn because the method is called from the sibling workspace module.

Add workspace-local `.build-eips.toml` loading and starter configuration. Introduce `config::ActiveRepo` to load the selected checkout’s `Build.toml`, validate explicit `-C` roots, and expose active repository context to later commands. Keep source materialization, initialization, and diagnostics in their owning later branches.
@ritorhymes
ritorhymes force-pushed the v2/04-workspace-config-discovery branch from 322e2f9 to 54fbb1b Compare July 18, 2026 06:44
@ritorhymes

Copy link
Copy Markdown
Contributor Author

This PR, on its own, doesn't add any functionality. I understand that it's foundational to later PRs, but the way you've split this up makes it hard to review. I can't tell if, for example, WorkspaceCommandContext is the best approach to solving a problem if I can't see what that problem is.

Yes, as we know, downstream verification is the tradeoff of this decomposition. Allow me to help make this more reviewable by providing the missing problem and consumption context below.

The underlying problem is that V2 moves from a single-repository build model to a multi-repository local workspace. Later commands therefore need to resolve two distinct kinds of context:

  • Repository-owned configuration from Build.toml, represented by ActiveRepo: the active repository identity, declared sibling repositories, source locations, and theme pin.
  • Developer-owned workspace configuration from .build-eips.toml, represented by LoadedWorkspaceConfig: local server settings and site URL defaults. Its discovered location also anchors the workspace root that sibling checkouts, the editable theme, and build output are laid out under by convention.

This PR introduces those loaders, upward workspace discovery, and explicit -C root validation so downstream commands do not each reinvent path and configuration resolution.

The most useful downstream branches to inspect are:

  • v2/06-workspace-init-baseline: ActiveRepo drives sibling and theme cloning, and init writes the starter .build-eips.toml. Explicit root validation is already exercised here.
  • v2/07-workspace-doctor: this is the clearest branch for evaluating WorkspaceCommandContext specifically. Doctor must diagnose the active repository and the surrounding workspace independently. It needs to report where workspace discovery began and which config candidate, if any, was found even when the repository root, Build.toml, or workspace config fails to load. Keeping that discovery provenance separate prevents doctor from aborting at the first failed loading step.
  • v2/08-execution-policy-resolution: this is the most concentrated example of the broader configuration design. ActiveRepo and LoadedWorkspaceConfig are combined to resolve local versus remote sibling sources, per-repository build roots, source-materialization policy, and site base-URL precedence.
  • v2/09-local-zola-theme-runner through v2/11-serve-runtime and v2/12-static-preview-runtime: ServerBinding enters the Zola serve API in 09. Branch 11 resolves it from workspace defaults and CLI overrides and wires it into serving, while branch 12 reuses it for static preview.
  • v2/18-preprocessor-final-integration: the best branch for judging the architecture with the complete command and runtime system in its final form.

For a focused review of WorkspaceCommandContext, I would compare this PR with 07. For the overall split between repository and workspace configuration, 08 is probably the clearest consumer. If reviewing the foundation separately creates too much context switching, reviewing the API surface here and judging the full approach at the stack tip is also reasonable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants