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

Provide nice error when an environment variable is not expanded on the command line, including the full command line for all switch errors Fixes #7210 #7213

Merged
merged 28 commits into from
Feb 18, 2022

Conversation

Forgind
Copy link
Member

@Forgind Forgind commented Jan 4, 2022

Fixes #7210

Context

When you specify an environment variable on the command line or via Directory.Build.rsp, and that environment variable is not defined, it puts the literal in instead. MSBuild misinterprets that as a duplicate project file and fails. This works around that.

Changes Made

Ignore environment variable "projects" if another project is found. (Otherwise doesn't check.)

Testing

Tried with a simplified repro. Fixed the problem.

Notes

Maybe consider giving a better error message instead of ignoring the environment variable?

@Forgind
Copy link
Member Author

Forgind commented Jan 4, 2022

@CarlHeinrichs, what do you think?

Copy link
Member

@CarlHeinrichs CarlHeinrichs left a comment

Choose a reason for hiding this comment

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

LGTM, but I am not that familiar with this code. I assume you are going to author some tests before completing the PR?

@ladipro
Copy link
Member

ladipro commented Jan 4, 2022

I am generally opposed to addressing the linked issue by overriding the default (and OS specific) command line expansion rules. If the shell passes %nonexistentvar% to all programs by design, there's IMO no good reason to have MSBuild try to detect and ignore it. It's not systematic and not worth the added complexity. I'm afraid that detecting and expanding environment variables in our code is asking for trouble. IsEnvironmentVariable, for example, should be platform specific, e.g. $somevar doesn't work on Windows.

@Forgind
Copy link
Member Author

Forgind commented Jan 4, 2022

I am generally opposed to addressing the linked issue by overriding the default (and OS specific) command line expansion rules. If the shell passes %nonexistentvar% to all programs by design, there's IMO no good reason to have MSBuild try to detect and ignore it. It's not systematic and not worth the added complexity. I'm afraid that detecting and expanding environment variables in our code is asking for trouble. IsEnvironmentVariable, for example, should be platform specific, e.g. $somevar doesn't work on Windows.

I should definitely make IsEnvironmentVariable platform-specific. I can't think of a reason you'd want an unexpanded environment variable as part of your command line, though. I'm not exactly expanding it but ignoring it if and only if there's an alternate "project" candidate. The other option I was considering is detecting something like that and giving a custom error about "why do you have an unexpanded environment variable in your command line?" Since current behavior is throwing a confusing error, that would be just making the error clearer rather than changing behavior.

I could also put the current change under a changewave if you'd like or have it additionally throw a warning such that it doesn't silently ignore an environment variable. Open to any of these.

@ladipro
Copy link
Member

ladipro commented Jan 4, 2022

My point is that many tools could use a similar logic (hey, this argument looks like an unexpanded env var and the command line otherwise makes sense so let's ignore it) but they don't. We shouldn't do it just because in our case it happens to be easy.

I would vote for adding a good command line logging instead. For example, I don't think you will find the full command line (as combined from response files) in the binlog today. When MSBuild is complaining about a command line argument or switch, it should always print the full command line together with the error so the user can tell what went wrong. I believe that it would have been enough for the linked issue to be a non-issue.

@Forgind
Copy link
Member Author

Forgind commented Jan 5, 2022

I like the idea of adding the full command line to switch-related error messages, so I added that. I'm still tempted to, instead of silently ignoring unexpanded env vars, make a new error that says "hey, your env var didn't expand, see?" instead. How would you feel about that?

(And also make the env var style os-specific.)

Copy link
Member

@rainersigwald rainersigwald left a comment

Choose a reason for hiding this comment

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

And also make the env var style os-specific.

Environment variables are not OS-specific; they are shell-specific. In addition, MSBuild's expansion of environment variables from response files uses Environment.ExpandEnvironmentVariables(string), which only considers %-delimited variables.

I do not think we should silently ignore unexpanded variables. I agree that we should log the full, expanded command line when giving a command-line-parsing error. I would not object to having a suberror if there is an unexpanded %variable_reference% to give a hint.

It otherwise only caught an environment variable if it was the first "project" switch but not if it was the duplicate.
@Forgind Forgind changed the title Ignore env variable switches Fixes #7210 Provide nice error when an environment variable is not expanded on the command line, including the full command line for all switch errors Fixes #7210 Jan 5, 2022
Copy link
Member

@ladipro ladipro left a comment

Choose a reason for hiding this comment

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

I took a very quick look and the command line now printed as part of error messages doesn't seem to include parts that came from response files. Example:

If Directory.Build.rsp contains %nonexistent% and MSBuild is invoked like so:
msbuild myproject.csproj

It prints:
MSBUILD : error MSB1060: Undefined environment variable passed in as switch. Full path: '...\msbuild.exe myproject.csproj'

I think that printing switches as combined from all sources (optionally also with provenance annotation, i.e. which switches came from where) would be a powerful diagnostic feature.

[Fact]
public void ProcessEnvironmentVariableSwitch()
{
string savedEnvironmentVariable = Environment.GetEnvironmentVariable("ENVIRONMENTVARIABLE");
Copy link
Member

Choose a reason for hiding this comment

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

nit: Can this use the TestEnvironment class? Similar to this:

using (var env = TestEnvironment.Create(_output))
{
env.SetEnvironmentVariable("MSBUILDLOGIMPORTS", "1");

@Forgind
Copy link
Member Author

Forgind commented Jan 6, 2022

@ladipro, I consider my last commit the "minimum" version, but there are a couple caveats. Most notably, we have to parse the command line not include the .rsp before parsing the .rsp because there can be switches that tell you to parse a particular .rsp, and there can be a switch to say not to parse the automatically found one. That means that if there's an error earlier in the command line parsing process, there's no way it will know about switches from .rsps, so the command line will necessarily be incomplete.

That aside, this should give you the full command line (not including extra switches from .rsps) plus any switches from .rsps we'd parsed. It does not specify where switches come from, but I can do something a little hacky to make that pretty clean. Currently, I add .rsp switches via:
commandLine += ' ' + string.Join(" ", argsFromResponseFile);

What would you think of making that:
commandLine += $"' '{responseFile}': '{string.Join(" ", argsFromResponseFile)}";

The advantage of something like that is that I can specify the exact path of every response file, and it's quick and easy. That doesn't include any words because they wouldn't be localized. Alternatively, I can add another string to localize like:
' Switches added from response file '{0}': '{1} and fill in 0 and 1 appropriately on this line.

@ladipro
Copy link
Member

ladipro commented Jan 7, 2022

@Forgind thank you!

What would you think of making that:
commandLine += $"' '{responseFile}': '{string.Join(" ", argsFromResponseFile)}";

This would be awesome. I don't have a strong opinion on the format of the output. Presumably the above would be clear enough.

Code-wise, I'm wondering if passing the command line string to CommandLineSwitches at construction time instead of having it as an argument of all Set*Error methods wouldn't be cleaner. With the change to print responseFile: argsFromResponseFile there should be no need to pass it by ref and mutate it, if I'm understanding the logic correctly.

@Forgind
Copy link
Member Author

Forgind commented Jan 7, 2022

@Forgind thank you!

What would you think of making that:
commandLine += $"' '{responseFile}': '{string.Join(" ", argsFromResponseFile)}";

This would be awesome. I don't have a strong opinion on the format of the output. Presumably the above would be clear enough.

Great! Will do.

Code-wise, I'm wondering if passing the command line string to CommandLineSwitches at construction time instead of having it as an argument of all Set*Error methods wouldn't be cleaner. With the change to print responseFile: argsFromResponseFile there should be no need to pass it by ref and mutate it, if I'm understanding the logic correctly.

This is tricky, and it might be easier to explain in PR reviews, but "the" command line mutates every time we encounter an .rsp. It's important to have all the switches we are currently aware of because if we've found an error, it must be among those, but at no point until we're finished are we confident that we have all the switches in our command line. If we were to try to set it once upon creating the CommandLineSwitches object, it would have the command line input by the user, but as you pointed out, that misses extra switches we find later. Since the list of known switches can change after processing any individual switch, we won't really have a chance to refresh CommandLineSwitches unless we either make a lot of them or accept that it isn't an intrinsic part of the object.

@Forgind
Copy link
Member Author

Forgind commented Jan 10, 2022

Change format to:
Expanded command line: 'msbuild thing /switch'
\Directory.Build.rsp: /switch2 /switch3
\Directory.Build2.rsp: /switch4 /switch5

(Or full command line)

@Forgind
Copy link
Member Author

Forgind commented Jan 10, 2022

Now looks like:

MSBUILD : error MSB1060: Undefined environment variable passed in as switch.
    Full command line: artifacts\bin\bootstrap\net472\MSBuild\Current\Bin\MSBuild.exe  C:\Users\forgind\Desktop\Archives\Bug-specific\myTemp\myTemp.csproj
C:\Users\forgind\Desktop\Archives\Bug-specific\myTemp\Directory.Build.rsp: %ENVIRONMENTVARIABLE%
Switch: %ENVIRONMENTVARIABLE%

For switch syntax, type "MSBuild -help"

#if FEATURE_GET_COMMANDLINE
commandLine
#else
string.Join(" ", commandLine)
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
string.Join(" ", commandLine)
string.Join(' ', commandLine)

Copy link
Member Author

Choose a reason for hiding this comment

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

There isn't a string.Join overload for char.

Copy link
Member

Choose a reason for hiding this comment

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

There isn't a string.Join overload for char.

. . . until .NET Core 2.0.

Copy link
Member

Choose a reason for hiding this comment

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

This code is under an ifdef and does not run on Framework. Just a nit and I wrote it only because you're adding a call to the char-taking overload in this PR (line ~1653).

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 thought I'd tested it at that location, but I was wrong. Thanks!

@@ -1901,25 +1911,27 @@ private static void GatherResponseFileSwitch(string unquotedCommandLineArg, Comm
}
}

GatherCommandLineSwitches(argsFromResponseFile, commandLineSwitches);
commandLine += $"\n{responseFile}: {string.Join(" ", argsFromResponseFile)}";
Copy link
Member

Choose a reason for hiding this comment

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

Ah, now I see why the command line string has to be passed by ref. I'm still not sure if this is the best approach. Ideally the command line would be kept in a more structured format (e.g. a dictionary mapping "response file" to "args from the response file") and maybe in a static field so it doesn't have to be passed around everywhere.

Aside from nicer and more allocation friendly code, It would make it possible to have a richer format of the output. I'm having second thoughts about the bare <response file>: <command line>, whether it is clear/expressive enough.

@@ -83,7 +83,8 @@
</comment>
</data>
<data name="CannotAutoDisableAutoResponseFile" UESanitized="false" Visibility="Public">
<value>MSBUILD : error MSB1027: The -noAutoResponse switch cannot be specified in the MSBuild.rsp auto-response file, nor in any response file that is referenced by the auto-response file.</value>
<value>MSBUILD : error MSB1027: The -noAutoResponse switch cannot be specified in the MSBuild.rsp auto-response file, nor in any response file that is referenced by the auto-response file.
Full command line: {0}</value>
Copy link
Member

Choose a reason for hiding this comment

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

I think it's unfortunate that so many resource strings are changed and will need attention from the localization team. Do you think the command line could be printed using only one shared resource instead?

Couldn't find this for some reason. Found it now
I don't know why it suddenly realized that Execute sets MSBuildLoadMicrosoftTargetsReadOnly and started complaining about it, but it did. This effectively unsets it after relevant tests.
@@ -476,7 +476,8 @@ public void TargetsSwitchIdentificationTests(string @switch)
public void TargetsSwitchParameter()
{
CommandLineSwitches switches = new CommandLineSwitches();
MSBuildApp.GatherCommandLineSwitches(new List<string>() { "/targets:targets.txt" }, switches);
string command = string.Empty;
MSBuildApp.GatherCommandLineSwitches(new List<string>() { "/targets:targets.txt" }, switches, command);
Copy link
Member

Choose a reason for hiding this comment

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

Why not provide a default argument so you don't have to pass an empty string to these many callers?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point 🙂

{
string[] msbuildTempXsdFilenames = Array.Empty<string>();
string projectFilename = null;
string oldValueForMSBuildOldOM = null;
string oldValueForMSBuildLoadMicrosoftTargetsReadOnly = Environment.GetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly");
Copy link
Member

Choose a reason for hiding this comment

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

Why is this needed?

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 don't know why it suddenly started noticing this, but there's an invariant check that we didn't change any environment variables, and this one gets set automatically early in Execute. This just ensures it isn't set before it checks that invariant.

src/MSBuild/CommandLineSwitches.cs Outdated Show resolved Hide resolved
src/MSBuild.UnitTests/XMake_Tests.cs Outdated Show resolved Hide resolved
src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs Outdated Show resolved Hide resolved
src/MSBuild/CommandLineSwitches.cs Outdated Show resolved Hide resolved
@Forgind Forgind added the merge-when-branch-open PRs that are approved, except that there is a problem that means we are not merging stuff right now. label Feb 14, 2022
@Forgind Forgind merged commit ceefec5 into dotnet:main Feb 18, 2022
@Forgind Forgind deleted the ignore-environment-variable-switches branch February 18, 2022 21:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merge-when-branch-open PRs that are approved, except that there is a problem that means we are not merging stuff right now.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

MSB1008: Only one project can be specified when Directory.Build.rsp contains undefined environment variable
4 participants