-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add extensibility points for source link #2960
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
Conversation
|
@rainersigwald Please review. Not sure why the tests are failing with They work on my box. Is log initialization different on Linux/OSX? |
|
@AndyGerlicher This PR adds the minimal amount of extensibility points to the common targets that are needed for linking PDBs and packages to the originating source (via SourceLink, commit sha in AssemblyInformationalAttribute, Repository Url in nuget packages, etc.) and for compilation with deterministic source paths. |
|
@tmat not sure if I'm just missing something at the moment, haven't tried it out. But why couldn't all this all come from a package reference? |
|
@AndyGerlicher Because NuGet targets, dotnet SDK and the compiler targets will have a dependency on these properties and targets. |
|
@AndyGerlicher I did reduce the set of targets and properties that go to common targets to minimum. I don't think there is anything more that can be reasonably removed without breaking layering or introducing hacks. |
| </ItemGroup> | ||
|
|
||
| <Error Text="SourceRoot paths are required to end with a slash or backslash: @(_InvalidSourceRoot, ', ')" | ||
| Condition="'@(_InvalidSourceRoot)' != ''" /> |
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 can't we just add them? That is what common targets normally does for this.
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.
Do we know which separator to add on Windows (\ or /)? The source root path needs to end with exactly the same separator that the full paths of source files that flow into the compiler use.
Is it possible that the compiler will see a source path with / in it on Windows?
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's some magic that changes backslash to slash for ToolTask invocation on Unix
Take a deep breath:
So I think appending \ will give the compiler \ on Windows and / on Unix.
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 is possible for the compiler to see / on Windows, but not \ on Unix.
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 can't/shouldn't the compiler understand that there's no difference between / and \ on Windows?
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.
Source paths in the PDB are matched to the patterns in SourceLink by simple prefix match when interpreted by the debugger.
I don't think requiring the path to end with slash is an issue in practice. SourceRoot items will usually be generated by another build task. Writing SourceRoots manually should be very rare.
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 think it's much of an issue. I asked because the append-trailing-slash-if-not-already-there idiom is pervasive in common targets. If there's a good reason to do it differently here, that's fine. Might be worth a comment.
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.
Will add a comment.
|
|
||
| <Target Name="SetSourceRevisionId"> | ||
| <PropertyGroup> | ||
| <SourceRevisionId>$(STANDARD_CI_SOURCE_REVISION_ID)</SourceRevisionId> |
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.
Shouldn't this allow the user to override
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, yes.
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.
Fixed. Using the same pattern as InitializePrivateRepositoryUrl
src/Tasks/Microsoft.Common.props
Outdated
| Build servers that comply with Standard CI specification set STANDARD_CI_REPOSITORY_URL and STANDARD_CI_SOURCE_REVISION_ID variables. | ||
| See https://github.com/dotnet/designs/blob/86f4ed0e39fc1b1ab5c4128990b17c4aead4420f/proposed/standard-ci-env-variables.md. | ||
| --> | ||
| <ContinuousIntegrationBuild Condition="'$(DesignTimeBuild)' != 'true' and '$(STANDARD_CI_REPOSITORY_URL)' != ''">true</ContinuousIntegrationBuild> |
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.
We should check if ContinuousIntegrationBuild is already set, no?
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.
Yes.
src/Tasks/MapSourceRoots.cs
Outdated
| public ITaskItem[] SourceRoots { get; set; } | ||
|
|
||
| [Output] | ||
| public ITaskItem[] MappedSourceRoots { get; set; } |
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.
private set for Output
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.
Fixed.
| <_InvalidSourceRoot Include="@(SourceRoot)" Condition="!HasTrailingSlash('%(SourceRoot.Identity)')" /> | ||
| </ItemGroup> | ||
|
|
||
| <Error Text="SourceRoot paths are required to end with a slash or backslash: @(_InvalidSourceRoot, ', ')" |
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.
Localization?
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.
How do I localize an error message in .targets file?
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.
In dotnet/sdk we have a custom task that we pass resource name and format arguments.
But it seems there is precedent for unlocalized errors in common targets:
I would think this is against the letter of the World Ready tenet. Given that we've already said that these are generally automatically provided by tooling and not typed in by users, the error would be rare, so it's probably OK in spirit.
@cdmihai Thoughts?
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.
Given that there's other error tasks in the file and I searched the visual studio drops for all occurrences of Microsoft.Common.CurrentVersion.targets but did not find anything, it's probably safe to say that, because it's always been not localized, it can remain not localized.
|
@rainersigwald All tests fixed. Thanks for help. Does the change look good now? |
|
@rainersigwald ping |
|
test Ubuntu16.04 Build for CoreCLR please |
rainersigwald
left a comment
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 seems like a straightforward addition of some properties, targets, and items. I still don't understand why this can't be done in a package. What information is required where? What properties and items must exist, where their absence cannot be interpreted as not opting in to the new behavior?
|
To be clear, I'm thinking of a package like
Someone could still override |
The properties and targets defined in this change are intended to be used at least in the following targets:
If the targets added by this PR were in a package than all of the above targets would need to depend on that package somehow, which is impossible as far as I can tell. |
I guess that's not strictly true. We could alternatively copy these targets into all of the above targets files and make a few copies of the |
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.
Should _InitializeSourceRevisionIdFromStandardCI go after $(InitializeSourceRevisionIdDependsOn)? That way other extensions can hook into this target, and they don't have to blindly overwrite the property?
As it is coded now, if an extension wants to write to <SourceRevisionId>, it can't check for empty, because _InitializeSourceRevisionIdFromStandardCI will set it first.
Same applies to InitializePrivateRepositoryUrl below.
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.
That depends on whether STANDARD_CI_SOURCE_REVISION_ID should be the default value that should be used if available or a fallback value that should be used if there is no extension setting SourceRevisionId, right?
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.
The extension could decide to
- add itself to
InitializeSourceRevisionIdDependsOn, or - chain itself before
InitializeSourceRevisionIdFromStandardCItarget to initialize to a different default value like so:
<Target Name="MyInitializeSourceRevisionIdFromStandardCI"
BeforeTargets="InitializeSourceRevisionIdFromStandardCI">
<PropertyGroup>
<SourceRevisionId Condition="'$(SourceRevisionId)' == ''">MyRevisionId</SourceRevisionId>
</PropertyGroup>
</Target>
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.
Think about if you were writing an extension to this target and you wanted to set <SourceRevisionId>. How would you do it?
Maybe instead of using a Target to set this property, you should just set the property in a normal <PropertyGroup>. Then the typical MSBuild ordering applies. An extension can set it either before OR after you. Here they only get 1 option - after.
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 have a working extension that does it: https://github.com/tmat/repository-info/blob/master/src/Microsoft.Build.Tasks.SourceControl/build/Microsoft.Build.Tasks.SourceControl.targets#L62-L67
It can't be set in normal <PropertyGroup> since a task has to be called to retrieve this 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 guess we could also have InitializeSourceRevisionIdDependsOn initialized to _InitializeSourceRevisionIdFromStandardCI and then let extensions to prepend or append their target name to this property.
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.
@rainersigwald Is there any guidelines for how to write these extension points?
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.
No, no formal guidelines that I'm aware of. I added some comments about the structure I'd expect, which I think is closer to what Eric expected too.
In general, I'd rather avoid creating new xDependsOn properties. They were required for MSBuild 2, but BeforeTargets/AfterTargets is a much better extensibility mechanism.
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.
OK. It would be nice to have these guidelines written down somewhere, so we don't need to spend time figuring this out every time.
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.
By the way, this
AfterTargetsdoesn't work for the consumption. If I need to use this value in another target I addInitializeSourceRevisionIdto my target'sDependsOnTargets.AfterTargetsofInitializeSourceRevisionIdwon't necessarily run.
Isn't true. It should be (targets should make a DAG), but it's not. The details of the traversal are https://docs.microsoft.com/en-us/visualstudio/msbuild/target-build-order#determining-the-target-build-order, and I don't see any way to change them to a DAG approach without breaking the whole world, so they can be relied upon.
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.
'$(ContinuousIntegrationBuild)' != '' seems strange. What if it is set to false?
It seems that the condition should be '$(ContinuousIntegrationBuild)' == 'true'.
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.
Yes! Good catch.
And assert success/failure explicitly.
Why? Why do those target have direct dependencies on this? Can't they just use properties/items? If so, the properties/items can just not be set by default, and the other targets can treat that as "don't opt into this". |
|
@rainersigwald How will the targets above make sure the properties are initialized before the targets run? |
|
The properties can't be initialized during evaluation phase. Tasks need to be called to retrieve the values. |
|
It would be the responsibility of the package that delivers these targets to ensure they happen at the right time by setting the right |
|
How should that package know what are all the consumers of these properties? It seems backwards. |
|
I'd be open to the idea of adding a new stub target in common targets to act as your hook point. Then any target (not Roslyn because of downlevel compat, but everybody else) could DependsOn it directly, and the package could inject targets before it. How's that sound? |
|
@rainersigwald What do you mean by a stub target? Isn't that what this change is adding? |
rainersigwald
left a comment
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.
Comments on the actual task/targets.
| The usage of SourceRoot might be sensitive to what kind of separator is used (e.g. in SourceLink where it needs | ||
| to match the corresponding separators used in paths given to the compiler). | ||
| --> | ||
| <Error Text="SourceRoot paths are required to end with a slash or backslash: @(_InvalidSourceRoot, ', ')" |
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 do this validation in the task and get a localized error?
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.
The task is only invoked in deterministic build. I think it's better do the validation for all builds.
To address the localization: is there a localized version of Error task for predefined resource strings?
|
|
||
| <Target Name="_InitializeSourceRevisionIdFromStandardCI"> | ||
| <PropertyGroup> | ||
| <SourceRevisionId Condition="'$(SourceRevisionId)' == ''">$(STANDARD_CI_SOURCE_REVISION_ID)</SourceRevisionId> |
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.
You expect STANDARD_CI_SOURCE_REVISION_ID to be set outside the build process, right? Can't this just be the default setting in the evaluation phase? Then you can eliminate this target and have only a stub for InitializeSourceRevisionId that's overridable.
|
|
||
| <Target Name="_InitializePrivateRepositoryUrlFromStandardCI"> | ||
| <PropertyGroup> | ||
| <PrivateRepositoryUrl Condition="'$(PrivateRepositoryUrl)' == ''">$(STANDARD_CI_REPOSITORY_URL)</PrivateRepositoryUrl> |
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.
Ditto
| <PropertyGroup> | ||
| <!-- | ||
| ContinuousIntegrationBuild is set to true when building on a CI server. The variable is then used to enable settings that only | ||
| apply to official builds, as opposed to local builds on developer machine. |
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 think I like the name here. Even in this comment you're describing the difference between "official" builds and something else . . . why call it ContinuousIntegration?
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.
Have a better name? Many repos call this variable CIBuild. STANDARD_CI_XXX variables include the "CI" in their name and this variable is tied to those (when available). So I think that's consistent naming.
| --> | ||
|
|
||
| <ItemGroup Condition="'@(_MappedSourceRoot)' != ''"> | ||
| <_MappedSourceRoot Remove="@(_MappedSourceRoot)" /> |
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.
Is this just defensive? That's not something we generally do.
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.
Yes, it's defensive since <Output> with ItemName adds to the list. Shouldn't we do it more generally? What if someone has a temporary item group of the same name?
| If the project references a package that provides source control information (such as Microsoft.Build.Tasks.Git, Microsoft.Build.Tasks.TFVC, etc.) | ||
| the revision id becomes available in local build environments as well. | ||
| Extensibility: Add target to InitializeSourceRevisionIdDependsOn to initialize SourceRevisionId. |
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 just override this target?
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.
That's one way to do it. The disadvantage is that the extension can't fall back to the default (like a base call in overridden method).
| MappedSourceRoots = SourceRoots; | ||
| } | ||
|
|
||
| return success; |
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.
In our experience, the best practice for returning from Task.Execute is
return !Log.HasLoggedErrors;That helps avoid the confusing situation where you fail a task but have no error in the log about why.
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.
OK.
| } | ||
|
|
||
| string EndWithSlash(string path) | ||
| => (path[path.Length - 1] == '/') ? path : path + '/'; |
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.
If this stays here you should have access to FileUtilities.EnsureTrailingSlash.
| string EndWithSlash(string path) | ||
| => (path[path.Length - 1] == '/') ? path : path + '/'; | ||
|
|
||
| // assign mapped paths to process source control roots first: |
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.
What's the intention behind this ordering?
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.
In most cases there will be a single SourceRoot that corresponds to the repository root, plus some additional SourceRoots for source packages and other external source roots.
The first SourceRoot in the item group is mapped to /_/ path prefix. Since the mapped paths are visible to the user it's better if /_/ prefix is commonly used for all the files in the repo (as opposed to /_N/ prefix based on the order the targets added their SourceRoots).
I'll add a comment.
|
Sounds good for What about |
|
InitializeSourceRoot needs to be called from C#, VB, F# and C++ targets. These targets consume the mapped paths (the output of the MapSourceRoot task). |
|
I don't understand how that impacts the dependency graph above. Can you elaborate? |
|
Currently each compiler has their own build task assembly. So to remove the MapSourceRoot from common tasks we would need 4 copies of that task. |
|
It would ship in what I'm calling |
|
Ok, so how do I add a FancyNew.Common.nupkg dependency to C++ targets? |
|
You'd add it at the project level, like all NuGet references. Referencing a package would serve as the opt-in for this feature set. For C++ specifically, this might require jumping through a couple of hoops until vcxproj is taught to handle PackageReference in the project, but I think it could be done with (shudder) an |
|
So all projects in the world will have this NuGet reference? It shouldn't be opt-in. The intent is to make it the default. |
|
I thought that when we discussed this a while ago we all agreed that it might be default later but must be opt-in now. |
|
Not determinism. Deterministic build is on by default for .NET Core projects. We agreed on making SourceLink opt-in. This is not about SourceLink. The SourceRoot item group is used for both deterministic build and for SourceLink. |
|
Superseded by #3063 |


Adds a few extensibility points to common targets that enable source control and SourceLink package features and fully deterministic compilation.
See https://github.com/tmat/repository-info/blob/master/docs/Readme.md for details.
TODO: