Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/design/tools/illink/task-cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ internal sealed class ILLinkCache
- Defer identifying the runtime that executes ILLink; do not include a runtime version or framework description in the key. The task runs in MSBuild, so its CoreLib informational version and framework description are not reliable identities for the child linker's runtime. The runtimeconfig describes requested frameworks and roll-forward policy, not the exact resolved CoreLib. Finding that CoreLib would require resolved-runtime information from the caller, host-resolution plumbing, or an additional process; defer that complexity and startup cost.
- This deliberately accepts that runtime installation changes not represented by other keyed inputs may reuse an entry; callers needing invalidation for those changes must clear or isolate the cache.
- Include platform, architecture, and culture. Do not enumerate or hash inherited environment variables, or bypass caching based on them. This deliberately keeps v1 simple rather than maintaining a partial list of runtime/loader settings. Environment changes are reflected only when they change other keyed inputs, such as the selected host path or culture. Callers must disable caching when inherited settings introduce otherwise untracked dependencies, affect outputs, or require tool-execution side effects (for example, startup hooks or profilers).
- Bypass caching for non-whitespace `ExtraArgs`, custom steps/data, dependency-dump options, and explicit `ToolTask.EnvironmentVariables` overrides. These are caller-supplied inputs that v1 does not model in its key and can introduce undeclared dependencies, external outputs, or side effects. These invocations still run the linker normally; no extra-argument parsing or runtime-specific exceptions are supported.
- Bypass caching for arbitrary `ExtraArgs`, custom steps/data, dependency-dump options, and explicit `ToolTask.EnvironmentVariables` overrides. These are caller-supplied inputs that v1 does not model in its key and can introduce undeclared dependencies, external outputs, or side effects. The supported `ExtraArgs` shape for runtime's library builds is `--ignore-link-attributes true` followed by zero or more `--link-attributes FILE`, `--substitutions FILE`, or `-d DIRECTORY` pairs. Hash every referenced XML file and every top-level `.dll`, `.exe`, and `.winmd` candidate in each search directory, including assembly sidecars/modules/resources. Preserve directory order in the arguments and leave linker resolution unchanged; adding, changing, or removing candidates invalidates the key, including shadowed candidates. An unreadable directory or uninspectable candidate bypasses caching. All other SDK options passed through `ExtraArgs` remain subject to the bypass.
- Log and bypass caching if required files cannot be read or an input assembly cannot be inspected, including PE images without managed metadata. Normal linking determines whether those inputs are acceptable. Input files must remain stable during hashing and linking.

## Entry layout
Expand Down
8 changes: 5 additions & 3 deletions docs/tools/illink/illink-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ clear or isolate the cache when that distinction is required.
Cache hits replace the output directory without running ILLink.
Directories are created in the cache only when storing a successful result. Hits do not replay warnings or other linker diagnostics.

Caching is bypassed with a diagnostic for non-whitespace `ExtraArgs`, custom steps/data,
dependency-dump options, and explicit task environment overrides. Options supplied through
`ExtraArgs` remain unsupported for caching; these invocations still run the linker normally.
Caching is bypassed with a diagnostic for arbitrary `ExtraArgs`, custom steps/data,
dependency-dump options, and explicit task environment overrides. Runtime's library-build
arguments for link attributes, substitutions, and assembly search directories are supported.
Their XML files and search-directory assembly candidates (including sidecars) are content-hashed,
without changing assembly resolution. Other SDK options supplied through `ExtraArgs` remain unsupported.
Inherited environment variables are not tracked; disable caching
when they affect outputs or dependencies beyond the keyed inputs, or require tool-execution
side effects such as startup hooks or profiling. Unreadable inputs or linker files also
Expand Down
52 changes: 41 additions & 11 deletions eng/illink.targets
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,9 @@
<Target Name="PrepareForAssembliesTrim">
<!-- ILLink.Tasks arguments common to runs for both individual libraries and for the entire runtime pack -->
<PropertyGroup>
<_ILLinkHasCustomArgs Condition="'$(ILLinkArgs)' != ''">true</_ILLinkHasCustomArgs>
<!-- don't remove attributes after build, our tooling is not ready for that -->
<ILLinkArgs>$(ILLinkArgs) --ignore-link-attributes true</ILLinkArgs>
<!-- ignore unresolved references -->
<ILLinkArgs>$(ILLinkArgs) --skip-unresolved true</ILLinkArgs>
</PropertyGroup>

<!-- When running from Desktop MSBuild, DOTNET_HOST_PATH is not set.
Expand All @@ -203,6 +202,18 @@
Condition="'$(ILLinkTrimAssembly)' == 'true'"
DependsOnTargets="PrepareForAssembliesTrim">
<PropertyGroup>
<!-- suppress warnings with the following codes:
IL2008: Could not find type A specified in resource B
IL2009: Could not find method A in type B specified in resource C
IL2121: Unnecessary UnconditionalSuppressMessage attribute
IL2025: Duplicate preserve of A in B
IL2035: Unresolved assembly A in DynamicDependencyAttribute on B
-->
<LinkerNoWarn>$(LinkerNoWarn);IL2008;IL2009;IL2121;IL2025;IL2035</LinkerNoWarn>
</PropertyGroup>

<!-- Preserve argument ordering and overrides for callers supplying custom linker arguments. -->
<PropertyGroup Condition="'$(_ILLinkHasCustomArgs)' == 'true'">
<!-- default action for assemblies with IsTrimmable attribute -->
<ILLinkArgs>$(ILLinkArgs) --trim-mode skip</ILLinkArgs>
<!-- default action for assemblies without IsTrimmable attribute -->
Expand All @@ -215,18 +226,30 @@
<ILLinkArgs Condition="'@(ILLinkDescriptorsLibraryBuildXml)' != ''">$(ILLinkArgs) -x "@(ILLinkDescriptorsLibraryBuildXml->'%(FullPath)', '" -x "')"</ILLinkArgs>
<ILLinkArgs Condition="'$(ILLinkSubstitutionsLibraryBuildXml)' != ''">$(ILLinkArgs) --substitutions "$(ILLinkSubstitutionsLibraryBuildXml)"</ILLinkArgs>
<ILLinkArgs Condition="'@(ILLinkSuppressionsLibraryBuildXml)' != ''">$(ILLinkArgs) --link-attributes "@(ILLinkSuppressionsLibraryBuildXml->'%(FullPath)', '" --link-attributes "')"</ILLinkArgs>
<!-- suppress warnings with the following codes:
IL2008: Could not find type A specified in resource B
IL2009: Could not find method A in type B specified in resource C
IL2121: Unnecessary UnconditionalSuppressMessage attribute
IL2025: Duplicate preserve of A in B
IL2035: Unresolved assembly A in DynamicDependencyAttribute on B
-->
<LinkerNoWarn>$(LinkerNoWarn);IL2008;IL2009;IL2121;IL2025;IL2035</LinkerNoWarn>
<ILLinkArgs>$(ILLinkArgs) --nowarn $(LinkerNoWarn)</ILLinkArgs>
<ILLinkArgs Condition="'$(ILLinkDisableIPConstProp)' == 'true'">$(ILLinkArgs) --disable-opt ipconstprop</ILLinkArgs>
</PropertyGroup>

<PropertyGroup Condition="'$(_ILLinkHasCustomArgs)' != 'true'">
<_ILLinkTrimMode>skip</_ILLinkTrimMode>
<_ILLinkDefaultAction>skip</_ILLinkDefaultAction>
<_ILLinkNoWarn>$(LinkerNoWarn)</_ILLinkNoWarn>
<_ILLinkRemoveSymbols>true</_ILLinkRemoveSymbols>
<_ILLinkRemoveSymbols Condition="'$(ILLinkRewritePDBs)' == 'true' and Exists('$(ILLinkTrimAssemblySymbols)')">false</_ILLinkRemoveSymbols>
<_ILLinkPreserveSymbolPaths Condition="'$(_ILLinkRemoveSymbols)' == 'false' and '$(DeterministicSourcePaths)' == 'true'">true</_ILLinkPreserveSymbolPaths>
<_ILLinkIPConstProp Condition="'$(ILLinkDisableIPConstProp)' == 'true'">false</_ILLinkIPConstProp>
<ILLinkArgs Condition="'$(ILLinkSubstitutionsLibraryBuildXml)' != ''">$(ILLinkArgs) --substitutions "$(ILLinkSubstitutionsLibraryBuildXml)"</ILLinkArgs>
<ILLinkArgs Condition="'@(ILLinkSuppressionsLibraryBuildXml)' != ''">$(ILLinkArgs) --link-attributes "@(ILLinkSuppressionsLibraryBuildXml->'%(FullPath)', '" --link-attributes "')"</ILLinkArgs>
</PropertyGroup>

<ItemGroup>
<_ILLinkAssemblyToTrim Include="$(ILLinkTrimInputPath)$(TargetName)$(TargetExt)">
<TrimMode Condition="'$(_ILLinkHasCustomArgs)' != 'true'">link</TrimMode>
</_ILLinkAssemblyToTrim>
<_ILLinkRootDescriptors Include="@(ILLinkDescriptorsLibraryBuildXml->'%(FullPath)')"
Condition="'$(_ILLinkHasCustomArgs)' != 'true'" />
</ItemGroup>

<!-- Move the assembly into a subdirectory for ILLink -->
<Move SourceFiles="$(ILLinkTrimAssemblyPath)"
DestinationFolder="$(ILLinkTrimInputPath)">
Expand Down Expand Up @@ -264,8 +287,15 @@
<ILLinkArgs Condition="@(_DependencyDirectories->Count()) > 0">$(ILLinkArgs) -d @(_DependencyDirectories->'"%(Identity)"', ' -d ')</ILLinkArgs>
</PropertyGroup>

<ILLink AssemblyPaths="$(ILLinkTrimInputPath)$(TargetName)$(TargetExt)"
<ILLink AssemblyPaths="@(_ILLinkAssemblyToTrim)"
RootAssemblyNames="@(ILLinkTrimInputAssembly)"
RootDescriptorFiles="@(_ILLinkRootDescriptors)"
TrimMode="$(_ILLinkTrimMode)"
DefaultAction="$(_ILLinkDefaultAction)"
NoWarn="$(_ILLinkNoWarn)"
RemoveSymbols="$(_ILLinkRemoveSymbols)"
PreserveSymbolPaths="$(_ILLinkPreserveSymbolPaths)"
IPConstProp="$(_ILLinkIPConstProp)"
OutputDirectory="$(ILLinkTrimOutputPath)"
ExtraArgs="$(ILLinkArgs)"
ToolExe="$(_DotNetHostFileName)"
Expand Down
5 changes: 3 additions & 2 deletions src/libraries/oob-trim.proj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<TargetFramework>$(NetCoreAppCurrent)-$(TargetOS)</TargetFramework>
<!-- Import the illink file which contains some of the logic required to illink the out-of-band assemblies. -->
<ExplicitlyImportCustomILLinkTargets>true</ExplicitlyImportCustomILLinkTargets>
<!-- Unnecessary suppressions - disable for now since we didn't clean the runtime yet -->
<OOBILLinkNoWarn Condition="'$(OOBILLinkNoWarn)' == ''">IL2121</OOBILLinkNoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -37,8 +39,6 @@

<PropertyGroup>
<OOBILLinkArgs>$(ILLinkArgs)</OOBILLinkArgs>
<!-- Unnecessary suppressions - disable for now since we didn't clean the runtime yet -->
<OOBILLinkArgs>$(ILLinkArgs) --nowarn IL2121</OOBILLinkArgs>
<OOBILLinkArgs Condition="'@(OOBLibrarySuppressionsXml)' != ''" >$(OOBILLinkArgs) --link-attributes "@(OOBLibrarySuppressionsXml->'%(FullPath)', '" --link-attributes "')"</OOBILLinkArgs>
</PropertyGroup>

Expand All @@ -52,6 +52,7 @@
ReferenceAssemblyPaths="@(OOBAssemblyReference)"
OutputDirectory="$(OOBAssembliesTrimDir)"
ExtraArgs="$(OOBILLinkArgs)"
NoWarn="$(OOBILLinkNoWarn)"
ToolExe="$(_DotNetHostFileName)"
ToolPath="$(_DotNetHostDirectory)" />

Expand Down
3 changes: 1 addition & 2 deletions src/libraries/sfx-finish.proj
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,6 @@

<PropertyGroup>
<SharedFrameworkILLinkArgs>$(ILLinkArgs)</SharedFrameworkILLinkArgs>
<!-- update debug symbols -->
<SharedFrameworkILLinkArgs>$(SharedFrameworkILLinkArgs) -b true</SharedFrameworkILLinkArgs>
<SharedFrameworkILLinkArgs Condition="'@(SharedFrameworkSuppressionsXml)' != ''" >$(SharedFrameworkILLinkArgs) --link-attributes "@(SharedFrameworkSuppressionsXml, '" --link-attributes "')"</SharedFrameworkILLinkArgs>
</PropertyGroup>

Expand All @@ -141,6 +139,7 @@
RootAssemblyNames="@(_SharedFrameworkRootAssemblyName)"
OutputDirectory="$(SharedFrameworkAssembliesTrimDir)"
ExtraArgs="$(SharedFrameworkILLinkArgs)"
RemoveSymbols="false"
ToolExe="$(_DotNetHostFileName)"
ToolPath="$(_DotNetHostDirectory)" />

Expand Down
103 changes: 97 additions & 6 deletions src/tools/illink/src/ILLink.Tasks/LinkTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,15 +310,16 @@ private bool TryComputeCacheKey(string pathToTool, string commandLineCommands, s
return false;
}

if (!string.IsNullOrWhiteSpace(ExtraArgs))
{
Log.LogMessage(MessageImportance.Low, "ILLink cache bypassed: extra arguments are not supported.");
return false;
}

try
{
var files = new SortedSet<string>(StringComparer.Ordinal);
var directories = new SortedSet<string>(StringComparer.Ordinal);
if (!TryAddCacheableExtraArgsInputs(files, directories))
{
Log.LogMessage(MessageImportance.Low, "ILLink cache bypassed: extra arguments are not supported.");
return false;
}

var assemblies = new HashSet<string>(StringComparer.Ordinal);
foreach (ITaskItem assembly in AssemblyPaths)
AddAssembly(assembly.ItemSpec);
Expand All @@ -327,6 +328,22 @@ private bool TryComputeCacheKey(string pathToTool, string commandLineCommands, s
foreach (ITaskItem descriptor in RootDescriptorFiles ?? Array.Empty<ITaskItem>())
AddFile(descriptor.ItemSpec);

// Preserve AssemblyResolver's search behavior instead of replacing -d with
// explicit references. Include every candidate so additions and shadowing invalidate.
foreach (string directory in directories)
{
foreach (string file in Directory.EnumerateFiles(directory))
{
string extension = Path.GetExtension(file);
if (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".exe", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".winmd", StringComparison.OrdinalIgnoreCase))
{
AddAssembly(file);
}
}
}

#pragma warning disable IL3000 // MSBuild tasks are loaded from assemblies on disk.
AddFile(typeof(ILLink).Assembly.Location);
#pragma warning restore IL3000
Expand Down Expand Up @@ -408,6 +425,80 @@ void AddAssembly(string path)
}
}

private bool TryAddCacheableExtraArgsInputs(SortedSet<string> files, SortedSet<string> directories)
{
ReadOnlySpan<char> arguments = ExtraArgs.AsSpan().Trim();
if (arguments.IsEmpty)
return true;

// The runtime library builds pass this policy through ExtraArgs. Keep the
// supported shape narrow so arbitrary linker arguments continue to bypass caching.
if (!TryReadArgument(ref arguments, out string option) ||
option != "--ignore-link-attributes" ||
!TryReadArgument(ref arguments, out string value) ||
value != "true")
{
return false;
}

while (!arguments.TrimStart().IsEmpty)
{
if (!TryReadArgument(ref arguments, out option) ||
option is not ("--link-attributes" or "--substitutions" or "-d") ||
!TryReadArgument(ref arguments, out string path) ||
string.IsNullOrEmpty(path))
{
return false;
}

if (option == "-d")
directories.Add(Path.GetFullPath(path));
else
files.Add(Path.GetFullPath(path));
}

return true;

static bool TryReadArgument(ref ReadOnlySpan<char> arguments, out string argument)
{
arguments = arguments.TrimStart();
if (arguments.IsEmpty)
{
argument = string.Empty;
return false;
}

if (arguments[0] == '"')
{
int closingQuote = arguments.Slice(1).IndexOf('"');
if (closingQuote < 0)
{
argument = string.Empty;
return false;
}

argument = arguments.Slice(1, closingQuote).ToString();
arguments = arguments.Slice(closingQuote + 2);
return arguments.IsEmpty || char.IsWhiteSpace(arguments[0]);
}

int end = 0;
while (end < arguments.Length && !char.IsWhiteSpace(arguments[end]))
end++;

ReadOnlySpan<char> token = arguments.Slice(0, end);
if (token.IndexOf('"') >= 0)
{
argument = string.Empty;
return false;
}

argument = token.ToString();
arguments = arguments.Slice(end);
return true;
}
}

private string _illinkPath = "";

public string ILLinkPath
Expand Down
Loading
Loading