Skip to content

Projects to deploy #79430

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

Merged
merged 3 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,9 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
])
],
SyntaxError = syntaxError,
ProjectsToRebuild = [project.Id],
ProjectsToRestart = ImmutableDictionary<ProjectId, ImmutableArray<ProjectId>>.Empty.Add(project.Id, [])
ProjectsToRebuild = [projectId],
ProjectsToRestart = ImmutableDictionary<ProjectId, ImmutableArray<ProjectId>>.Empty.Add(projectId, []),
ProjectsToRedeploy = [projectId],
};
};

Expand Down Expand Up @@ -276,6 +277,7 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
SyntaxError = null,
ProjectsToRebuild = [],
ProjectsToRestart = ImmutableDictionary<ProjectId, ImmutableArray<ProjectId>>.Empty,
ProjectsToRedeploy = [],
};
};

Expand Down
14 changes: 9 additions & 5 deletions src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,8 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
Diagnostics = solutionUpdate.Diagnostics,
SyntaxError = solutionUpdate.SyntaxError,
ProjectsToRestart = solutionUpdate.ProjectsToRestart,
ProjectsToRebuild = solutionUpdate.ProjectsToRebuild
ProjectsToRebuild = solutionUpdate.ProjectsToRebuild,
ProjectsToRedeploy = solutionUpdate.ProjectsToRedeploy,
};
}

Expand Down Expand Up @@ -787,7 +788,8 @@ public async ValueTask<ImmutableArray<ImmutableArray<ActiveStatementSpan>>> GetB
var oldProject = LastCommittedSolution.GetProject(projectId);
if (oldProject == null)
{
// document is in a project that's been added to the solution
// Document is in a project that's been added to the solution
// No need to map the breakpoint from its original (base) location supplied by the debugger to a new one.
continue;
}

Expand All @@ -807,7 +809,9 @@ public async ValueTask<ImmutableArray<ImmutableArray<ActiveStatementSpan>>> GetB
var (oldDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newDocument, cancellationToken).ConfigureAwait(false);
if (oldDocument == null)
{
// Document is out-of-sync, can't reason about its content with respect to the binaries loaded in the debuggee.
// Document is either
// 1) added -- no need to map the breakpoint from original location to a new one
// 2) out-of-sync, in which case we can't reason about its content with respect to the binaries loaded in the debuggee.
continue;
}

Expand Down Expand Up @@ -906,7 +910,7 @@ public async ValueTask<ImmutableArray<ActiveStatementSpan>> GetAdjustedActiveSta
var oldProject = LastCommittedSolution.GetProject(newProject.Id);
if (oldProject == null)
{
// TODO: https://github.com/dotnet/roslyn/issues/1204
// TODO: https://github.com/dotnet/roslyn/issues/79423
// Enumerate all documents of the new project.
return [];
}
Expand Down Expand Up @@ -939,7 +943,7 @@ public async ValueTask<ImmutableArray<ActiveStatementSpan>> GetAdjustedActiveSta
var (oldUnmappedDocument, _) = await LastCommittedSolution.GetDocumentAndStateAsync(newUnmappedDocument, cancellationToken).ConfigureAwait(false);
if (oldUnmappedDocument == null)
{
// document out-of-date
// document added or out-of-date
continue;
}

Expand Down
157 changes: 114 additions & 43 deletions src/Features/Core/Portable/EditAndContinue/EditSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,11 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
return false;
}

if (HasProjectLevelDifferences(oldProject, newProject, differences) && differences == null)
{
return true;
}

foreach (var documentId in newProject.State.DocumentStates.GetChangedStateIds(oldProject.State.DocumentStates, ignoreUnchangedContent: true))
{
var document = newProject.GetRequiredDocument(documentId);
Expand All @@ -361,7 +366,7 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
return true;
}

differences.Value.ChangedOrAddedDocuments.Add(document);
differences.ChangedOrAddedDocuments.Add(document);
}

foreach (var documentId in newProject.State.DocumentStates.GetAddedStateIds(oldProject.State.DocumentStates))
Expand All @@ -377,7 +382,7 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
return true;
}

differences.Value.ChangedOrAddedDocuments.Add(document);
differences.ChangedOrAddedDocuments.Add(document);
}

foreach (var documentId in newProject.State.DocumentStates.GetRemovedStateIds(oldProject.State.DocumentStates))
Expand All @@ -393,7 +398,7 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
return true;
}

differences.Value.DeletedDocuments.Add(document);
differences.DeletedDocuments.Add(document);
}

// The following will check for any changes in non-generated document content (editorconfig, additional docs).
Expand Down Expand Up @@ -436,10 +441,64 @@ internal static async ValueTask<bool> HasDifferencesAsync(Project oldProject, Pr
return false;
}

internal static async Task GetProjectDifferencesAsync(TraceLog log, Project oldProject, Project newProject, ProjectDifferences documentDifferences, ArrayBuilder<Diagnostic> diagnostics, CancellationToken cancellationToken)
/// <summary>
/// Return true if projects might have differences in state other than document content that migth affect EnC.
/// The checks need to be fast. May return true even if the changes don't actually affect the behavior.
/// </summary>
internal static bool HasProjectLevelDifferences(Project oldProject, Project newProject, ProjectDifferences? differences)
{
Debug.Assert(oldProject.CompilationOptions != null);
Debug.Assert(newProject.CompilationOptions != null);

if (oldProject.ParseOptions != newProject.ParseOptions ||
HasDifferences(oldProject.CompilationOptions, newProject.CompilationOptions) ||
oldProject.AssemblyName != newProject.AssemblyName)
{
if (differences != null)
{
differences.HasSettingChange = true;
}
else
{
return true;
}
}

if (!oldProject.MetadataReferences.SequenceEqual(newProject.MetadataReferences) ||
!oldProject.ProjectReferences.SequenceEqual(newProject.ProjectReferences))
{
if (differences != null)
{
differences.HasReferenceChange = true;
}
else
{
return true;
}
}

return false;
}

/// <summary>
/// True if given compilation options differ in a way that might affect EnC.
/// </summary>
internal static bool HasDifferences(CompilationOptions oldOptions, CompilationOptions newOptions)
=> !oldOptions
.WithSyntaxTreeOptionsProvider(newOptions.SyntaxTreeOptionsProvider)
.WithStrongNameProvider(newOptions.StrongNameProvider)
.WithXmlReferenceResolver(newOptions.XmlReferenceResolver)
.Equals(newOptions);

internal static async Task GetProjectDifferencesAsync(TraceLog log, Project? oldProject, Project newProject, ProjectDifferences documentDifferences, ArrayBuilder<Diagnostic> diagnostics, CancellationToken cancellationToken)
{
documentDifferences.Clear();

if (oldProject == null)
{
return;
}

if (!await HasDifferencesAsync(oldProject, newProject, documentDifferences, cancellationToken).ConfigureAwait(false))
{
return;
Expand Down Expand Up @@ -697,6 +756,16 @@ private static bool HasReferenceRudeEdits(ImmutableDictionary<string, OneOrMany<
return hasRudeEdit;
}

private static bool HasAddedReference(Compilation oldCompilation, Compilation newCompilation)
{
using var pooledOldNames = SharedPools.StringIgnoreCaseHashSet.GetPooledObject();
var oldNames = pooledOldNames.Object;
Debug.Assert(oldNames.Comparer == AssemblyIdentityComparer.SimpleNameComparer);

oldNames.AddRange(oldCompilation.ReferencedAssemblyNames.Select(static r => r.Name));
return newCompilation.ReferencedAssemblyNames.Any(static (newReference, oldNames) => !oldNames.Contains(newReference.Name), oldNames);
}

internal static async ValueTask<ProjectChanges> GetProjectChangesAsync(
ActiveStatementsMap baseActiveStatements,
Compilation oldCompilation,
Expand Down Expand Up @@ -900,9 +969,11 @@ public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(
using var _1 = ArrayBuilder<ManagedHotReloadUpdate>.GetInstance(out var deltas);
using var _2 = ArrayBuilder<(Guid ModuleId, ImmutableArray<(ManagedModuleMethodId Method, NonRemappableRegion Region)>)>.GetInstance(out var nonRemappableRegions);
using var _3 = ArrayBuilder<ProjectBaseline>.GetInstance(out var newProjectBaselines);
using var _4 = ArrayBuilder<(ProjectId id, Guid mvid)>.GetInstance(out var projectsToStale);
using var _5 = ArrayBuilder<ProjectId>.GetInstance(out var projectsToUnstale);
using var _4 = ArrayBuilder<ProjectId>.GetInstance(out var addedUnbuiltProjects);
using var _5 = ArrayBuilder<ProjectId>.GetInstance(out var projectsToRedeploy);
using var _6 = PooledDictionary<ProjectId, ArrayBuilder<Diagnostic>>.GetInstance(out var diagnosticBuilders);

// Project differences for currently analyzed project. Reused and cleared.
using var projectDifferences = new ProjectDifferences();

// After all projects have been analyzed "true" value indicates changed document that is only included in stale projects.
Expand Down Expand Up @@ -945,39 +1016,14 @@ void UpdateChangedDocumentsStaleness(bool isStale)
}

var oldProject = oldSolution.GetProject(newProject.Id);
if (oldProject == null)
{
Log.Write($"EnC state of {newProject.GetLogDisplay()} queried: project not loaded");

// TODO (https://github.com/dotnet/roslyn/issues/1204):
//
// When debugging session is started some projects might not have been loaded to the workspace yet (may be explicitly unloaded by the user).
// We capture the base solution. Edits in files that are in projects that haven't been loaded won't be applied
// and will result in source mismatch when the user steps into them.
//
// We can allow project to be added by including all its documents here.
// When we analyze these documents later on we'll check if they match the PDB.
// If so we can add them to the committed solution and detect further changes.
// It might be more efficient though to track added projects separately.

continue;
}

Debug.Assert(oldProject.SupportsEditAndContinue());

if (!oldProject.ProjectSettingsSupportEditAndContinue(Log))
{
// reason alrady reported
continue;
}

projectDiagnostics = ArrayBuilder<Diagnostic>.GetInstance();
Debug.Assert(oldProject == null || oldProject.SupportsEditAndContinue());

await GetProjectDifferencesAsync(Log, oldProject, newProject, projectDifferences, projectDiagnostics, cancellationToken).ConfigureAwait(false);
projectDifferences.Log(Log, newProject);

if (projectDifferences.HasDocumentChanges)
if (projectDifferences.IsEmpty)
{
Log.Write($"Found {projectDifferences.ChangedOrAddedDocuments.Count} potentially changed, {projectDifferences.DeletedDocuments.Count} deleted document(s) in project {newProject.GetLogDisplay()}");
continue;
}

var (mvid, mvidReadError) = await DebuggingSession.GetProjectModuleIdAsync(newProject, cancellationToken).ConfigureAwait(false);
Expand All @@ -989,8 +1035,9 @@ void UpdateChangedDocumentsStaleness(bool isStale)
if (mvid == staleModuleId || mvidReadError != null)
{
Log.Write($"EnC state of {newProject.GetLogDisplay()} queried: project is stale");
UpdateChangedDocumentsStaleness(isStale: true);

// Track changed documents that are only included in stale or unbuilt projects:
UpdateChangedDocumentsStaleness(isStale: true);
continue;
}

Expand All @@ -1003,17 +1050,32 @@ void UpdateChangedDocumentsStaleness(bool isStale)
// The MVID is required for emit so we consider the error permanent and report it here.
// Bail before analyzing documents as the analysis needs to read the PDB which will likely fail if we can't even read the MVID.
projectDiagnostics.Add(mvidReadError);
projectSummaryToReport = ProjectAnalysisSummary.ValidChanges;
continue;
}

if (mvid == Guid.Empty)
{
Log.Write($"Changes not applied to {newProject.GetLogDisplay()}: project not built");
// If the project has been added to the solution, ask the project system to build it.
if (oldProject == null)
{
Log.Write($"Project build requested for {newProject.GetLogDisplay()}");
addedUnbuiltProjects.Add(newProject.Id);
}
else
{
Log.Write($"Changes not applied to {newProject.GetLogDisplay()}: project not built");
}

// Track changed documents that are only included in stale or unbuilt projects:
UpdateChangedDocumentsStaleness(isStale: true);
continue;
}

if (oldProject == null)
{
continue;
}

// Ensure that all changed documents are in-sync. Once a document is in-sync it can't get out-of-sync.
// Therefore, results of further computations based on base snapshots of changed documents can't be invalidated by
// incoming events updating the content of out-of-sync documents.
Expand Down Expand Up @@ -1079,8 +1141,7 @@ void UpdateChangedDocumentsStaleness(bool isStale)

// Unsupported changes in referenced assemblies will be reported below.
if (projectSummary is ProjectAnalysisSummary.NoChanges or ProjectAnalysisSummary.ValidInsignificantChanges &&
oldProject.MetadataReferences.SequenceEqual(newProject.MetadataReferences) &&
oldProject.ProjectReferences.SequenceEqual(newProject.ProjectReferences))
!projectDifferences.HasReferenceChange)
{
continue;
}
Expand Down Expand Up @@ -1140,6 +1201,14 @@ void UpdateChangedDocumentsStaleness(bool isStale)
continue;
}

// If the project references new dependencies, the host needs to invoke ReferenceCopyLocalPathsOutputGroup target on this project
// to deploy these dependencies to the projects output directory. The deployment shouldn't overwrite existing files.
// It should only happen if the project has no rude edits (especially not rude edits related to references) -- we bailed above if so.
if (HasAddedReference(oldCompilation, newCompilation))
{
projectsToRedeploy.Add(newProject.Id);
}

if (projectSummary is ProjectAnalysisSummary.NoChanges or ProjectAnalysisSummary.ValidInsignificantChanges)
{
continue;
Expand Down Expand Up @@ -1286,9 +1355,9 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
}
finally
{
if (projectSummaryToReport.HasValue)
if (projectSummaryToReport.HasValue || !projectDiagnostics.IsEmpty)
{
Telemetry.LogProjectAnalysisSummary(projectSummaryToReport.Value, newProject.State.ProjectInfo.Attributes.TelemetryId, projectDiagnostics);
Telemetry.LogProjectAnalysisSummary(projectSummaryToReport, newProject.State.ProjectInfo.Attributes.TelemetryId, projectDiagnostics);
}

if (!projectDiagnostics.IsEmpty)
Expand Down Expand Up @@ -1338,6 +1407,7 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
solution,
updates,
diagnostics,
addedUnbuiltProjects,
runningProjects,
out var projectsToRestart,
out var projectsToRebuild);
Expand All @@ -1352,7 +1422,8 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance
diagnostics,
syntaxError: null,
projectsToRestart,
projectsToRebuild);
projectsToRebuild,
projectsToRedeploy.ToImmutable());
}
catch (Exception e) when (LogException(e) && FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public void LogAnalysisTime(TimeSpan span)
public void LogSyntaxError()
=> _hadSyntaxErrors = true;

public void LogProjectAnalysisSummary(ProjectAnalysisSummary summary, Guid projectTelemetryId, IEnumerable<Diagnostic> diagnostics)
public void LogProjectAnalysisSummary(ProjectAnalysisSummary? summary, Guid projectTelemetryId, IEnumerable<Diagnostic> diagnostics)
{
lock (_guard)
{
Expand All @@ -110,6 +110,10 @@ public void LogProjectAnalysisSummary(ProjectAnalysisSummary summary, Guid proje

switch (summary)
{
case null:
// report diagnostics only
break;

case ProjectAnalysisSummary.NoChanges:
break;

Expand Down
Loading
Loading