Skip to content

Allow an Override Flag in UsingTasks#6783

Merged
rokonec merged 43 commits into
dotnet:mainfrom
benvillalobos:taskregistry-firstdefined
Feb 23, 2022
Merged

Allow an Override Flag in UsingTasks#6783
rokonec merged 43 commits into
dotnet:mainfrom
benvillalobos:taskregistry-firstdefined

Conversation

@benvillalobos
Copy link
Copy Markdown
Member

@benvillalobos benvillalobos commented Aug 23, 2021

Fixes #5541

Context

When we define multiple usingtasks like so:

  <UsingTask TaskName="VSCTCompiler" AssemblyFile="$(VsSDKAssemblyFile)" />
  <UsingTask TaskName="VSCTCompiler" AssemblyFile="$(VsSDKAssemblyFile)" Architecture="x86" />

and call the task without specifying Architecture, the architecture-specific UsingTask should be returned when MSBuild searches for the task. Currently, MSBuild prefers the most "exact match".

This change allows the following:

  <UsingTask TaskName="VSCTCompiler" AssemblyFile="$(VsSDKAssemblyFile)" />
  <UsingTask TaskName="VSCTCompiler" AssemblyFile="$(VsSDKAssemblyFile)" Override="true" Architecture="x86" />

Users can specify Override="true" in a UsingTask element. 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:

  • Multiple usingtasks overriding the same task will log MSB4275
  • If a usingtask overrides, it is preferred over other usingtasks.

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:

  1. Project-level usingtask override
  2. .tasks/.overridetasks file
  3. UsingTask defined elsewhere

@benvillalobos
Copy link
Copy Markdown
Member Author

Thinking out loud, is #5541 more about when a task has Architecture defined? Maybe we can add a check that prioritizes the usingtask that has Architecture (and any other "high priority" attributes) defined when trying to find the task from the registry?

@rainersigwald
Copy link
Copy Markdown
Member

Thinking out loud, is #5541 more about when a task has Architecture defined?

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.

Maybe we can add a check that prioritizes the usingtask that has Architecture (and any other "high priority" attributes) defined when trying to find the task from the registry?

I think so? Architecture is the only one we care about right now, maybe someday we'll want core/framework.

@benvillalobos
Copy link
Copy Markdown
Member Author

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.

@benvillalobos
Copy link
Copy Markdown
Member Author

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.

@benvillalobos
Copy link
Copy Markdown
Member Author

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 Architecture, first one wins.

@benvillalobos benvillalobos changed the title Return First-Defined UsingTask Return First-Defined UsingTask With Architecture Metadata Sep 1, 2021
@benvillalobos
Copy link
Copy Markdown
Member Author

NTS: But what about different tasks with the same name? Might need to check the dll its coming from as well.

@benvillalobos
Copy link
Copy Markdown
Member Author

Because this will be modifying behavior of UsingTasks, we should update https://docs.microsoft.com/en-us/visualstudio/msbuild/usingtask-element-msbuild?view=vs-2019 when this merges.

@benvillalobos
Copy link
Copy Markdown
Member Author

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".

@benvillalobos
Copy link
Copy Markdown
Member Author

benvillalobos commented Nov 2, 2021

Notes after chatting with @rainersigwald

Keep in mind that we'll want to generalize this for Runtime as a parameter as well.

The arch/runtime should match the current arch/runtime.

Final thoughts we came to: A usingtask should specify some Override="true" to opt into this. If that's the case, it should override ANY call to a task with that name. Even if there are two tasks with the same name from two separate assemblies. At that point a user can call Namespace.Class.Foo to call the specific task they want.

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:

  • FindTaskInRegistry <- First attempts exact match then fuzzy
    • GetRegisteredTask
      • GetTaskRegistrationRecord
        • // If fuzzy match
          • GetRelevantRegistrations <- Returns Dictionary<TaskIdentity, List>
            • for each registration
              • GetMatchingRegistration <- Finds the first partial match

We're thinking the bulk of the change should be made in GetRelevantRegistrations or RegisterTask.

Options if we modify GetRelevantRegistrations:

  • Sort usingtasks based on this Override flag, always return that one.
  • Store tasks marked as Override in a separate collection, short circuit this method if the task being searched for has an Override usingtask defined.

Options if we modify RegisterTask:

  • If some task Foo has Override specified, delete or modify existing entries of the Foo task to specifically use this usingtask.

Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Comment thread src/Build/Instance/TaskRegistry.cs Outdated
@benvillalobos benvillalobos marked this pull request as ready for review November 8, 2021 22:57
Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

@Forgind Forgind requested a review from baronfel November 15, 2021 16:36
Copy link
Copy Markdown
Member

@baronfel baronfel left a comment

Choose a reason for hiding this comment

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

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 OverrideReason attribute where inline doc could be added, so we could add a MS1000000: 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

@Forgind
Copy link
Copy Markdown
Contributor

Forgind commented Nov 15, 2021

Informational level message you mean? I think "Override" is clear enough that we shouldn't give a warning.

@baronfel
Copy link
Copy Markdown
Member

Yeah, an informational level message is the correct term.

Copy link
Copy Markdown
Contributor

@Forgind Forgind left a comment

Choose a reason for hiding this comment

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

Looks roughly good, though I'm a little worried that you changed TaskRegistry_Tests so much.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah these are holdovers from the previous solution. Will need to revert the commit that changed all these tests.

Comment thread src/Build.UnitTests/BackEnd/TaskRegistry_Tests.cs Outdated
Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why this change? You can inline it instead if you want with ...out bool retrievedFromCache);

Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems like you should be able to override an override.

Suggested change
if (overrideTask && !overriddenTasks.ContainsKey(taskName))
if (overrideTask)

Maybe a warning?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@benvillalobos benvillalobos changed the title Return First-Defined UsingTask With Architecture Metadata Allow an Override Flag in UsingTasks Nov 18, 2021
@benvillalobos
Copy link
Copy Markdown
Member Author

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 OverrideReason attribute where inline doc could be added, so we could add a MS1000000: Task 'TaskName' from assembly 'AssemblyName' was chosen instead of 'other task options' because of 'OverrideReason' informational-level warning.

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 Override=true (with the same task name)?

@baronfel
Copy link
Copy Markdown
Member

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 OverrideReason attribute where inline doc could be added, so we could add a MS1000000: Task 'TaskName' from assembly 'AssemblyName' was chosen instead of 'other task options' because of 'OverrideReason' informational-level warning.

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 Override=true (with the same task name)?

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.

@benvillalobos
Copy link
Copy Markdown
Member Author

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.

Copy link
Copy Markdown
Contributor

@Forgind Forgind left a comment

Choose a reason for hiding this comment

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

Tests look much better, thanks

Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Comment thread src/Build/Resources/Strings.resx Outdated
Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Copy link
Copy Markdown
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.

Can you also prepare a draft of the docs changes that will describe this? I think that'll help clarify the behavior design.

Comment thread ref/Microsoft.Build/net/Microsoft.Build.cs Outdated
Comment thread src/Build/Resources/Strings.resx Outdated
Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Comment thread src/Build/Resources/Strings.resx Outdated
Comment thread src/Build/Resources/Strings.resx Outdated
Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The cool kids are doing it this way now (I don't actually care which you do):

Suggested change
RegisteredTaskRecord newRecord = new RegisteredTaskRecord(taskName, assemblyLoadInfo, taskFactory, taskFactoryParameters, inlineTaskRecord);
RegisteredTaskRecord newRecord = new(taskName, assemblyLoadInfo, taskFactory, taskFactoryParameters, inlineTaskRecord);

Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How hard would it be to do "if the task comes from .overridetasks and there's something elsewhere with an override, error"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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:

  1. Project UsingTask with Override attribute
  2. Anything in .overridetasks (or .tasks?)
  3. 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.

Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why a warning and not an error?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

#6783 (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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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
@benvillalobos benvillalobos force-pushed the taskregistry-firstdefined branch from f918fa2 to 91cc04a Compare November 23, 2021 22:58
@benvillalobos benvillalobos force-pushed the taskregistry-firstdefined branch from 91cc04a to fbcbc98 Compare November 24, 2021 19:17
Comment thread ref/Microsoft.Build/net/Microsoft.Build.cs Outdated
Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How hard would it be to do "if the task comes from .overridetasks and there's something elsewhere with an override, error"?

Comment thread src/Build/Resources/Strings.resx Outdated
Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Comment on lines +702 to +707
string unqualifiedTaskName = taskName;

if (unqualifiedTaskName.Contains('.'))
{
unqualifiedTaskName = taskName.Split('.').Last();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
string unqualifiedTaskName = taskName;
if (unqualifiedTaskName.Contains('.'))
{
unqualifiedTaskName = taskName.Split('.').Last();
}
string unqualifiedTaskName = taskName.Split('.').Last();

Comment thread src/Build/Instance/TaskRegistry.cs Outdated
// check every registration that exists in the list.
foreach (RegisteredTaskRecord rec in recs)
{
// Does the same registration already exist? (same exact name)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Comment on lines +726 to +729
// 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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// 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.

@benvillalobos
Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@rainersigwald
Copy link
Copy Markdown
Member

Prioritization order of usingtasks:

  1. Project-level usingtask override
  2. .tasks/.overridetasks file
  3. UsingTask defined elsewhere

Is this right? Shouldn't it be

  1. Project-level UsingTask override
  2. .overridetasks file
  3. Project-level UsingTask
  4. .tasks file

?

Copy link
Copy Markdown
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.

Looking great, just a couple of things to look at!

Comment thread src/Build.UnitTests/BackEnd/TaskRegistry_Tests.cs
Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Comment thread src/Build/Instance/TaskRegistry.cs Outdated
Comment thread src/Build/Resources/Strings.resx
Comment thread src/Build/Resources/Strings.resx Outdated
@benvillalobos
Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@Forgind
Copy link
Copy Markdown
Contributor

Forgind commented Feb 16, 2022

@benvillalobos, resolved a merge conflict in case you want to look at it, but it was pretty straightforward.

@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 16, 2022
@rokonec rokonec merged commit 047227b into dotnet:main Feb 23, 2022
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.

UsingTask overrides do not apply if newly specifying Architecture/Runtime

5 participants