Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project>

<!-- A multi-targeted project restored with a single TargetFramework must not overwrite its
packages.lock.json.

Ordinary restore never does that: `dotnet restore` evaluates the outer project, where
TargetFramework is empty and TargetFrameworks lists both frameworks. Dependabot's NuGet
discovery does. It invokes Restore through ProjectReferenceBuildTargets, and MSBuild
calls a referenced multi-targeted project with one TargetFramework, so NuGet wrote a lock
file for that framework alone. Both of Dependabot's NuGet PRs dropped net8.0 from these
projects' lock files and failed locked restore (issue #63).
tools/lockfile-probe/check.sh reproduces that invocation and fails if it rewrites a lock
file.

In that context the lock file goes to obj/ instead. Nothing reads it there. The real lock
file is left for the full restore, which Dependabot also runs, to update correctly. -->
<PropertyGroup Condition="'$(TargetFrameworks)' != '' and '$(TargetFramework)' != ''">
<NuGetLockFilePath>$(MSBuildProjectExtensionsPath)single-framework.packages.lock.json</NuGetLockFilePath>
</PropertyGroup>

</Project>
9 changes: 9 additions & 0 deletions scripts/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,15 @@ fi
# This packs it and builds a real net8.0 consumer against the .nupkg, then builds this
# repository's own packaged projects, from a copy, with that analyzer attached. `full` only:
# it packs and restores, which the inner loop should not pay for on every run.
# Dependabot's NuGet discovery restores referenced projects one framework at a time, which used
# to rewrite the multi-targeted lock files without net8.0 (#63). Directory.Build.targets stops
# that; this reproduces the exact invocation in a copy of the tree and fails if any lock file
# changes. `full` only: it restores every test project's graph.
if [[ "$MODE" == "full" ]]; then
step "A single-framework restore leaves the lock files alone"
run "lockfile-probe" tools/lockfile-probe/check.sh || true
fi

if [[ "$MODE" == "full" ]]; then
step "Analyzer reaches a consumer, and the kernel passes it"
run "analyzer-probe" tools/analyzer-probe/check.sh || true
Expand Down
54 changes: 54 additions & 0 deletions tools/lockfile-probe/check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# check.sh -- prove a single-framework restore cannot rewrite a multi-targeted lock file.
#
# Dependabot's NuGet discovery runs, in each project directory:
# dotnet msbuild <project> /t:Restore,ResolveProjectReferences,GenerateBuildDependencyFile
# /p:ProjectReferenceBuildTargets="Restore;ResolveProjectReferences;GenerateBuildDependencyFile"
# (dependabot-core, nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs).
# Through the project references, that restored RulesKernel, RulesKernel.Randomness and
# RulesKernel.Testing with one TargetFramework each and wrote net10.0-only lock files: issue #63,
# reproduced exactly this way. Directory.Build.targets redirects the lock file in that context.
#
# This runs the same invocation from every test project in a clean copy of the tree and fails
# if any tracked packages.lock.json changed. It reads the copy's own `git status`, so a lock file
# that changed is named, not inferred.
#
# tools/lockfile-probe/check.sh
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT

# A copy of the working tree -- tracked files plus untracked ones that are not ignored, the
# inventory tools/repo-checks.py examines -- committed into a scratch repository so its lock
# files have a baseline to diff against. This checkout's obj/ and lock files are never touched.
mkdir -p "$WORK/tree"
(cd "$REPO_ROOT" && git ls-files -z --cached --others --exclude-standard | xargs -0 cp --parents -t "$WORK/tree")
(cd "$WORK/tree" && git init -q && git add -A \
&& git -c user.name=probe -c user.email=probe@example.invalid commit -qm baseline)

mapfile -t projects < <(cd "$WORK/tree" && grep -rl --include=*.csproj '<IsTestProject>true</IsTestProject>' tests probes | sort)
if [[ "${#projects[@]}" -eq 0 ]]; then
echo "FAIL: no test projects found to restore from -- this check proved nothing"
exit 1
fi

for project in "${projects[@]}"; do
if ! (cd "$WORK/tree/$(dirname "$project")" && dotnet msbuild "$(basename "$project")" \
"/t:Restore,ResolveProjectReferences,GenerateBuildDependencyFile" \
'/p:ProjectReferenceBuildTargets="Restore;ResolveProjectReferences;GenerateBuildDependencyFile"' \
/p:TreatWarningsAsErrors=false -nologo -v:q) >"$WORK/restore.log" 2>&1; then
echo "FAIL: the Dependabot-style restore of $project did not run"
tail -10 "$WORK/restore.log"
exit 1
fi
done

changed="$(cd "$WORK/tree" && git status --porcelain -- '*packages.lock.json')"
if [[ -n "$changed" ]]; then
echo "FAIL: a Dependabot-style restore rewrote lock files (issue #63):"
printf '%s\n' "$changed" | sed 's/^/ /'
exit 1
fi
echo "ok ${#projects[@]} Dependabot-style restore(s) left every packages.lock.json unchanged"