Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically detect missing NuGet packages and restore #70851

Merged
merged 2 commits into from
Dec 4, 2023

Conversation

dibarbet
Copy link
Member

@dibarbet dibarbet commented Nov 16, 2023

Implements basic support for automatic nuget restore in the C# standalone project system. This is done by checking the project.assets.json to ensure all project dependencies are present.

Client PR: dotnet/vscode-csharp#6676

Looks like:
automatic_nuget_restore

@sharwell
Copy link
Member

Does this still work if the only change is removing a package reference? Why not just always run a restore after open? NuGet is expected to have a fast-path NOP if you run a restore after it was already up-to-date.

@dibarbet
Copy link
Member Author

Does this still work if the only change is removing a package reference? Why not just always run a restore after open? NuGet is expected to have a fast-path NOP if you run a restore after it was already up-to-date.

It works for both - we'll detect a missing project.assets.json and run a restore if that is the case.

@dibarbet dibarbet marked this pull request as ready for review November 21, 2023 21:51
@dibarbet dibarbet requested review from a team as code owners November 21, 2023 21:51

if (projectFileInfo.PackageReferences.IsEmpty)
{
// If there are no package references then there are no unresolved dependencies.
Copy link
Member

Choose a reason for hiding this comment

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

What about stuff from the SDK itself? I admit I don't know the details here but I'm just asking.

Copy link
Member Author

Choose a reason for hiding this comment

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

Missing stuff from the SDK should be covered under the presence of the project.assets.json. I think the only potential problem would be if you change TFM, but I'm not quite certain how to detect that...

Copy link
Member Author

Choose a reason for hiding this comment

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

Looks like there is some information in the assets file on the target frameworks that change if you modify TFMs. Going to take a look to see if its as simple as checking if the TFMs in the project match the TFMs in the lock file.

Copy link
Member

Choose a reason for hiding this comment

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

Missing stuff from the SDK should be covered under the presence of the project.assets.json.

I wasn't sure about the case where we've seen during dogfooding where you upgrade to some SDK version and the various implicit packs aren't actually in feeds yet.

Copy link
Member Author

Choose a reason for hiding this comment

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

I added some code to look at target framework changes as well. However I don't think it will be easy to detect SDK version changes - we'd have to track and listen to changes in the global.json or look at which SDK the build host used to load the project (if you manually upgrade without having a global.json). I don't think that is necessarily worth doing.

{
var message = string.Format(LanguageServerResources.Project_0_has_unresolved_dependencies, projectFileInfo.FilePath)
+ Environment.NewLine
+ string.Join(Environment.NewLine, unresolved.Select(r => $"{r.Name}-{r.VersionRange}"));
Copy link
Member

Choose a reason for hiding this comment

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

Should we be indenting this or something?

@jasonmalinowski
Copy link
Member

@dibarbet: Regarding @sharwell's point:

What about something like:

  1. Open a project, a restore happens (any number of ways.)
  2. Then remove a project reference

At that point the project.assets.json will exist and will contain all the things restored. Does that need to run a restore again to remove the reference from the project? I don't know if the underlying SDK tasks will ignore that or will still operate on the last restore.

@dibarbet
Copy link
Member Author

After offline discussion - to ensure we run restore when a package reference is removed or a version changes (but still satisfies the range), going to look at diffing the last set of the package reference items we saw on the project, than fall back to project.assets.json checks (in case things change on disk).

@dibarbet dibarbet force-pushed the automatic_nuget_restore branch from 98a3d40 to 696bf99 Compare November 28, 2023 22:00
@dibarbet dibarbet force-pushed the automatic_nuget_restore branch from 696bf99 to 809fd2f Compare November 28, 2023 23:02
Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

Feedback is all naming/ordering things and could be done as a follow-up.

Comment on lines +41 to +44
if (packageReferenceItems == null)
{
return ImmutableArray<PackageReference>.Empty;
}
Copy link
Member

Choose a reason for hiding this comment

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

This can happen? We aren't doing that for any of the other GetItems() checks above; do those need to be corrected too?

@@ -154,6 +160,7 @@ public override string ToString()
ImmutableArray<DocumentFileInfo> additionalDocuments,
ImmutableArray<DocumentFileInfo> analyzerConfigDocuments,
ImmutableArray<ProjectFileReference> projectReferences,
ImmutableArray<PackageReference> packageReferences,
Copy link
Member

Choose a reason for hiding this comment

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

Did you want to keep the constructor sorted with the properties?

{
private const string UnresolvedProjectDependenciesName = "workspace/_roslyn_projectHasUnresolvedDependencies";

internal static bool HasUnresolvedDependencies(ProjectFileInfo newProjectFileInfo, ProjectFileInfo? previousProjectFileInfo, ILogger logger)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
internal static bool HasUnresolvedDependencies(ProjectFileInfo newProjectFileInfo, ProjectFileInfo? previousProjectFileInfo, ILogger logger)
internal static bool NeedsRestore(ProjectFileInfo newProjectFileInfo, ProjectFileInfo? previousProjectFileInfo, ILogger logger)

Since this does a lot more than just check for unresolved dependencies now.

Comment on lines +29 to +33
if (newProjectFileInfo.TargetFramework != previousProjectFileInfo.TargetFramework)
{
// If the target framework has changed then we need to run a restore.
return true;
}
Copy link
Member

Choose a reason for hiding this comment

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

This will never happen, since we use TFM as a part of the identity of the project:

Contract.ThrowIfFalse(newProjectInfo.TargetFramework == _mostRecentFileInfo.TargetFramework);

If we want to diff on TFM addition/removal we'd need to do that elsewhere.

Copy link
Member Author

@dibarbet dibarbet Dec 4, 2023

Choose a reason for hiding this comment

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

Ah will remove, but this will still work- it will hit the case where we don't have any previous project file info and we'll trigger a restore.

return true;
}

if (!newPackageReferences.SetEquals(previousPackageReferences))
Copy link
Member

Choose a reason for hiding this comment

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

We should really just teach SetEquals to do the length check first, but that can be later.

private static bool CheckProjectAssetsForUnresolvedDependencies(ProjectFileInfo projectFileInfo, ILogger logger)
{
var projectAssetsPath = projectFileInfo.ProjectAssetsFilePath;
if (!File.Exists(projectAssetsPath))
Copy link
Member

Choose a reason for hiding this comment

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

Today I learned File.Exists() takes a null. I'm not sure how I feel about that.

@@ -26,7 +26,7 @@ internal class ProjectLoadTelemetryReporter(ILoggerFactory loggerFactory, Server
/// so that we are able to compare data accurately.
/// See https://github.com/OmniSharp/omnisharp-roslyn/blob/b2e64c6006beed49460f063117793f42ab2a8a5c/src/OmniSharp.MSBuild/ProjectLoadListener.cs#L36
/// </summary>
public async Task ReportProjectLoadTelemetryAsync(Dictionary<ProjectFileInfo, (ImmutableArray<CommandLineReference> MetadataReferences, OutputKind OutputKind)> projectFileInfos, ProjectToLoad projectToLoad, CancellationToken cancellationToken)
public async Task ReportProjectLoadTelemetryAsync(Dictionary<ProjectFileInfo, (ImmutableArray<CommandLineReference> MetadataReferences, OutputKind OutputKind, bool HasUnresolvedDependencies)> projectFileInfos, ProjectToLoad projectToLoad, CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public async Task ReportProjectLoadTelemetryAsync(Dictionary<ProjectFileInfo, (ImmutableArray<CommandLineReference> MetadataReferences, OutputKind OutputKind, bool HasUnresolvedDependencies)> projectFileInfos, ProjectToLoad projectToLoad, CancellationToken cancellationToken)
public async Task ReportProjectLoadTelemetryAsync(Dictionary<ProjectFileInfo, (ImmutableArray<CommandLineReference> MetadataReferences, OutputKind OutputKind, bool NeedsRestore)> projectFileInfos, ProjectToLoad projectToLoad, CancellationToken cancellationToken)

@@ -185,11 +185,13 @@ public async ValueTask<(ImmutableArray<CommandLineReference>, OutputKind)> Updat

WatchProjectAssetsFile(newProjectInfo, _fileChangeContext);

var hasUnresolvedDependencies = ProjectDependencyHelper.HasUnresolvedDependencies(newProjectInfo, _mostRecentFileInfo, logger);
Copy link
Member

Choose a reason for hiding this comment

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

Also rename to "needs restore".

@@ -195,7 +212,7 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList<Project
return binaryLogPath;
}

private async Task<(LSP.MessageType? FailureType, BuildHostProcessKind? PreferredKind)> LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken)
private async Task<(LSP.MessageType? FailureType, BuildHostProcessKind? PreferredKind, bool HasUnresolvedDependencies)> LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
private async Task<(LSP.MessageType? FailureType, BuildHostProcessKind? PreferredKind, bool HasUnresolvedDependencies)> LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken)
private async Task<(LSP.MessageType? FailureType, BuildHostProcessKind? PreferredKind, bool NeedsRestore)> LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken)

@dibarbet dibarbet merged commit 02de7d7 into dotnet:main Dec 4, 2023
@ghost ghost added this to the Next milestone Dec 4, 2023
@dibarbet
Copy link
Member Author

dibarbet commented Dec 4, 2023

responding to feedback in a followup

@dibarbet dibarbet deleted the automatic_nuget_restore branch December 5, 2023 19:31
@Cosifne Cosifne modified the milestones: Next, 17.9 P3 Jan 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants