-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Conversation
@CarlHeinrichs, what do you think? |
There was a problem hiding this 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?
I am generally opposed to addressing the linked issue by overriding the default (and OS specific) command line expansion rules. If the shell passes |
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. |
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. |
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.) |
There was a problem hiding this 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.
There was a problem hiding this 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"); |
There was a problem hiding this comment.
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:
msbuild/src/Build.OM.UnitTests/Definition/Project_Tests.cs
Lines 4104 to 4106 in 797fd82
using (var env = TestEnvironment.Create(_output)) | |
{ | |
env.SetEnvironmentVariable("MSBUILDLOGIMPORTS", "1"); |
@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: What would you think of making that: 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: |
@Forgind thank you!
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 |
Great! Will do.
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. |
Change format to: (Or full command line) |
Now looks like:
|
src/MSBuild/XMake.cs
Outdated
#if FEATURE_GET_COMMANDLINE | ||
commandLine | ||
#else | ||
string.Join(" ", commandLine) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
string.Join(" ", commandLine) | |
string.Join(' ', commandLine) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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!
src/MSBuild/XMake.cs
Outdated
@@ -1901,25 +1911,27 @@ private static void GatherResponseFileSwitch(string unquotedCommandLineArg, Comm | |||
} | |||
} | |||
|
|||
GatherCommandLineSwitches(argsFromResponseFile, commandLineSwitches); | |||
commandLine += $"\n{responseFile}: {string.Join(" ", argsFromResponseFile)}"; |
There was a problem hiding this comment.
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.
src/MSBuild/Resources/Strings.resx
Outdated
@@ -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> |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed?
There was a problem hiding this comment.
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.
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?