Pack: reuse existing evaluations instead of forcing BuildProjectReferences=false - #7541
Merged
zivkan merged 3 commits intoJul 16, 2026
Merged
Conversation
…ences=false Remove BuildProjectReferences=false from the inner MSBuild task calls in the Pack targets so they reuse the already-evaluated/built project instances rather than forking redundant evaluations. Additionally detect single-targeting projects and omit the injected TargetFramework global property so those inner data-collection calls reuse the current instance instead of creating a pack-specific evaluation. Updates PackCommand_IncludeSource_AddsSourceFiles to assert that at least the authored source files are present, tolerating the additional build-generated Compile items now included in source packages. Addresses NuGet/Home#11530 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
baronfel
force-pushed
the
baronfel-pack-remove-buildprojectreferences
branch
from
July 7, 2026 14:11
5c658a7 to
4ef06e3
Compare
… _GetFrameworkAssemblyReferences under NoBuild _GetFrameworkAssemblyReferences depends on ResolveReferences, which participates in the project-to-project reference protocol. Removing BuildProjectReferences=false caused ResolveReferences to invoke Build on referenced projects, failing with NETSDK1085 during 'pack --no-build'. Restore BuildProjectReferences=false for that call only when NoBuild=true, so normal packs keep reusing already-built reference evaluations (no redundant evaluations) while no-build packs remain correct. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates NuGet’s MSBuild pack targets to avoid forcing a distinct global property set (BuildProjectReferences=false) in inner MSBuild calls, enabling MSBuild to reuse already-evaluated/already-built project instances from the preceding build and reducing redundant evaluations during pack.
Changes:
- Removed
BuildProjectReferences=falsefrom the innerMSBuildcall used to collect project-reference versions (_GetProjectReferenceVersions). - Refactored
_WalkEachTargetPerFrameworkto reuse the existing instance for single-targeting projects (omitTargetFrameworkas a global property) while keeping per-TFM addressing for cross-targeting projects; retainedBuildProjectReferences=falseonly for theNoBuild=truepath where_GetFrameworkAssemblyReferencesrunsResolveReferences. - Updated the
--include-sourcefunctional test to assert presence of authored sources without pinning an exactsrc/file count (to allow additional build-generatedCompileitems when reusing built instances).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/NuGet.Core/NuGet.Build.Tasks/NuGet.Build.Tasks.Pack.targets | Stops forcing BuildProjectReferences=false in most inner pack MSBuild calls and adds single-targeting evaluation reuse; preserves NoBuild safety for _GetFrameworkAssemblyReferences. |
| test/NuGet.Core.FuncTests/Dotnet.Integration.Test/PackCommandTests.cs | Adjusts --include-source assertions to validate authored sources are included without requiring a fixed count. |
ViktorHofer
reviewed
Jul 8, 2026
…alse item Address review feedback: instead of recomputing the TargetFrameworks conditions, transform the already-computed _ProjectsWithTFM items and append BuildProjectReferences=false to their AdditionalProperties. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
ViktorHofer
approved these changes
Jul 8, 2026
nkolev92
reviewed
Jul 15, 2026
zivkan
approved these changes
Jul 16, 2026
This was referenced Jul 22, 2026
This was referenced Jul 25, 2026
Merged
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pack: stop forcing
BuildProjectReferences=falsein inner MSBuild calls to reuse existing evaluationsFixes: NuGet/Home#11530
Fixes: NuGet/Home#14998
Summary
The NuGet Pack targets (
NuGet.Build.Tasks.Pack.targets) orchestrate packing by calling backinto the project (and its project references) with the
MSBuildtask to gather versions, sourcefiles, framework references, and suppressed dependencies. Several of those inner calls passed
BuildProjectReferences=falseas a global property.Because MSBuild keys project evaluations (and project instances) by *project path + global
properties*, adding
BuildProjectReferences=falseproduced a distinct global-property set fromthe instances that were already evaluated and built during the
Buildthat precedesPack. Theresult was a second, redundant evaluation (and instance) for each affected target framework and
project reference — effectively doubling evaluations for multi-targeting graphs.
This change removes
BuildProjectReferences=falsefrom those inner calls so they match thealready-evaluated/already-built instances and reuse them instead of forcing new evaluations.
It additionally eliminates a second source of redundant evaluations that is specific to
single-targeting projects (see Single-targeting evaluation reuse below).
Why this is safe:
BuildProjectReferencesis only meaningful to the P2P protocolThe key observation is that
BuildProjectReferencesonly influences the MSBuildproject-to-project reference (P2P) protocol — specifically
ResolveProjectReferences/_MSBuildProjectReferenceExistent, where it decides whether a referenced project is built (targetoutputs produced) or merely has its outputs predicted. It is not consulted by any of the targets the
Pack targets actually invoke on these calls (
_GetProjectVersion,SourceFilesProjectOutputGroup,_GetFrameworkAssemblyReferences,_GetFrameworksWithSuppressedDependencies, and the_WalkEachTargetPerFrameworkdata-collection targets). None of those targets participate in the P2Pprotocol.
So passing
BuildProjectReferences=falsegave no direct benefit to what Pack was collecting — itonly changed the global-property set, which forced MSBuild to spin up a separate evaluation and
re-compute target results, duplicating work that the preceding
Buildhad already done. Removing itlets Pack reuse those existing evaluations and target results with no change to the data it gathers.
Change detail
src/NuGet.Core/NuGet.Build.Tasks.Pack/NuGet.Build.Tasks.Pack.targets:_GetProjectReferenceVersions— removedProperties="BuildProjectReferences=false;"from theMSBuildtask that collects versions from project references. The child now inherits the parent'sglobal properties and reuses the reference's existing (outer) evaluation.
_WalkEachTargetPerFramework— deleted the_ProjectsWithTFMNoBuilditem entirely. It wasidentical to
_ProjectsWithTFMexcept for the extraBuildProjectReferences=falsemetadata._ProjectsWithTFMNoBuildnow use_ProjectsWithTFM(
TargetFramework=<tfm>only):SourceFilesProjectOutputGroup(only runs whenIncludeSource=true)_GetFrameworkAssemblyReferences_GetFrameworksWithSuppressedDependencies_WalkEachTargetPerFramework—_ProjectsWithTFMis now defined conditionally on whether theproject is cross-targeting:
'$(TargetFrameworks)' != ''): unchanged — each inner-framework instance isaddressed by its
TargetFramework=<tfm>global property.'$(TargetFrameworks)' == ''): theTargetFrameworkglobal property isomitted so the inner
MSBuildcalls reuse the current (already-built) instance instead of forkinga redundant single-TFM evaluation. This mirrors the existing
IsInnerBuilddetection already usedin the same file.
Net diff: 6 insertions / 7 deletions in the targets file.
Data-equivalence
The data produced by the changed targets is identical to the old targets for the common case:
.nuspec(main and symbols) — identical..nupkg— byte-identical.Validated by A/B packing the same projects with the unmodified vs. modified targets and diffing the
nuspec/package outputs.
Side effect: source packages can contain strictly-more
CompileitemsThere is one intentional behavioral change, and it only affects
pack --include-source(i.e.producing a
.symbols.nupkgwith asrc/folder). It does not affect normal packages.SourceFilesProjectOutputGroupreturns@(Compile)and depends only onPrepareForBuild;AssignTargetPaths. Previously theBuildProjectReferences=falsecall resolved to afresh, unbuilt instance, so
@(Compile)contained only the authored source files. Now that thecall reuses the already-built inner-framework instance,
@(Compile)also contains the compileitems that build targets injected during
Build(e.g.CoreGenerateAssemblyInfoadding<Assembly>.AssemblyInfo.cs, and the per-framework<FrameworkMoniker>.AssemblyAttributes.cs).Concretely, this is additive — every source file that used to be present is still present; the
source package now also includes the build-generated compile items. With the single-targeting
evaluation reuse (below), this now applies consistently to both single- and multi-targeting
projects — in both cases the
src/set gains the generated compile items, e.g.:Why this is acceptable
the debugger would step into, generated files included) is defensible and arguably more correct.
--include-sourcepacks are affected.BuildProjectReferences=falseon just theSourceFilescall) would keepa redundant evaluation for that path and was rejected in favor of eliminating the property entirely.
The existing functional test was updated to reflect this (see below).
Performance characteristics
Measured on a representative multi-targeting graph:
App→Lib1..Lib6→Core(8 projects), eachtargeting
net8.0;net48;netstandard2.0; defaultpack(no--include-source). Evaluation time isthe sum of all
ProjectEvaluationdurations captured in the pack binary log (with/profileevaluationenabled). Averages of 3 iterations per cell:BuildProjectReferencesevalsDeltas (baseline → modified):
BuildProjectReferences=falseevaluations.BuildProjectReferencesevaluations: 31 → 0.Notes:
across build nodes and overlap other work; eliminated evaluations do not translate 1:1 into
end-to-end time, but they meaningfully reduce evaluation CPU/allocation pressure.
restore, whose evaluations are identical for both configs(they cancel in the delta but inflate the totals). The 31-evaluation delta is purely pack-side.
regression concern raised on the issue) benefit proportionally more.
Single-targeting evaluation reuse
The multi-targeting numbers above come from removing
BuildProjectReferences=false. A second,independent redundancy affects single-targeting projects: the pack targets always injected
TargetFramework=<tfm>as a global property into the innerMSBuilddata-collection calls. On asingle-targeting project the main build/pack instance has no
TargetFrameworkglobal property, soadding one forks a distinct, pack-specific evaluation that duplicates the instance already built by the
preceding
Build.Detecting single-targeting (
'$(TargetFrameworks)' == '', matching the existingIsInnerBuildlogic)and omitting the
TargetFrameworkglobal lets those calls reuse the current instance. Measured on asingle-TFM (
net8.0) project,dotnet pack:The default
.nuspecand.nupkg(lib/) are identical to the pre-change output (only the randompsmdcpGUID differs). The--include-sourceside effect described above becomes consistent betweensingle- and multi-targeting projects.
Test changes
test/NuGet.Core.FuncTests/Dotnet.Integration.Test/PackCommandTests.cs::PackCommand_IncludeSource_AddsSourceFilessrc/files. With the source-package side effect above,multi-targeting projects legitimately include additional generated compile items.
(
ClassLibrary1.csproj,Class1.cs,Extensions/ExtensionMethods.cs,Utils/Utility.cs) viaAssert.Contains, without pinning an exact count — tolerating the additional generated files whilestill verifying authored sources are packaged.
No other tests assert on
src/source-package contents or onBuildProjectReferences.Test harness used to derive these conclusions
The measurements above were produced with a self-contained harness (outside the repo) rather than a
full repo build, using the SDK's shipped copy of the Pack targets as the substrate and applying the
identical edits made to the repo file. Reproduction steps:
Substrate / override. Pin SDK
9.0.315viaglobal.json. Inject a modified Pack targets fileinto
dotnet packby passing:-p:CustomBeforeMicrosoftCommonProps=<override.props>whereoverride.propssetsImportNuGetBuildTasksPackTargetsFromSdk=trueand pointsNuGetPackTaskAssemblyFileat the SDK'sreal
NuGet.Build.Tasks.Pack.dll(absolute path), and-p:NuGetBuildTasksPackTargets=<Pack.baseline.targets | Pack.modified.targets>.Pack.baseline.targetsis a verbatim copy of the SDK targets;Pack.modified.targetsis that copywith the same three edits described above. (Passing both properties is required — the SDK only sets
ImportNuGetBuildTasksPackTargetsFromSdkinside a block that is skipped whenNuGetBuildTasksPackTargetsis pre-set.)Graph.
App→Lib1..Lib6→Core; every project multi-targetsnet8.0;net48;netstandard2.0.Binlog + profiling. Each
dotnet packrun emits-bl:<run>.binlogwith/profileevaluationso evaluation timing is captured in the log.
Evaluation extraction. A small .NET console tool (
MSBuild.StructuredLogger) loads each binlog,enumerates every
ProjectEvaluation, and reports:BuildProjectReferences=false.Thermal protocols.
dotnet build-server shutdown, then delete all projectbin/objdirectories, thenpack. (Absolute evaluation times are higher — JIT/disk/first-run costs.)
.nupkgfiles between runs, leavingbin/objintact.Iterations. 3 runs per (config × thermal) cell; results averaged. The evaluation count
(96 → 65) was identical on every run; timing varied within a few percent.
Data-equivalence checks. Separately, the baseline vs. modified nuspec, dependency versions, and
default
.nupkgwere diffed to confirm byte/data identity; the--include-sourcecase was diffedto characterize the additive source-package change.