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

Extend dotnet run to invoke a run-command-producing Target #42240

Merged
merged 40 commits into from
Aug 23, 2024

Conversation

baronfel
Copy link
Member

@baronfel baronfel commented Jul 18, 2024

Fixes #42155

This PR implements a new extensibility point in dotnet run handling that allows other build logic to configure the way the binary launched by dotnet run is discovered and run.

See details on the linked issue, but the gist is that if the project implements the new ComputeRunArguments target then that target will be used to provide overrides for the

  • RunArguments
  • RunCommand
  • RunWorkingDirectory

properties, which were already used to define how a project should be launched.

Putting this ability behind a Target means that any prerequisites required to launch a project can be discovered and shown to the user.

When target execution fails, we show that to the user via the existing MSBuild Console Logger. Ideally this would be terminal logger but that's internal at the moment.

image

If we hack it to reflection-load TerminalLogger, it looks much better IMO:
image

Testing:

  • Tests showing using the new extensibility point
  • Tests showing how arbitrary errors during execution are relayed to the user

@baronfel baronfel changed the title Spike Extend dotnet run to invoke a run-command-producing Target Jul 28, 2024
@baronfel baronfel force-pushed the extend-run-with-target branch from fce1998 to c35ca62 Compare July 31, 2024 19:44
@baronfel baronfel marked this pull request as ready for review August 12, 2024 18:41
@baronfel baronfel requested review from MiYanni and a team August 12, 2024 18:41
@baronfel baronfel force-pushed the extend-run-with-target branch from 8d6ff76 to 5f097a2 Compare August 12, 2024 19:40
Comment on lines 35 to 46
var oldPath = Directory.GetCurrentDirectory();
_basePath = basePath;
Directory.SetCurrentDirectory(_basePath);
Telemetry.Telemetry.CurrentSessionId = null;
try
{
action();
}
finally
{
Directory.SetCurrentDirectory(oldPath);
_basePath = null;
Copy link
Member Author

Choose a reason for hiding this comment

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

The intent of this helper is to make the app act like it's in a given path, but certain codeflows were using Directory.GetCurrentDirectory(). That meant that this helper wasn't doing what it was expected to do. This aligns the helper with its intent.

@@ -129,7 +129,7 @@ public static CliArgument<string> DefaultToCurrentDirectory(this CliArgument<str
{
Description = CommonLocalizableStrings.DisableBuildServersOptionDescription
}
.ForwardAsMany(_ => new string[] { "-p:UseRazorBuildServer=false", "-p:UseSharedCompilation=false", "/nodeReuse:false" });
.ForwardAsMany(_ => ["--property:UseRazorBuildServer=false", "--property:UseSharedCompilation=false", "/nodeReuse:false"]);
Copy link
Member Author

Choose a reason for hiding this comment

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

For testing purposes, since we're using the shared Property option, these output properties to forward to MSBuild are all unified to --project for maximal clarity.

@@ -173,8 +173,8 @@ public static bool DiagOptionPrecedesSubcommand(this string[] args, string subCo

internal static string GetCommandLineRuntimeIdentifier(this ParseResult parseResult)
{
return parseResult.HasOption(RunCommandParser.RuntimeOption) ?
parseResult.GetValue(RunCommandParser.RuntimeOption) :
return parseResult.HasOption(CommonOptions.RuntimeOption) ?
Copy link
Member Author

Choose a reason for hiding this comment

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

remove the bespoke options in favor of the common ones

Comment on lines +20 to +29
[InlineData(new string[] { "-p:prop1=true" }, new string[] { "--property:prop1=true" })]
[InlineData(new string[] { "--property:prop1=true" }, new string[] { "--property:prop1=true" })]
[InlineData(new string[] { "--property", "prop1=true" }, new string[] { "--property:prop1=true" })]
[InlineData(new string[] { "-p", "prop1=true" }, new string[] { "--property:prop1=true" })]
[InlineData(new string[] { "-p", "prop1=true", "-p", "prop2=false" }, new string[] { "--property:prop1=true", "--property:prop2=false" })]
[InlineData(new string[] { "-p:prop1=true;prop2=false" }, new string[] { "--property:prop1=true", "--property:prop2=false" })]
[InlineData(new string[] { "-p", "MyProject.csproj", "-p:prop1=true" }, new string[] { "--property:prop1=true" })]
// The longhand --property option should never be treated as a project
[InlineData(new string[] { "--property", "MyProject.csproj", "-p:prop1=true" }, new string[] { "-p:MyProject.csproj", "-p:prop1=true" })]
[InlineData(new string[] { "--disable-build-servers" }, new string[] { "-p:UseRazorBuildServer=false", "-p:UseSharedCompilation=false", "/nodeReuse:false" })]
[InlineData(new string[] { "--property", "MyProject.csproj", "-p:prop1=true" }, new string[] { "--property:MyProject.csproj", "--property:prop1=true" })]
[InlineData(new string[] { "--disable-build-servers" }, new string[] { "--property:UseRazorBuildServer=false", "--property:UseSharedCompilation=false", "/nodeReuse:false" })]
Copy link
Member Author

Choose a reason for hiding this comment

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

These expectations all remained the same, with the exception of the 'full' --property form.

public void MsbuildInvocationIsCorrect(string[] args, string[] expectedArgs)
{

string[] constantRestoreArgs = ["-nologo", "-verbosity:quiet"];
Copy link
Member Author

Choose a reason for hiding this comment

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

Since we're unifying args better now we get these constants that we have to be aware of.

{
launchSettingsModel = default;
if (!UseLaunchProfile)
{
return true;
}

var buildPathContainer = File.Exists(Project) ? Path.GetDirectoryName(Project) : Project;
string propsDirectory;
var launchSettingsPath = TryFindLaunchSettings(ProjectFileFullPath);
Copy link
Member Author

Choose a reason for hiding this comment

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

Pulled out discovery of launch settings for readability as well. The idea is that we try to find the settings, and then try to find a specific profile inside the settings for later application,

LaunchProfile = launchProfile;
NoLaunchProfile = noLaunchProfile;
Args = args;
RestoreArgs = GetRestoreArguments(restoreArgs);
Copy link
Member Author

Choose a reason for hiding this comment

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

big change here: we precompute the RestoreArgs because we need them in more than one location now. Once for the implicit restore/build, if any, and once for the project evaluation to read the Run arguments. This is a behavior change/bugfix from previous SDKs, because there older SDKs wouldn't apply any properties so evaluation may differ.

{ Constants.MSBuildExtensionsPath, AppContext.BaseDirectory }
};
// TODO for MSBuild usage here: need to sync loggers (primarily binlog) used with this evaluation
var project = EvaluateProject(ProjectFileFullPath, RestoreArgs);
Copy link
Member Author

Choose a reason for hiding this comment

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

Extracted prior MSBuild evaluation to a method for legibility, then made each of the following 'transformation' steps methods as well so it all flows.

Comment on lines +245 to +252
var userPassedProperties = DeriveUserPassedProperties(restoreArgs);
if (userPassedProperties is not null)
{
foreach (var (key, values) in userPassedProperties)
{
globalProperties[key] = string.Join(";", values);
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Here's that major difference again - we now detect and forward any global properties to the run evaluation. Future work here should probably also pull the loggers (binlog, terminal logger, console logger) and any configuration to apply to the actual target execution as well, so that we can have a consistent experience between build/restore and run argument evaluation.

}
else
{
throw new GracefulException("boom");
Copy link
Member Author

Choose a reason for hiding this comment

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

nit: need a localized message here. We also need a way to surface errors from the build here - we need to hook up some kind of logger, and that logger should probably be TL, but we can't easily do this from code. cc @rainersigwald for this gap - maybe we need to make TL a public API?

Copy link
Member

Choose a reason for hiding this comment

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

You would need to access the current logger, not hook up a new one, right? Since it already built? So would we need to use a LoggingService or a ProjectCollection to retain access to it?

If you have a list of the ILoggers MSBuild used, I don't think MSBuild needs to know the ILogger is a TL to log a message, but I also don't know how we'd log it from there.

Copy link
Member Author

Choose a reason for hiding this comment

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

@Forgind part of the problem is that we're in a hybrid mode of using MSBuild here:

  • the restore/build commands that run inherently relies on use MSBuild like a CLI app, so they express opinions about logging in terms of the CLI arguments passed to MSBuild, not anything more structured
  • this evaluation/target execution for run uses MSBuild via the API, so if we want to keep things unified in terms of experience we should try to recreate similar logger configurations, but because we've delegated this to MSBuild via CLI arguments it's hard to get that level of consistency

Copy link
Member Author

Choose a reason for hiding this comment

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

Rainer blessed the temporary reflection I've used. Short term will will IVT MSBuild so that the CLI can access the TL types, but medium/long term MSbuild should refactor argument parsing to be something that callers can programmatically invoke and get structured outputs for.

@baronfel baronfel force-pushed the extend-run-with-target branch from 5f097a2 to a01b1a6 Compare August 12, 2024 20:07
@baronfel baronfel requested review from tmat, arunchndr and a team as code owners August 21, 2024 16:56
@baronfel
Copy link
Member Author

Ok, tests should be green now - in both cases the tests were parsing raw stdout but terminal logger was leaving ANSI text on those streams that the expectations couldn't handle. I've scrubbed it now, but there's a gap here where we need a way to script all ANSI text from a console stream. There are libraries for this but nothing immediately usable.

Copy link
Member

@dsplaisted dsplaisted left a comment

Choose a reason for hiding this comment

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

I didn't look at everything in depth, but generally this looks good to me.

One thing that is different is that normally when you run a target to get information, the target should return the values with that information. Here the target doesn't return anything and you are just getting the updated property values. I'm not sure if there's a problem with using a different model.

@baronfel
Copy link
Member Author

@dsplaisted said:

One thing that is different is that normally when you run a target to get information, the target should return the values with that information. Here the target doesn't return anything and you are just getting the updated property values. I'm not sure if there's a problem with using a different model.

I talked with @tmeschter a bit about this yesterday and did agreed that it would require some work for the project system to use this model - everything they have expects a Returns and works with the returned Items. However, that presents some concerns that I'll detail below, and I think they are concerns that we can deal without outside the SDK itself.

The concerns are twofold:

  • the requirement to return Items means complications transforming the command, arguments, and working directory into not just item groups, but also tagging each returned Item with what kind of item it is. This is cumbersome to deal with vs just doing Property reads from a ProjectInstance
  • @tmeschter pointed out that there is a pattern of using design-time targets to reformat data in ways that is helpful/specific to the project system, and it would be possible to let the design-time targets handle doing that data transformation instead of the SDK - the core point was that the targets become contractual surface area that must be maintained/versioned so keeping that surface area as small as possible helps with future agility.

@baronfel baronfel merged commit 1aec5c6 into dotnet:main Aug 23, 2024
37 checks passed
@baronfel baronfel deleted the extend-run-with-target branch August 23, 2024 20:40
@baronfel
Copy link
Member Author

/backport to release/9.0.1xx

Copy link
Contributor

Started backporting to release/9.0.1xx: https://github.com/dotnet/sdk/actions/runs/10532093519

jonathanpeppers added a commit to dotnet/android that referenced this pull request Oct 30, 2024
…target

Context: dotnet/sdk#42155
Context: dotnet/sdk#42240
Fixes: dotnet/sdk#31253

The .NET SDK has introduced a new `ComputeRunArguments` MSBuild target
that allows you to set `$(RunCommand)` and `$(RunArguments)` in a more
dynamic way.

So, on Android:

* `ComputeRunArguments` depends on `Install`, so the app is deployed,
  the `<FastDeploy/>` MSBuild target runs, etc.

* `$(RunCommand)` is a path to `adb`

* `$(RunArguments)` is an `shell am start` command to launch the main
  activity.

The new implementation also allows us to use the `-p` parameter with
`dotnet run`, such as:

    dotnet run -bl -p:AdbTarget=-d

This will pass `-d` to `adb`, which allows you to select an attached
device if an emulator is running.

Previously, we had no way to pass `-p` arguments to `dotnet run`.
jonpryor pushed a commit to dotnet/android that referenced this pull request Dec 2, 2024
Context: dotnet/sdk#31253
Context: dotnet/sdk#42155
Context: dotnet/sdk#42240

The .NET SDK has introduced a new `ComputeRunArguments` MSBuild
target that allows you to set `$(RunCommand)` and `$(RunArguments)`
in a more dynamic way.

So, on Android:

  * `ComputeRunArguments` depends on `Install`, so the app is
    deployed, the `<FastDeploy/>` MSBuild target runs, etc.

  * `$(RunCommand)` is a path to `adb`

  * `$(RunArguments)` is an `shell am start` command to launch the main
    activity.

The new implementation also allows us to use the `-p` parameter with
`dotnet run`, such as:

	dotnet run -bl -p:AdbTarget=-d

This will pass `-d` to `adb`, which allows you to select an attached
device if an emulator is running.

Previously, we had no way to pass `-p` arguments to `dotnet run`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Extend the dotnet run command discovery protocol to be settable by a Target instead of just Evaluation
4 participants