Allow an Override Flag in UsingTasks#6783
Conversation
|
Thinking out loud, is #5541 more about when a task has |
I think so; the question is "given an old poorly-annotated x86 task + target buried in a NuGet package or old SDK or something, is there something a user project can do to fix the build?" With other things (like targets or properties or items) you can override behavior if you can get the ordering right. But here you can't.
I think so? Architecture is the only one we care about right now, maybe someday we'll want core/framework. |
|
Current thought for this PR is to have some Dictionary containing <Name of task that has an Architecture-specific usingtask, The registration record for that task>. And I'll short-circuit the task lookup method when the task we're looking for happens to have an architecture-specific version. |
|
NTS: I didn't consider there potentially being multiple usingtasks that define Architecture but I haven't gotten to the selection process for preferred usingtasks yet. GenerateResource so far has multiple usingtasks. |
From standup today, if multiple UsingTasks define |
|
NTS: But what about different tasks with the same name? Might need to check the dll its coming from as well. |
|
Because this will be modifying behavior of |
|
NTS: Need to modify tests in such a way that account for any usingtask with Architecture defined is prioritized. The problem is many tests check for Architecture/Runtime when doing their tests. Might be able to get away without the bool "hack". |
|
Notes after chatting with @rainersigwald Keep in mind that we'll want to generalize this for The arch/runtime should match the current arch/runtime. Final thoughts we came to: A usingtask should specify some We were looking at a project with the following usingtasks:
<UsingTask TaskName="Foo" AssemblyFile="$(Outdir)task.dll" Architecture="x86" />
<UsingTask TaskName="Foo" AssemblyFile="$(Outdir)task.dll"/>
<UsingTask TaskName="Foo" AssemblyFile="$(Outdir)task234.dll"/>
<!-- Problem: If Foo is called, the second declaration here will be
found on "Exact" match before fuzzy search happens. -->
<Target Name="Foo" AfterTargets="Build">
<Foo/>
</Target>
The first UsingTask will be stored in a dictionary of <(TaskName, TaskIdentity), List>. High level ordering of relevant functions when finding a task with a given name:
We're thinking the bulk of the change should be made in Options if we modify GetRelevantRegistrations:
Options if we modify RegisterTask:
|
There was a problem hiding this comment.
Note: I moved this check down 1 layer from GetRegisteredTask to GetTaskRegistrationRecord since the former calls the latter no matter what.
Unit tests also call directly into the latter.
There was a problem hiding this comment.
The proposed design makes sense to me, a few questions I had are:
- would a user ever want to know about this decision being made? if so, should there be a reason why the decision was made? e.g a
OverrideReasonattribute where inline doc could be added, so we could add aMS1000000: Task 'TaskName' from assembly 'AssemblyName' was chosen instead of 'other task options' because of 'OverrideReason'informational-level warning. - the new field should be added to the XSD with brief docs
|
Informational level message you mean? I think "Override" is clear enough that we shouldn't give a warning. |
|
Yeah, an informational level message is the correct term. |
Forgind
left a comment
There was a problem hiding this comment.
Looks roughly good, though I'm a little worried that you changed TaskRegistry_Tests so much.
There was a problem hiding this comment.
Why this slew of test changes? It makes it look like there's a serious breaking change in here. There shouldn't need to be any test changes, right?
There was a problem hiding this comment.
Ah these are holdovers from the previous solution. Will need to revert the commit that changed all these tests.
There was a problem hiding this comment.
Why this change? You can inline it instead if you want with ...out bool retrievedFromCache);
There was a problem hiding this comment.
It seems like you should be able to override an override.
| if (overrideTask && !overriddenTasks.ContainsKey(taskName)) | |
| if (overrideTask) |
Maybe a warning?
There was a problem hiding this comment.
That's debatable, depending on where we want to go with this. @baronfel may lean one way or the other?
Either way, I agree a warning should be logged.
There was a problem hiding this comment.
When would this happen? Some complex Condition evaluation that a user thought was exhaustive but didn't quite catch one particular corner case? It's my understanding that most <UsingTask>s are done in a block, with conditions and such to make sure that the best one is chosen. That would mean multiple Overrides applying would be unexpected.
A message sounds reasonable when registering an overridden task. Related to another comment in this thread, should we log a warning when multiple usingtasks are marked as |
I think yes, because at that point we had to make a judgement call (first seen, last seen, random based on entropy seed generated from a bee's wings) as to which Task's configuration was actually used. I think anytime we have to make a judgement call like that, the user should be told about it. |
|
I think the current test failure has to do with the build context when calling this task directly? I haven't seen an issue like this before. Will try to check for the logged warning case by building a full project that has multiple usingtasks in it, instead of calling the method directly. |
Forgind
left a comment
There was a problem hiding this comment.
Tests look much better, thanks
rainersigwald
left a comment
There was a problem hiding this comment.
Can you also prepare a draft of the docs changes that will describe this? I think that'll help clarify the behavior design.
There was a problem hiding this comment.
The cool kids are doing it this way now (I don't actually care which you do):
| RegisteredTaskRecord newRecord = new RegisteredTaskRecord(taskName, assemblyLoadInfo, taskFactory, taskFactoryParameters, inlineTaskRecord); | |
| RegisteredTaskRecord newRecord = new(taskName, assemblyLoadInfo, taskFactory, taskFactoryParameters, inlineTaskRecord); |
There was a problem hiding this comment.
You're keying off of a string name--how does that work with the partial-class-name resolution that normal task lookup does? Does this require an exact match to the invocation?
There was a problem hiding this comment.
That's a wrinkle in this solution, at the moment it requires an exact match at the calling site. Another aspect to consider, how should this play with the overridetasks file?
There was a problem hiding this comment.
How hard would it be to do "if the task comes from .overridetasks and there's something elsewhere with an override, error"?
There was a problem hiding this comment.
Found an area that documentation will need to be updated (if we change it): https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-tasks?view=vs-2022#overridden-tasks
Tasks in these files override any other tasks with the same names, including tasks in the project file.
Looks like precedence will be:
- Project UsingTask with Override attribute
- Anything in .overridetasks (or .tasks?)
- Project files
How hard would it be to do "if the task comes from .overridetasks and there's something elsewhere with an override, error"?
If we tried to go that route, one issue would be the lack of state in this class. When parsing a .tasks or .overridetasks file (see RegisterOverrideTasks), it calls LoadAndRegisterFromTasksFile which calls the static RegisterTasksFromUsingTaskElement. I guess we could try and determine whether we're in a .tasks or .overridetasks based on the IFileSystem that gets passed? Doesn't look like we can get the current file through it though.
There was a problem hiding this comment.
Why a warning and not an error?
There was a problem hiding this comment.
The conversation has mostly been about whether or not to log a warning, but could also be applied for logging an error.
IMO it's not worth failing the whole build over. My thinking is the resulting build could still be valid if the user sees this warning.
could still be valid
Or is that precisely why it's worth failing the whole build over?
There was a problem hiding this comment.
My thinking is: we can always back it down to a warning if we discover a good reason to. But if we start as a warning we can't go to an error if we discover a good reason that way. So I prefer errors for new scenarios.
…not looking for an exact match, taskregistry will now return the first-defined usingtask
f918fa2 to
91cc04a
Compare
91cc04a to
fbcbc98
Compare
There was a problem hiding this comment.
How hard would it be to do "if the task comes from .overridetasks and there's something elsewhere with an override, error"?
| string unqualifiedTaskName = taskName; | ||
|
|
||
| if (unqualifiedTaskName.Contains('.')) | ||
| { | ||
| unqualifiedTaskName = taskName.Split('.').Last(); | ||
| } |
There was a problem hiding this comment.
| string unqualifiedTaskName = taskName; | |
| if (unqualifiedTaskName.Contains('.')) | |
| { | |
| unqualifiedTaskName = taskName.Split('.').Last(); | |
| } | |
| string unqualifiedTaskName = taskName.Split('.').Last(); |
| // check every registration that exists in the list. | ||
| foreach (RegisteredTaskRecord rec in recs) | ||
| { | ||
| // Does the same registration already exist? (same exact name) |
There was a problem hiding this comment.
Please only include useful comments. This and the past two comments added nothing to the line of code immediately below them. Comments are easily forgotten when changing code.
| // Create a dictionary containing the unqualified name (for quick lookups when the task is called). | ||
| // Place the new record using the potentially fully qualified name to account for partial matches. | ||
| overriddenTasks.Add(unqualifiedTaskName, new List<RegisteredTaskRecord>()); | ||
| overriddenTasks[unqualifiedTaskName].Add(newRecord); |
There was a problem hiding this comment.
| // Create a dictionary containing the unqualified name (for quick lookups when the task is called). | |
| // Place the new record using the potentially fully qualified name to account for partial matches. | |
| overriddenTasks.Add(unqualifiedTaskName, new List<RegisteredTaskRecord>()); | |
| overriddenTasks[unqualifiedTaskName].Add(newRecord); | |
| // New record's name may be fully qualified. Use it anyway to account for partial matches. | |
| List<RegisteredTaskRecord> unqualifiedTaskNameMatches = new(); | |
| unqualifiedTaskNameMatches.Add(newRecord); | |
| overriddenTasks.Add(unqualifiedTaskName, unqualifiedTaskNameMatches); |
?
Saves a dictionary access, too.
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
Is this right? Shouldn't it be
? |
rainersigwald
left a comment
There was a problem hiding this comment.
Looking great, just a couple of things to look at!
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
@benvillalobos, resolved a merge conflict in case you want to look at it, but it was pretty straightforward. |
Fixes #5541
Context
When we define multiple usingtasks like so:
and call the task without specifying
Architecture, the architecture-specificUsingTaskshould be returned when MSBuild searches for the task. Currently, MSBuild prefers the most "exact match".This change allows the following:
Users can specify
Override="true"in aUsingTaskelement. This will modify the TaskRegistry to always return that task registration when attempting to call that task.Changes Made
The TaskRegistry will return the first usingtask marked with
Override="true"when searching for a task.Testing
A test showing that:
Notes
Don't review commit by commit.
Expected behaviors
If multiple overrides override a task with the exact same name (fully qualified), MSB4275 will be logged
Prioritization order of usingtasks: