Skip to content

Commit eec49ce

Browse files
committed
File based programs
1 parent d65f939 commit eec49ce

File tree

51 files changed

+1024
-277
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1024
-277
lines changed

sdk.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<File Path="test.sh" />
2424
</Folder>
2525
<Folder Name="/src/">
26+
<Project Path="src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.shproj" Id="374c251e-bf99-45b2-a58e-40229ed8aaca" />
2627
<Project Path="src/Microsoft.DotNet.ProjectTools/Microsoft.DotNet.ProjectTools.csproj" />
2728
<Project Path="src/Microsoft.DotNet.TemplateLocator/Microsoft.DotNet.TemplateLocator.csproj" />
2829
<Project Path="src/Microsoft.Net.Sdk.Compilers.Toolset/Microsoft.Net.Sdk.Compilers.Toolset.csproj" />

src/BuiltInTools/Watch.Aspire/DotNetWatchLauncher.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static async Task<bool> RunAsync(string workingDirectory, DotNetWatchOpti
2727
var rootProjectOptions = new ProjectOptions()
2828
{
2929
IsRootProject = true,
30-
ProjectPath = options.ProjectPath,
30+
Representation = new ProjectRepresentation(options.ProjectPath, entryPointFilePath: null),
3131
WorkingDirectory = workingDirectory,
3232
TargetFramework = null,
3333
BuildArguments = [],

src/BuiltInTools/Watch/Aspire/AspireServiceFactory.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public async ValueTask<RunningProject> StartProjectAsync(string dcpId, string se
106106
{
107107
ObjectDisposedException.ThrowIf(_isDisposed, this);
108108

109-
_logger.LogDebug("Starting project: {Path}", projectOptions.ProjectPath);
109+
_logger.LogDebug("Starting: '{Path}'", projectOptions.Representation.ProjectOrEntryPointFilePath);
110110

111111
var processTerminationSource = new CancellationTokenSource();
112112
var outputChannel = Channel.CreateUnbounded<OutputLine>(s_outputChannelOptions);
@@ -143,7 +143,7 @@ public async ValueTask<RunningProject> StartProjectAsync(string dcpId, string se
143143
if (runningProject == null)
144144
{
145145
// detailed error already reported:
146-
throw new ApplicationException($"Failed to launch project '{projectOptions.ProjectPath}'.");
146+
throw new ApplicationException($"Failed to launch '{projectOptions.Representation.ProjectOrEntryPointFilePath}'.");
147147
}
148148

149149
await _service.NotifySessionStartedAsync(dcpId, sessionId, runningProject.ProcessId, cancellationToken);
@@ -221,7 +221,7 @@ private ProjectOptions GetProjectOptions(ProjectLaunchRequest projectLaunchInfo)
221221
return new()
222222
{
223223
IsRootProject = false,
224-
ProjectPath = projectLaunchInfo.ProjectPath,
224+
Representation = ProjectRepresentation.FromProjectOrEntryPointFilePath(projectLaunchInfo.ProjectPath),
225225
WorkingDirectory = Path.GetDirectoryName(projectLaunchInfo.ProjectPath) ?? throw new InvalidOperationException(),
226226
BuildArguments = _hostProjectOptions.BuildArguments,
227227
Command = "run",

src/BuiltInTools/Watch/Browser/BrowserLauncher.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,6 @@ private bool CanLaunchBrowser(ProjectOptions projectOptions, [NotNullWhen(true)]
128128
private LaunchSettingsProfile GetLaunchProfile(ProjectOptions projectOptions)
129129
{
130130
return (projectOptions.NoLaunchProfile == true
131-
? null : LaunchSettingsProfile.ReadLaunchProfile(projectOptions.ProjectPath, projectOptions.LaunchProfileName, logger)) ?? new();
131+
? null : LaunchSettingsProfile.ReadLaunchProfile(projectOptions.Representation, projectOptions.LaunchProfileName, logger)) ?? new();
132132
}
133133
}

src/BuiltInTools/Watch/Build/EvaluationResult.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ public static ImmutableDictionary<string, string> GetGlobalBuildOptions(IEnumera
6666
/// </summary>
6767
public static EvaluationResult? TryCreate(
6868
ProjectGraphFactory factory,
69-
string rootProjectPath,
7069
ILogger logger,
7170
GlobalOptions options,
7271
EnvironmentOptions environmentOptions,
@@ -76,7 +75,6 @@ public static ImmutableDictionary<string, string> GetGlobalBuildOptions(IEnumera
7675
var buildReporter = new BuildReporter(logger, options, environmentOptions);
7776

7877
var projectGraph = factory.TryLoadProjectGraph(
79-
rootProjectPath,
8078
logger,
8179
projectGraphRequired: true,
8280
cancellationToken);
@@ -94,7 +92,7 @@ public static ImmutableDictionary<string, string> GetGlobalBuildOptions(IEnumera
9492
{
9593
if (!rootNode.ProjectInstance.Build([TargetNames.Restore], loggers))
9694
{
97-
logger.LogError("Failed to restore project '{Path}'.", rootProjectPath);
95+
logger.LogError("Failed to restore '{Path}'.", rootNode.ProjectInstance.FullPath);
9896
loggers.ReportOutput();
9997
return null;
10098
}

src/BuiltInTools/Watch/Build/ProjectGraphFactory.cs

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,85 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Immutable;
5+
using System.Reflection;
6+
using System.Runtime.Versioning;
57
using Microsoft.Build.Evaluation;
8+
using Microsoft.Build.Execution;
69
using Microsoft.Build.Graph;
10+
using Microsoft.DotNet.ProjectTools;
711
using Microsoft.Extensions.Logging;
812
using ILogger = Microsoft.Extensions.Logging.ILogger;
913

1014
namespace Microsoft.DotNet.Watch;
1115

12-
internal sealed class ProjectGraphFactory(ImmutableDictionary<string, string> globalOptions)
16+
internal sealed class ProjectGraphFactory
1317
{
1418
/// <summary>
1519
/// Reuse <see cref="ProjectCollection"/> with XML element caching to improve performance.
1620
///
1721
/// The cache is automatically updated when build files change.
1822
/// https://github.com/dotnet/msbuild/blob/b6f853defccd64ae1e9c7cf140e7e4de68bff07c/src/Build/Definition/ProjectCollection.cs#L343-L354
1923
/// </summary>
20-
private readonly ProjectCollection _collection = new(
21-
globalProperties: globalOptions,
22-
loggers: [],
23-
remoteLoggers: [],
24-
ToolsetDefinitionLocations.Default,
25-
maxNodeCount: 1,
26-
onlyLogCriticalEvents: false,
27-
loadProjectsReadOnly: false,
28-
useAsynchronousLogging: false,
29-
reuseProjectRootElementCache: true);
24+
private readonly ProjectCollection _collection;
25+
26+
private readonly ImmutableDictionary<string, string> _globalOptions;
27+
private readonly ProjectRepresentation _rootProject;
28+
29+
// Only the root project can be virtual. #:project does not support targeting other single-file projects.
30+
private readonly VirtualProjectBuilder? _virtualRootProjectBuilder;
31+
32+
public ProjectGraphFactory(
33+
ProjectRepresentation rootProject,
34+
string? targetFramework,
35+
ImmutableDictionary<string, string> globalOptions)
36+
{
37+
_collection = new(
38+
globalProperties: globalOptions,
39+
loggers: [],
40+
remoteLoggers: [],
41+
ToolsetDefinitionLocations.Default,
42+
maxNodeCount: 1,
43+
onlyLogCriticalEvents: false,
44+
loadProjectsReadOnly: false,
45+
useAsynchronousLogging: false,
46+
reuseProjectRootElementCache: true);
47+
48+
_globalOptions = globalOptions;
49+
_rootProject = rootProject;
50+
51+
if (rootProject.EntryPointFilePath != null)
52+
{
53+
_virtualRootProjectBuilder = new VirtualProjectBuilder(rootProject.EntryPointFilePath, targetFramework ?? GetProductTargetFramework());
54+
}
55+
}
56+
57+
private static string GetProductTargetFramework()
58+
{
59+
var attribute = typeof(VirtualProjectBuilder).Assembly.GetCustomAttribute<TargetFrameworkAttribute>() ?? throw new InvalidOperationException();
60+
var version = new FrameworkName(attribute.FrameworkName).Version;
61+
return $"net{version.Major}.{version.Minor}";
62+
}
3063

3164
/// <summary>
3265
/// Tries to create a project graph by running the build evaluation phase on the <paramref name="rootProjectFile"/>.
3366
/// </summary>
3467
public ProjectGraph? TryLoadProjectGraph(
35-
string rootProjectFile,
3668
ILogger logger,
3769
bool projectGraphRequired,
3870
CancellationToken cancellationToken)
3971
{
40-
var entryPoint = new ProjectGraphEntryPoint(rootProjectFile, globalOptions);
72+
var entryPoint = new ProjectGraphEntryPoint(_rootProject.ProjectGraphPath, _globalOptions);
4173
try
4274
{
43-
return new ProjectGraph([entryPoint], _collection, projectInstanceFactory: null, cancellationToken);
75+
return new ProjectGraph([entryPoint], _collection, (path, globalProperties, collection) => CreateProjectInstance(path, globalProperties, collection, logger), cancellationToken);
76+
}
77+
catch (ProjectCreationFailedException)
78+
{
79+
// Errors have already been reported.
4480
}
4581
catch (Exception e) when (e is not OperationCanceledException)
4682
{
47-
// ProejctGraph aggregates OperationCanceledException exception,
83+
// ProjectGraph aggregates OperationCanceledException exception,
4884
// throw here to propagate the cancellation.
4985
cancellationToken.ThrowIfCancellationRequested();
5086

@@ -54,7 +90,10 @@ internal sealed class ProjectGraphFactory(ImmutableDictionary<string, string> gl
5490
{
5591
foreach (var inner in innerExceptions)
5692
{
57-
Report(inner);
93+
if (inner is not ProjectCreationFailedException)
94+
{
95+
Report(inner);
96+
}
5897
}
5998
}
6099
else
@@ -77,4 +116,38 @@ void Report(Exception e)
77116

78117
return null;
79118
}
119+
120+
private ProjectInstance CreateProjectInstance(string projectPath, Dictionary<string, string> globalProperties, ProjectCollection projectCollection, ILogger logger)
121+
{
122+
if (_virtualRootProjectBuilder != null && projectPath == _rootProject.ProjectGraphPath)
123+
{
124+
var anyError = false;
125+
126+
_virtualRootProjectBuilder.CreateProjectInstance(
127+
projectCollection,
128+
(sourceFile, textSpan, message) =>
129+
{
130+
anyError = true;
131+
logger.LogError("{Location}: {Message}", sourceFile.GetLocationString(textSpan), message);
132+
},
133+
out var projectInstance,
134+
out _);
135+
136+
if (anyError)
137+
{
138+
throw new ProjectCreationFailedException();
139+
}
140+
141+
return projectInstance;
142+
}
143+
144+
return new ProjectInstance(
145+
projectPath,
146+
globalProperties,
147+
toolsVersion: "Current",
148+
subToolsetVersion: null,
149+
projectCollection);
150+
}
151+
152+
private sealed class ProjectCreationFailedException() : Exception();
80153
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.DotNet.ProjectTools;
5+
6+
namespace Microsoft.DotNet.Watch;
7+
8+
/// <summary>
9+
/// Project can be reprented by project file or by entry point file (for single-file apps).
10+
/// </summary>
11+
internal readonly struct ProjectRepresentation(string projectGraphPath, string? projectPath, string? entryPointFilePath)
12+
{
13+
/// <summary>
14+
/// Path used in Project Graph (may be virtual).
15+
/// </summary>
16+
public readonly string ProjectGraphPath = projectGraphPath;
17+
18+
/// <summary>
19+
/// Path to an physical (non-virtual) project, if available.
20+
/// </summary>
21+
public readonly string? PhysicalPath = projectPath;
22+
23+
/// <summary>
24+
/// Path to an entry point file, if available.
25+
/// </summary>
26+
public readonly string? EntryPointFilePath = entryPointFilePath;
27+
28+
public ProjectRepresentation(string? projectPath, string? entryPointFilePath)
29+
: this(projectPath ?? VirtualProjectBuilder.GetVirtualProjectPath(entryPointFilePath!), projectPath, entryPointFilePath)
30+
{
31+
}
32+
33+
public string ProjectOrEntryPointFilePath
34+
=> PhysicalPath ?? EntryPointFilePath!;
35+
36+
public string GetContainingDirectory()
37+
=> Path.GetDirectoryName(ProjectOrEntryPointFilePath)!;
38+
39+
public static ProjectRepresentation FromProjectOrEntryPointFilePath(string projectOrEntryPointFilePath)
40+
=> string.Equals(Path.GetExtension(projectOrEntryPointFilePath), ".csproj", StringComparison.OrdinalIgnoreCase)
41+
? new(projectPath: null, entryPointFilePath: projectOrEntryPointFilePath)
42+
: new(projectPath: projectOrEntryPointFilePath, entryPointFilePath: null);
43+
44+
public ProjectRepresentation WithProjectGraphPath(string projectGraphPath)
45+
=> new(projectGraphPath, PhysicalPath, EntryPointFilePath);
46+
}

src/BuiltInTools/Watch/Context/ProjectOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace Microsoft.DotNet.Watch;
66
internal sealed record ProjectOptions
77
{
88
public required bool IsRootProject { get; init; }
9-
public required string ProjectPath { get; init; }
9+
public required ProjectRepresentation Representation { get; init; }
1010
public required string WorkingDirectory { get; init; }
1111
public required string? TargetFramework { get; init; }
1212
public required IReadOnlyList<string> BuildArguments { get; init; }

src/BuiltInTools/Watch/HotReload/CompilationHandler.cs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ internal sealed class CompilationHandler : IDisposable
4848
public CompilationHandler(DotNetWatchContext context)
4949
{
5050
_context = context;
51-
_processRunner = processRunner;
52-
Workspace = new HotReloadMSBuildWorkspace(logger, projectFile => (instances: _projectInstances.GetValueOrDefault(projectFile, []), project: null));
51+
Workspace = new HotReloadMSBuildWorkspace(context.Logger, projectFile => (instances: _projectInstances.GetValueOrDefault(projectFile, []), project: null));
5352
_hotReloadService = new HotReloadService(Workspace.CurrentSolution.Services, () => ValueTask.FromResult(GetAggregateCapabilities()));
5453
}
5554

@@ -848,20 +847,20 @@ private static ImmutableDictionary<string, ImmutableArray<ProjectInstance>> Crea
848847
keySelector: static group => group.Key,
849848
elementSelector: static group => group.Select(static node => node.ProjectInstance).ToImmutableArray());
850849

851-
public async Task UpdateProjectConeAsync(ProjectGraph projectGraph, string projectPath, CancellationToken cancellationToken)
850+
public async Task UpdateProjectConeAsync(ProjectGraph projectGraph, ProjectRepresentation project, CancellationToken cancellationToken)
852851
{
853-
_logger.LogInformation("Loading projects ...");
852+
Logger.LogInformation("Loading projects ...");
854853
var stopwatch = Stopwatch.StartNew();
855854

856855
_projectInstances = CreateProjectInstanceMap(projectGraph);
857856

858-
var solution = await Workspace.UpdateProjectConeAsync(projectPath, cancellationToken);
857+
var solution = await Workspace.UpdateProjectConeAsync(project.ProjectGraphPath, cancellationToken);
859858
await SolutionUpdatedAsync(solution, "project update", cancellationToken);
860859

861-
_logger.LogInformation("Projects loaded in {Time}s.", stopwatch.Elapsed.TotalSeconds.ToString("0.0"));
860+
Logger.LogInformation("Projects loaded in {Time}s.", stopwatch.Elapsed.TotalSeconds.ToString("0.0"));
862861
}
863862

864-
public async Task UpdateFileContentAsync(ImmutableList<ChangedFile> changedFiles, CancellationToken cancellationToken)
863+
public async Task UpdateFileContentAsync(IReadOnlyList<ChangedFile> changedFiles, CancellationToken cancellationToken)
865864
{
866865
var solution = await Workspace.UpdateFileContentAsync(changedFiles.Select(static f => (f.Item.FilePath, f.Kind.Convert())), cancellationToken);
867866
await SolutionUpdatedAsync(solution, "document update", cancellationToken);
@@ -872,16 +871,16 @@ private Task SolutionUpdatedAsync(Solution newSolution, string operationDisplayN
872871

873872
private async Task ReportSolutionFilesAsync(Solution solution, int updateId, string operationDisplayName, CancellationToken cancellationToken)
874873
{
875-
_logger.LogDebug("Solution after {Operation}: v{Version}", operationDisplayName, updateId);
874+
Logger.LogDebug("Solution after {Operation}: v{Version}", operationDisplayName, updateId);
876875

877-
if (!_logger.IsEnabled(LogLevel.Trace))
876+
if (!Logger.IsEnabled(LogLevel.Trace))
878877
{
879878
return;
880879
}
881880

882881
foreach (var project in solution.Projects)
883882
{
884-
_logger.LogDebug(" Project: {Path}", project.FilePath);
883+
Logger.LogDebug(" Project: {Path}", project.FilePath);
885884

886885
foreach (var document in project.Documents)
887886
{
@@ -902,7 +901,7 @@ private async Task ReportSolutionFilesAsync(Solution solution, int updateId, str
902901
async ValueTask InspectDocumentAsync(TextDocument document, string kind)
903902
{
904903
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
905-
_logger.LogDebug(" {Kind}: {FilePath} [{Checksum}]", kind, document.FilePath, Convert.ToBase64String(text.GetChecksum().ToArray()));
904+
Logger.LogDebug(" {Kind}: {FilePath} [{Checksum}]", kind, document.FilePath, Convert.ToBase64String(text.GetChecksum().ToArray()));
906905
}
907906
}
908907
}

0 commit comments

Comments
 (0)