Visitar URL original
Support SourceLink revision placeholders for reusable compiler outputs by chsienki · Pull Request #1784 · dotnet/sourcelink · GitHub
Skip to content

Support SourceLink revision placeholders for reusable compiler outputs - #1784

Merged
tmat merged 4 commits into
dotnet:mainfrom
chsienki:chsienki/deferred-provenance
Oct 30, 2026
Merged

tmat merged 4 commits into
dotnet:mainfrom
chsienki:chsienki/deferred-provenance

Conversation

@chsienki

@chsienki chsienki commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

A checkout revision can change SourceLink compiler inputs even when the compiled sources are unchanged. Build systems that reuse compiler outputs need stable inputs while retaining the real revision for source debugging at publication time.

Add an opt-in SourceLinkUsePlaceholderValues mode that keeps normal repository and provider discovery, emits deterministic equal-width revision markers in the SourceLink map, and writes the resolved revisions to a separate JSON dictionary.

chsienki and others added 2 commits October 28, 2026 15:47
Keep mapping discovery and JSON serialization independent of file I/O so
content tests do not require output paths. Preserve existing file updates,
empty-output cleanup, and diagnostic behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cb41f08-51b6-4c9a-93ce-5d22ee71a6d7
Allow builds to reuse compiler outputs across revision-only changes while
retaining current repository discovery and real source-root metadata.

Emit deterministic, equal-width revisions in SourceLink and write the real
values to a separate dictionary. Keep the dictionary out of compiler inputs
and refresh it even when compilation is skipped.

Validate placeholder mappings and output destinations, remove generated
outputs on task failure, and cover revision-only reuse and source/URL
invalidation with unit and SDK integration tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cb41f08-51b6-4c9a-93ce-5d22ee71a6d7
@chsienki
chsienki requested a review from tmat as a code owner October 28, 2026 23:39
@chsienki

Copy link
Copy Markdown
Member Author

/cc @sbomer


// Fixed-width revisions allow in-place substitution in uncompressed metadata.
// A unique occurrence avoids replacing unrelated URL text.
if ((revision.Length != 40 && revision.Length != 64) ||

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.

Would you mind extracting this to a shared helper?

Similar checks are here:

static bool IsHexDigit(char c)
=> c is >= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F';
var revisionId = SourceRoot.GetMetadata(Names.SourceRoot.RevisionId);
// SHA-1 (40 hex digits) or SHA-256 (64 hex digits) object name.
if (revisionId == null || revisionId.Length is not (40 or 64) || !revisionId.All(IsHexDigit))

and here:

private bool IsObjectId(string reference)
=> reference.Length == _objectNameFormat.HashSize * 2 && reference.All(CharUtils.IsHexadecimalDigit);

{
WriteSourceLinkFile(GenerateSourceLinkContent());
var mappings = GetSourceLinkMappings();
if (Log.HasLoggedErrors)

@tmat tmat Oct 29, 2026 •

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.

Might be better to put this in a try-finally, so we always delete the files on error.

string? placeholder = null;
if (UsePlaceholderValues)
{
// Stable mapped roots give placeholders checkout-independent identities.

@tmat tmat Oct 29, 2026 •

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.

I'd extract this to a method -- TryGetRevision(..., out revision, out placeholder)

var template = url.Substring(0, revisionStart) + "<revision>" + url.Substring(revisionStart + revision.Length);
using (var sha = SHA256.Create())
{
placeholder = BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(mappedPath + "\n" + template)))

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.

Any particular reason for this format? Please, add a comment explaining.

// Duplicate roots make document mappings ambiguous, including fixed mappings.
if (!mappedPaths.Add(mapping.LocalPath))
{
Log.LogError(Resources.InvalidSourceLinkPlaceholderRoot, mapping.Root.ItemSpec);

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.

Should this be reported even for null Placeholder?


if (File.Exists(OutputFile))
// Relative paths must not be resolved against MSBuild's shared process working directory.
if (path == null || path.Length == 0 || !PathUtilities.IsPathFullyQualified(path))

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.

Nit: do we need to check empty path explicitly here?

{
// Validate both destinations before either output is written.
var otherPath = isRevisionFile ? OutputFile : RevisionOutputFile;
if (otherPath == null || otherPath.Length == 0 || !PathUtilities.IsPathFullyQualified(otherPath))

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.

Nit: do we need to check empty path explicitly here?


if (File.Exists(OutputFile))
// Relative paths must not be resolved against MSBuild's shared process working directory.
if (path == null || path.Length == 0 || !PathUtilities.IsPathFullyQualified(path))

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.

I'd move path validation to the start of the Execute method.

{
// Validate both destinations before either output is written.
var otherPath = isRevisionFile ? OutputFile : RevisionOutputFile;
if (otherPath == null || otherPath.Length == 0 || !PathUtilities.IsPathFullyQualified(otherPath))

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.

I'd move path validation to the start of the Execute method.

}
}

if (isRevisionFile)

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 don't we create directory for sourcelink.json file as well?

void deleteFile(string? path)
{
// Invalid paths must not be resolved against MSBuild's shared process working directory.
if (path == null || path.Length == 0 || !PathUtilities.IsPathFullyQualified(path))

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.

This check would be unnecessary if we checked all output paths upfront, at the start of Execute

SourceLink = null;
}

internal sealed class SourceLinkMapping(ITaskItem root, string localPath, string url, string revision, string? placeholder)

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.

Nit: struct?

Comment thread docs/README.md Outdated
URLs. Unsupported or ambiguous mappings fail the build. Fixed mappings without source-control
or revision metadata remain unchanged.

Each marker has the same width as its revision. It is the lowercase SHA-256 of the mapped

@tmat tmat Oct 29, 2026 •

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.

Seems like an implementation detail that does not need to be mentioned here.

It is the lowercase SHA-256 of the mappedExpand commentComment on line R161
root, a newline, and the URL with its revision replaced by <revision>, truncated to that
width.

Comment thread docs/README.md Outdated
### SourceLinkUsePlaceholderValues

Set `SourceLinkUsePlaceholderValues` to `true` to generate stable revision markers in the
compiler's Source Link input and write the resolved revisions separately. The default

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.

... separately to a file specified by SourceLinkRevisionFile.

Comment thread docs/README.md Outdated
Set by target `InitializeSourceControlInformationFromSourceControlManager` and consumed by NuGet `Pack` target and `GenerateAssemblyInfo` target.
May be used by custom targets that need this information.

### SourceLinkUsePlaceholderValues

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.

Do we need a separate property for this?
Why not just use SourceLinkRevisionFile if specified (non-empty) and not do it when it's not?

Comment thread docs/README.md Outdated

When using placeholder values, this specifies the revision dictionary path, relative to the
project directory or fully qualified. The default is
`$(IntermediateOutputPath)$(MSBuildProjectName).sourcelink.revisions.json`. The task receives

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 task receives a normalized absolute path, independent of MSBuild's process working directory.

Remove

Comment thread docs/README.md Outdated
{"SourceLinkRevision_<marker>":"<resolved-revision>"}
```

The dictionary is refreshed from current discovery even when compilation skips. Empty

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.

To much detail here. Most if does not add value.

Keep the MSBuild opt-in independent of the optional revision-file path so
builds can enable placeholders without choosing a sidecar location.

Validate destinations before I/O and clean generated outputs after execution
failures. Preserve fixed mappings outside placeholder uniqueness checks and
use a NUL delimiter to keep marker inputs unambiguous.

Share object-ID validation without changing callers' casing or repository
width requirements, and cover the contracts with unit and SDK build controls.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cb41f08-51b6-4c9a-93ce-5d22ee71a6d7
@chsienki

Copy link
Copy Markdown
Member Author

@tmat thanks! This is ready for another review

Describe the generated sidecar by its placeholder replacement contract.
Align the task output parameter and MSBuild property with that purpose,
while preserving the dictionary format and default filename.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cb41f08-51b6-4c9a-93ce-5d22ee71a6d7
@tmat
tmat merged commit ba04c00 into dotnet:main Oct 30, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants