Skip to content

Default BuildProjectReferences=false when NoBuild=true (fixes NETSDK1085 during --no-build with P2P references) #55176

Description

@baronfel

Summary

Proposal: have the SDK default BuildProjectReferences=false when NoBuild=true, mirroring the existing rule for DesignTimeBuild. This would make project-to-project (P2P) reference resolution consistent with the --no-build contract and centrally fix a class of NETSDK1085 failures that occur when any target chain reaches ResolveReferences under NoBuild=true.

Background / current behavior

--no-build forwards NoBuild=true as a global property, which is inherited by referenced projects. Reference resolution in Microsoft.Common.CurrentVersion.targets (ResolveProjectReferences) chooses between two branches based on BuildProjectReferences:

  • non-building GetTargetPath call, taken when BuildProjectReferences != ''true''
  • invoke the reference''s Build target, taken when BuildProjectReferences == ''true''
<!-- Microsoft.Common.CurrentVersion.targets (~line 2133): non-building path -->
<MSBuild Projects="@(_MSBuildProjectReferenceExistent)" Targets="GetTargetPath"
         Condition="... ('$(BuildingInsideVisualStudio)' == 'true' or '$(BuildProjectReferences)' != 'true') ..." />

<!-- (~line 2153): building path -->
<MSBuild Projects="@(_MSBuildProjectReferenceExistent)" Targets="%(_MSBuildProjectReferenceExistent.Targets)"
         Condition="... '$(BuildProjectReferences)' == 'true' ..." />

Today there is no NoBuild -> BuildProjectReferences rule, and _GlobalPropertiesToRemoveFromProjectReferences only strips OutputPath (not NoBuild). So with the default BuildProjectReferences=true, a --no-build operation that resolves references will:

  1. take the building branch and invoke Build on each P2P reference, then
  2. each reference sees the inherited NoBuild=true, and
  3. _CheckForBuildWithNoBuild (Microsoft.NET.Sdk.targets) fires error NETSDK1085: The 'NoBuild' property was set to true but the 'Build' target was invoked.
<!-- Microsoft.NET.Sdk.targets (~line 260) -->
<Target Name="_CheckForBuildWithNoBuild" Condition="'$(NoBuild)' == 'true'">
  <NETSdkError ResourceName="NoBuildRequested" />
</Target>

How this surfaced

This was hit by NuGet''s Pack targets: _GetFrameworkAssemblyReferences DependsOnTargets="ResolveReferences". When packing a project that has P2P references with dotnet pack --no-build, resolving references invoked Build on the referenced projects and failed with NETSDK1085. See NuGet/NuGet.Client#7541 for the NuGet-side fix and a full repro. publish --no-build avoids the same shape only because it hand-rolls a separate _PublishNoBuildAlternative path in Microsoft.NET.Publish.targets.

Proposal

Add an SDK default that mirrors the existing DesignTimeBuild rule:

<!-- existing, Microsoft.Common.CurrentVersion.targets ~line 375 -->
<BuildProjectReferences Condition="'$(BuildProjectReferences)' == '' and '$(DesignTimeBuild)' == 'true'">false</BuildProjectReferences>

<!-- proposed addition -->
<BuildProjectReferences Condition="'$(BuildProjectReferences)' == '' and '$(NoBuild)' == 'true'">false</BuildProjectReferences>

(Exact placement TBD — could live in the .NET SDK props/targets rather than Common, so it only affects SDK-style projects.)

Why this is reasonable

  • Semantically correct. --no-build asserts "everything is already built; just consume outputs." Building references contradicts that; resolving them via GetTargetPath (read existing output paths) is exactly what no-build means.
  • Cascades correctly. Because NoBuild is global and inherited by every reference, each reference''s own evaluation independently sets BuildProjectReferences=false, so the whole transitive graph resolves without building — no need to also mark it global.
  • Precedent exists. DesignTimeBuild=true already forces BuildProjectReferences=false via the same mechanism.
  • Centralizes a fix. Any target chain that reaches ResolveReferences under NoBuild (Pack, custom output-group targets, third-party targets) benefits, instead of each having to special-case it the way publish and NuGet Pack do.

Considerations / risks

  • Different failure mode when preconditions are not met. If someone runs --no-build without having built first (or with stale/missing reference outputs), today they get NETSDK1085; afterward they''d get a downstream "reference assembly not found" style error from GetTargetPath returning a non-existent path. Arguably clearer, but it is a diagnostic change.
  • Removes a (very unusual) capability. "Don''t build me, but do build my references" would no longer be expressible via NoBuild alone. This seems like an anti-pattern, but calling it out.
  • Only a default. Guarded on '$(BuildProjectReferences)' == '', so anyone who explicitly sets BuildProjectReferences=true keeps current behavior.

Questions to dig into

  1. Should this live in Common.targets (all project types) or the .NET SDK targets/props (SDK-style only)?
  2. Does test --no-build / any other NoBuild consumer rely on references being built today?
  3. Interaction with BuildingInsideVisualStudio (VS already takes the non-building branch regardless).
  4. Would this let Microsoft.NET.Publish.targets retire its bespoke no-build alternative, or is that still needed for other reasons?

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions