Skip to content

feat: msbuildProperties — evaluation-time MSBuild properties - #3

Merged
Wtiben merged 3 commits into
Wtiben:mainfrom
Fireblade954:feat/msbuild-properties
Jul 27, 2026
Merged

feat: msbuildProperties — evaluation-time MSBuild properties#3
Wtiben merged 3 commits into
Wtiben:mainfrom
Fireblade954:feat/msbuild-properties

Conversation

@Fireblade954

Copy link
Copy Markdown
Contributor

Closes #1.

Adds a workspace-level msbuildProperties map (string → string) applied to every MSBuild evaluation behind dependency and task inference, as -p:NAME=VALUE:

dotnet:
  msbuildProperties:
    SkipApiClientGen: 'true'

Why: conditional, codegen-only ProjectReferences (ReferenceOutputAssembly=false, gated on a property like SkipApiClientGen) put build-ordering edges into the inferred graph that a production/Docker build never compiles — over-attributing affected projects, and platform-dependently when the condition involves globs. Evaluating with the same properties the deployment build uses makes the graph match how the code actually ships. See #1 for the full case.

How:

  • EvalEnv carries the properties; with_eval_env renders them via a new pure msbuild_property_args helper. That covers both the batched traversal and every per-project fallback in one place — command-line global properties propagate through the traversal's <MSBuild> task into child projects, so batch and per-project evaluation see identical values.
  • The properties are folded into the eval-cache digest (framed like a file, under a name no real file can have): a conditional PackageReference gated on such a property evaluates differently, so a cached package set from one configuration is never served under the other. Read and write keys stay symmetric — hash_task_contents now parses the toolchain config before the cache read.
  • Evaluation-time only: inferred task commands do not pass the properties, so moon run builds stay exactly what the project defines.
  • parse_manifest carries no toolchain config (same pre-existing limitation as dotnetRoot); comment extended.

Tests: schema/deserialization (camelCase key), -p: rendering incl. deterministic ordering, eval-cache invalidation on property add/change. Full suite green locally (112 passed, wasm target builds).

Validated end-to-end against our 177-project workspace: with msbuildProperties: {SkipApiClientGen: 'true'} the gateway over-attribution disappears and our fan-out regression suite passes. This is the port of the feature we run in production via the internal fork (innovadis-dev dotnet_toolchain v0.2.0), adapted to this repo's batched-evaluation architecture.

Fireblade954 and others added 3 commits July 27, 2026 10:27
Conditional, codegen-only ProjectReferences (ReferenceOutputAssembly=false,
gated on a property like SkipApiClientGen) put build-ordering edges into the
inferred graph that a production/Docker build never compiles, over-attributing
affected projects — and platform-dependently when the condition involves
globs. A workspace-level msbuildProperties map is applied to every dependency/
task-inference evaluation as -p:NAME=VALUE, so the graph matches how the code
actually deploys.

Properties ride on EvalEnv, so the batched traversal and every per-project
fallback see identical values (command-line global properties propagate
through the traversal's MSBuild task). They are folded into the eval-cache
digest: a conditional PackageReference gated on such a property evaluates
differently, so a cached package set from one configuration is never served
under another. Inferred task commands deliberately do not pass them —
evaluation-time only.
- Apply cargo fmt. `cargo fmt --all --check` is a CI gate and was failing on
  config.rs and msbuild.rs, which would have turned main red on merge.
- Document the setting in the README, the one place every other setting is
  listed, and state the two boundaries the implementation already has: the
  properties need to match how the code is really built (inferred builds pass
  --no-dependencies, so moon is the only thing ordering dependencies, and
  dropping an edge drops that ordering), and they reach evaluation but not
  `dotnet restore`, so a package gated on one can be hashed without being
  restored.
- Add two integration tests that assert the properties reach MSBuild and change
  the result, rather than only that they render into -p: arguments: one for the
  inferred graph, one for the evaluated package set, which is the case that
  justifies folding them into the eval-cache digest. Both reuse the existing
  matrix/cond fixture, whose gated reference is keyed on a property the project
  sets itself, so they also pin the global-property-wins semantics the feature
  depends on.
- Use the imported BTreeMap instead of the inline std path.
@Wtiben

Wtiben commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Thanks, this is well put together. The design fits the architecture, and folding the properties into the eval-cache digest is the part that would have been easy to miss.

I pushed 45f72bf to your branch rather than sending small stuff back and forth. Three things in it.

rustfmt. cargo fmt --all --check is a CI gate and was failing on config.rs and msbuild.rs, so this would have turned main red on merge.

README. The settings table is the one place every other setting is listed, so it now has a row, plus a short "Evaluation properties" section with the motivating example.

Integration coverage. Your three tests cover arg rendering, the schema and the digest, which are the mechanical parts, but nothing asserted that the properties reach MSBuild and change the result. Added two that do, both on the existing matrix/cond fixture:

  • msbuild_properties_are_applied_to_the_evaluation, where the gated ProjectReference drops out under EnableDeepRef=0.
  • msbuild_properties_change_the_evaluated_package_set, where ExtraPkg drops out while the unconditional RootPkg stays. That is the scenario your digest change exists for, so it seemed worth pinning against a real evaluation.

Cond.csproj sets <EnableDeepRef>1</EnableDeepRef> itself, so both tests also pin the global-property-wins semantics the feature depends on.

One thing worth passing on, because it briefly had me chasing a bug that did not exist: the sandbox resolves the wasm through find_wasm_file, which prefers target/wasm32-wasip1/release over debug. I had a stale release binary lying around, so my new tests ran against old code and failed. Your three tests are pure and never load the wasm, so they pass either way. Worth clearing that artifact before running the suite.

On the two boundaries, I documented them rather than changing anything, since scoping to evaluation looks right for a first cut:

  • Inferred builds pass --no-dependencies, so moon is the only thing ordering dependencies. Dropping an edge drops that ordering too, which is correct when the real build also skips the work behind it, and a trap otherwise.
  • A PackageReference gated on one of these properties is hashed but not restored, since restore does not get the properties.

Both are in the README and the setting's doc comment now.

CI is green on all four jobs, including ubuntu and macOS, which had never run on this branch (fork PRs need approval, I approved it). Left ## Unreleased as is, it gets renamed at release time.

@Wtiben
Wtiben merged commit 39ccdcb into Wtiben:main Jul 27, 2026
4 checks passed
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.

Evaluation-time MSBuild properties: conditional codegen-only ProjectReferences over-attribute the inferred graph

2 participants