Skip to content

Commit 2a4b33e

Browse files
authored
Add support for flat launch settings (#49769)
1 parent 86e7689 commit 2a4b33e

27 files changed

+432
-206
lines changed

src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,11 +299,8 @@ public bool IsServerSupported(ProjectGraphNode projectNode, HotReloadAppModel ap
299299

300300
private LaunchSettingsProfile GetLaunchProfile(ProjectOptions projectOptions)
301301
{
302-
var projectDirectory = Path.GetDirectoryName(projectOptions.ProjectPath);
303-
Debug.Assert(projectDirectory != null);
304-
305302
return (projectOptions.NoLaunchProfile == true
306-
? null : LaunchSettingsProfile.ReadLaunchProfile(projectDirectory, projectOptions.LaunchProfileName, context.Reporter)) ?? new();
303+
? null : LaunchSettingsProfile.ReadLaunchProfile(projectOptions.ProjectPath, projectOptions.LaunchProfileName, context.Reporter)) ?? new();
307304
}
308305
}
309306
}

src/BuiltInTools/dotnet-watch/Process/LaunchSettingsProfile.cs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44

5+
using System.Diagnostics;
56
using System.Text.Json;
67
using System.Text.Json.Serialization;
8+
using Microsoft.DotNet.Cli.Commands;
9+
using Microsoft.DotNet.Cli.Commands.Run;
710

811
namespace Microsoft.DotNet.Watch
912
{
@@ -22,10 +25,30 @@ internal sealed class LaunchSettingsProfile
2225
public bool LaunchBrowser { get; init; }
2326
public string? LaunchUrl { get; init; }
2427

25-
internal static LaunchSettingsProfile? ReadLaunchProfile(string projectDirectory, string? launchProfileName, IReporter reporter)
28+
internal static LaunchSettingsProfile? ReadLaunchProfile(string projectPath, string? launchProfileName, IReporter reporter)
2629
{
27-
var launchSettingsPath = Path.Combine(projectDirectory, "Properties", "launchSettings.json");
28-
if (!File.Exists(launchSettingsPath))
30+
var projectDirectory = Path.GetDirectoryName(projectPath);
31+
Debug.Assert(projectDirectory != null);
32+
33+
var launchSettingsPath = CommonRunHelpers.GetPropertiesLaunchSettingsPath(projectDirectory, "Properties");
34+
bool hasLaunchSettings = File.Exists(launchSettingsPath);
35+
36+
var projectNameWithoutExtension = Path.GetFileNameWithoutExtension(projectPath);
37+
var runJsonPath = CommonRunHelpers.GetFlatLaunchSettingsPath(projectDirectory, projectNameWithoutExtension);
38+
bool hasRunJson = File.Exists(runJsonPath);
39+
40+
if (hasLaunchSettings)
41+
{
42+
if (hasRunJson)
43+
{
44+
reporter.Warn(string.Format(CliCommandStrings.RunCommandWarningRunJsonNotUsed, runJsonPath, launchSettingsPath));
45+
}
46+
}
47+
else if (hasRunJson)
48+
{
49+
launchSettingsPath = runJsonPath;
50+
}
51+
else
2952
{
3053
return null;
3154
}
@@ -39,7 +62,7 @@ internal sealed class LaunchSettingsProfile
3962
}
4063
catch (Exception ex)
4164
{
42-
reporter.Verbose($"Error reading launchSettings.json: {ex}.");
65+
reporter.Verbose($"Error reading '{launchSettingsPath}': {ex}.");
4366
return null;
4467
}
4568

src/Cli/dotnet/Commands/CliCommandStrings.resx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,8 @@ dotnet.config is a name don't translate.</comment>
692692
<value>Do not use arguments specified in launch profile to run the application.</value>
693693
</data>
694694
<data name="CommandOptionNoLaunchProfileDescription" xml:space="preserve">
695-
<value>Do not attempt to use launchSettings.json to configure the application.</value>
695+
<value>Do not attempt to use launchSettings.json or [app].run.json to configure the application.</value>
696+
<comment>{Locked="launchSettings.json"}{Locked=".run.json"}</comment>
696697
</data>
697698
<data name="CommandOptionProjectDescription" xml:space="preserve">
698699
<value>The path to the project file to run (defaults to the current directory if there is only one project).</value>
@@ -766,8 +767,8 @@ dotnet.config is a name don't translate.</comment>
766767
<value>Description</value>
767768
</data>
768769
<data name="DeserializationExceptionMessage" xml:space="preserve">
769-
<value>An error was encountered when reading launchSettings.json.
770-
{0}</value>
770+
<value>An error was encountered when reading '{0}': {1}</value>
771+
<comment>{0} is file path. {1} is exception message.</comment>
771772
</data>
772773
<data name="DetailDescription" xml:space="preserve">
773774
<value>Show detail result of the query.</value>
@@ -1743,7 +1744,12 @@ The default is to publish a framework-dependent application.</value>
17431744
{1}</value>
17441745
</data>
17451746
<data name="RunCommandExceptionCouldNotLocateALaunchSettingsFile" xml:space="preserve">
1746-
<value>The specified launch profile '{0}' could not be located.</value>
1747+
<value>Cannot use launch profile '{0}' because the launch settings file could not be located. Locations tried:
1748+
{1}</value>
1749+
</data>
1750+
<data name="RunCommandWarningRunJsonNotUsed" xml:space="preserve">
1751+
<value>Warning: Settings from '{0}' are not used because '{1}' has precedence.</value>
1752+
<comment>{0} is an app.run.json file path. {1} is a launchSettings.json file path.</comment>
17471753
</data>
17481754
<data name="RunCommandExceptionMultipleProjects" xml:space="preserve">
17491755
<value>Specify which project file to use because {0} contains more than one project file.</value>

src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,10 @@ public static Dictionary<string, string> GetGlobalPropertiesFromArgs(string[] ar
3636
}
3737
return globalProperties;
3838
}
39+
40+
public static string GetPropertiesLaunchSettingsPath(string directoryPath, string propertiesDirectoryName)
41+
=> Path.Combine(directoryPath, propertiesDirectoryName, "launchSettings.json");
42+
43+
public static string GetFlatLaunchSettingsPath(string directoryPath, string projectNameWithoutExtension)
44+
=> Path.Join(directoryPath, $"{projectNameWithoutExtension}.run.json");
3945
}

src/Cli/dotnet/Commands/Run/LaunchSettings/LaunchSettingsManager.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ static LaunchSettingsManager()
2222
};
2323
}
2424

25-
public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSettingsJsonContents, string? profileName = null)
25+
public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSettingsPath, string? profileName = null)
2626
{
27+
var launchSettingsJsonContents = File.ReadAllText(launchSettingsPath);
2728
try
2829
{
2930
var jsonDocumentOptions = new JsonDocumentOptions
@@ -115,7 +116,7 @@ public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSett
115116
}
116117
catch (JsonException ex)
117118
{
118-
return new LaunchSettingsApplyResult(false, string.Format(CliCommandStrings.DeserializationExceptionMessage, ex.Message));
119+
return new LaunchSettingsApplyResult(false, string.Format(CliCommandStrings.DeserializationExceptionMessage, launchSettingsPath, ex.Message));
119120
}
120121
}
121122

src/Cli/dotnet/Commands/Run/RunCommand.cs

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,9 @@ internal bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel
202202
return true;
203203
}
204204

205-
var launchSettingsPath = ReadCodeFromStdin ? null : TryFindLaunchSettings(ProjectFileFullPath ?? EntryPointFileFullPath!);
206-
if (!File.Exists(launchSettingsPath))
205+
var launchSettingsPath = ReadCodeFromStdin ? null : TryFindLaunchSettings(projectOrEntryPointFilePath: ProjectFileFullPath ?? EntryPointFileFullPath!, launchProfile: LaunchProfile);
206+
if (launchSettingsPath is null)
207207
{
208-
if (!string.IsNullOrEmpty(LaunchProfile))
209-
{
210-
Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionCouldNotLocateALaunchSettingsFile, launchSettingsPath).Bold().Red());
211-
}
212208
return true;
213209
}
214210

@@ -221,8 +217,7 @@ internal bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel
221217

222218
try
223219
{
224-
var launchSettingsFileContents = File.ReadAllText(launchSettingsPath);
225-
var applyResult = LaunchSettingsManager.TryApplyLaunchSettings(launchSettingsFileContents, LaunchProfile);
220+
var applyResult = LaunchSettingsManager.TryApplyLaunchSettings(launchSettingsPath, LaunchProfile);
226221
if (!applyResult.Success)
227222
{
228223
Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionCouldNotApplyLaunchSettings, profileName, applyResult.FailureReason).Bold().Red());
@@ -241,13 +236,9 @@ internal bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel
241236

242237
return true;
243238

244-
static string? TryFindLaunchSettings(string projectOrEntryPointFilePath)
239+
static string? TryFindLaunchSettings(string projectOrEntryPointFilePath, string? launchProfile)
245240
{
246-
var buildPathContainer = File.Exists(projectOrEntryPointFilePath) ? Path.GetDirectoryName(projectOrEntryPointFilePath) : projectOrEntryPointFilePath;
247-
if (buildPathContainer is null)
248-
{
249-
return null;
250-
}
241+
var buildPathContainer = Path.GetDirectoryName(projectOrEntryPointFilePath)!;
251242

252243
string propsDirectory;
253244

@@ -263,8 +254,37 @@ internal bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel
263254
propsDirectory = "Properties";
264255
}
265256

266-
var launchSettingsPath = Path.Combine(buildPathContainer, propsDirectory, "launchSettings.json");
267-
return launchSettingsPath;
257+
string launchSettingsPath = CommonRunHelpers.GetPropertiesLaunchSettingsPath(buildPathContainer, propsDirectory);
258+
bool hasLaunchSetttings = File.Exists(launchSettingsPath);
259+
260+
string appName = Path.GetFileNameWithoutExtension(projectOrEntryPointFilePath);
261+
string runJsonPath = CommonRunHelpers.GetFlatLaunchSettingsPath(buildPathContainer, appName);
262+
bool hasRunJson = File.Exists(runJsonPath);
263+
264+
if (hasLaunchSetttings)
265+
{
266+
if (hasRunJson)
267+
{
268+
Reporter.Output.WriteLine(string.Format(CliCommandStrings.RunCommandWarningRunJsonNotUsed, runJsonPath, launchSettingsPath).Yellow());
269+
}
270+
271+
return launchSettingsPath;
272+
}
273+
274+
if (hasRunJson)
275+
{
276+
return runJsonPath;
277+
}
278+
279+
if (!string.IsNullOrEmpty(launchProfile))
280+
{
281+
Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionCouldNotLocateALaunchSettingsFile, launchProfile, $"""
282+
{launchSettingsPath}
283+
{runJsonPath}
284+
""").Bold().Red());
285+
}
286+
287+
return null;
268288
}
269289
}
270290

@@ -573,6 +593,7 @@ public static RunCommand FromParseResult(ParseResult parseResult)
573593
out string? entryPointFilePath);
574594

575595
bool noBuild = parseResult.HasOption(RunCommandParser.NoBuildOption);
596+
string launchProfile = parseResult.GetValue(RunCommandParser.LaunchProfileOption) ?? string.Empty;
576597

577598
if (readCodeFromStdin && entryPointFilePath != null)
578599
{
@@ -583,6 +604,11 @@ public static RunCommand FromParseResult(ParseResult parseResult)
583604
throw new GracefulException(CliCommandStrings.InvalidOptionForStdin, RunCommandParser.NoBuildOption.Name);
584605
}
585606

607+
if (!string.IsNullOrWhiteSpace(launchProfile))
608+
{
609+
throw new GracefulException(CliCommandStrings.InvalidOptionForStdin, RunCommandParser.LaunchProfileOption.Name);
610+
}
611+
586612
// If '-' is specified as the input file, read all text from stdin into a temporary file and use that as the entry point.
587613
// We create a new directory for each file so other files are not included in the compilation.
588614
// We fail if the file already exists to avoid reusing the same file for multiple stdin runs (in case the random name is duplicate).
@@ -605,7 +631,7 @@ public static RunCommand FromParseResult(ParseResult parseResult)
605631
noBuild: noBuild,
606632
projectFileFullPath: projectFilePath,
607633
entryPointFileFullPath: entryPointFilePath,
608-
launchProfile: parseResult.GetValue(RunCommandParser.LaunchProfileOption) ?? string.Empty,
634+
launchProfile: launchProfile,
609635
noLaunchProfile: parseResult.HasOption(RunCommandParser.NoLaunchProfileOption),
610636
noLaunchProfileArguments: parseResult.HasOption(RunCommandParser.NoLaunchProfileArgumentsOption),
611637
noRestore: parseResult.HasOption(RunCommandParser.NoRestoreOption) || parseResult.HasOption(RunCommandParser.NoBuildOption),

src/Cli/dotnet/Commands/Test/SolutionAndProjectUtility.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModule
248248
string projectFullPath = project.GetPropertyValue(ProjectProperties.ProjectFullPath);
249249

250250
// TODO: Support --launch-profile and pass it here.
251-
var launchSettings = TryGetLaunchProfileSettings(Path.GetDirectoryName(projectFullPath)!, project.GetPropertyValue(ProjectProperties.AppDesignerFolder), noLaunchProfile, profileName: null);
251+
var launchSettings = TryGetLaunchProfileSettings(Path.GetDirectoryName(projectFullPath)!, Path.GetFileNameWithoutExtension(projectFullPath), project.GetPropertyValue(ProjectProperties.AppDesignerFolder), noLaunchProfile, profileName: null);
252252

253253
return new TestModule(runProperties, PathUtility.FixFilePath(projectFullPath), targetFramework, isTestingPlatformApplication, isTestProject, launchSettings, project.GetPropertyValue(ProjectProperties.TargetPath));
254254

@@ -270,20 +270,36 @@ static RunProperties GetRunProperties(ProjectInstance project, ICollection<ILogg
270270
}
271271
}
272272

273-
private static ProjectLaunchSettingsModel? TryGetLaunchProfileSettings(string projectDirectory, string appDesignerFolder, bool noLaunchProfile, string? profileName)
273+
private static ProjectLaunchSettingsModel? TryGetLaunchProfileSettings(string projectDirectory, string projectNameWithoutExtension, string appDesignerFolder, bool noLaunchProfile, string? profileName)
274274
{
275275
if (noLaunchProfile)
276276
{
277277
return null;
278278
}
279279

280-
var launchSettingsPath = Path.Combine(projectDirectory, appDesignerFolder, "launchSettings.json");
281-
if (!File.Exists(launchSettingsPath))
280+
var launchSettingsPath = CommonRunHelpers.GetPropertiesLaunchSettingsPath(projectDirectory, appDesignerFolder);
281+
bool hasLaunchSettings = File.Exists(launchSettingsPath);
282+
283+
var runJsonPath = CommonRunHelpers.GetFlatLaunchSettingsPath(projectDirectory, projectNameWithoutExtension);
284+
bool hasRunJson = File.Exists(runJsonPath);
285+
286+
if (hasLaunchSettings)
287+
{
288+
if (hasRunJson)
289+
{
290+
Reporter.Output.WriteLine(string.Format(CliCommandStrings.RunCommandWarningRunJsonNotUsed, runJsonPath, launchSettingsPath).Yellow());
291+
}
292+
}
293+
else if (hasRunJson)
294+
{
295+
launchSettingsPath = runJsonPath;
296+
}
297+
else
282298
{
283299
return null;
284300
}
285301

286-
var result = LaunchSettingsManager.TryApplyLaunchSettings(File.ReadAllText(launchSettingsPath), profileName);
302+
var result = LaunchSettingsManager.TryApplyLaunchSettings(launchSettingsPath, profileName);
287303
if (!result.Success)
288304
{
289305
Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionCouldNotApplyLaunchSettings, profileName, result.FailureReason).Bold().Red());

src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf

Lines changed: 15 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)