Skip to content

Commit 3e02f9a

Browse files
authored
Support attribute trimming opt-in (dotnet/linker#1839)
* Support attribute opt-in * Update docs * Rename test attributes to match * Don't pass native SDK assemblies to tests on Windows * Update LinkTask * PR feedback - Don't warn on duplicate attributes - Remove comment - Fix typos and wording - Use static array - Rename CheckIsTrimmable -> IsTrimmable * PR feedback: rename options - --trim-action -> --trim-mode -- --default-action -> --action * Fix ILLink.Tasks test * PR feedback - Add plenty of comments - RootNonTrimmableAssemblies -> ProcessReferencesStep - Make -reference order stable using List instead of HashSet * PR feedback Make GetInputAssemblyPaths private Commit migrated from dotnet/linker@44907d9
1 parent f56144d commit 3e02f9a

File tree

86 files changed

+508
-241
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+508
-241
lines changed

src/tools/illink/docs/error-codes.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ the error code. For example:
129129

130130
#### `IL1038`: Exported type '{type.Name}' cannot be resolved
131131

132+
#### `IL1039`: Reference assembly '{assemblyPath}' could not be loaded
133+
134+
- A reference assembly input passed via -reference could not be loaded.
135+
132136
----
133137
## Warning Codes
134138

@@ -1510,6 +1514,15 @@ This is technically possible if a custom assembly defines `DynamicDependencyAttr
15101514
</linker>
15111515
```
15121516

1517+
#### `IL2102`: Invalid AssemblyMetadata("IsTrimmable", ...) attribute in assembly 'assembly'. Value must be "True"
1518+
1519+
- AssemblyMetadataAttribute may be used at the assembly level to turn on trimming for the assembly. The only supported value is "True", but the attribute contained an unsupported value.
1520+
1521+
``` C#
1522+
// IL2102: Invalid AssemblyMetadata("IsTrimmable", "False") attribute in assembly 'assembly'. Value must be "True"
1523+
[assembly: AssemblyMetadata("IsTrimmable", "False")]
1524+
```
1525+
15131526
## Single-File Warning Codes
15141527

15151528
#### `IL3000`: 'member' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'

src/tools/illink/docs/illink-options.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,25 @@ The linker can do the following things on all or individual assemblies
5858
- `delete`- remove them from the output
5959
- `save` - save them in memory without linking
6060

61-
You can specify an action per assembly using `-p` option like this:
61+
You can specify an action per assembly using `--action` option like this:
6262

63-
`illink -p link Foo`
63+
`illink --action link Foo`
6464

6565
or
6666

67-
`illink -p skip System.Windows.Forms`
67+
`illink --action skip System.Windows.Forms`
6868

69-
Or you can specify what to do for the core assemblies.
69+
Or you can specify what to do for the trimmed assemblies.
7070

71-
Core assemblies are the assemblies that belong to the base class library,
72-
like `System.Private.CoreLib.dll`, `System.dll` or `System.Windows.Forms.dll`.
71+
A trimmable assembly is any assembly that includes the attribute `System.Reflection.AssemblyMetadata("IsTrimmable", "True")`.
7372

74-
You can specify what action to do on the core assemblies with the option:
73+
You can specify what action to do on the trimmed assemblies with the option:
7574

76-
`-c skip|copy|link`
75+
`--trim-mode skip|copy|copyused|link`
76+
77+
You can specify what action to do on assemblies without such an attribute with the option:
78+
79+
`--action copy|link`
7780

7881
### The output directory
7982

src/tools/illink/docs/illink-tasks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ The linker can be invoked as an MSBuild task, `ILLink`. We recommend not using t
5858
RootAssemblyNames="@(LinkerRootAssemblies)"
5959
RootDescriptorFiles="@(LinkerRootDescriptors)"
6060
OutputDirectory="output"
61-
ExtraArgs="-t -c link" />
61+
ExtraArgs="-t --trim-mode link" />
6262
```
6363

6464
## Default Linking Behavior

src/tools/illink/src/ILLink.Tasks/LinkTask.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ public class ILLink : ToolTask
2323
/// UnusedInterfaces
2424
/// IPConstProp
2525
/// Sealer
26-
/// Maps to '-reference', and possibly '-p', '--enable-opt', '--disable-opt'
26+
/// Maps to '-reference', and possibly '--action', '--enable-opt', '--disable-opt'
2727
/// </summary>
2828
[Required]
2929
public ITaskItem[] AssemblyPaths { get; set; }
3030

3131
/// <summary>
3232
/// Paths to assembly files that are reference assemblies,
3333
/// representing the surface area for compilation.
34-
/// Maps to '-reference', with action set to 'skip' via '-p'.
34+
/// Maps to '-reference', with action set to 'skip' via '--action'.
3535
/// </summary>
3636
public ITaskItem[] ReferenceAssemblyPaths { get; set; }
3737

@@ -180,11 +180,16 @@ public class ILLink : ToolTask
180180
bool? _removeSymbols;
181181

182182
/// <summary>
183-
/// Sets the default action for assemblies.
184-
/// Maps to '-c' and '-u'.
183+
/// Sets the default action for trimmable assemblies.
184+
/// Maps to '--trim-mode'
185185
/// </summary>
186186
public string TrimMode { get; set; }
187187

188+
/// <summary>
189+
/// Sets the default action for assemblies which have not opted into trimming.
190+
/// Maps to '--action'
191+
public string DefaultAction { get; set; }
192+
188193
/// <summary>
189194
/// A list of custom steps to insert into the linker pipeline.
190195
/// Each ItemSpec should be the path to the assembly containing the custom step.
@@ -296,7 +301,7 @@ protected override string GenerateResponseFileCommands ()
296301

297302
string trimMode = assembly.GetMetadata ("TrimMode");
298303
if (!String.IsNullOrEmpty (trimMode)) {
299-
args.Append ("-p ");
304+
args.Append ("--action ");
300305
args.Append (trimMode);
301306
args.Append (' ').AppendLine (Quote (assemblyName));
302307
}
@@ -329,7 +334,7 @@ protected override string GenerateResponseFileCommands ()
329334
// Treat reference assemblies as "skip". Ideally we
330335
// would not even look at the IL, but only use them to
331336
// resolve surface area.
332-
args.Append ("-p skip ").AppendLine (Quote (assemblyName));
337+
args.Append ("--action skip ").AppendLine (Quote (assemblyName));
333338
}
334339
}
335340

@@ -396,7 +401,10 @@ protected override string GenerateResponseFileCommands ()
396401
args.AppendLine ("-b");
397402

398403
if (TrimMode != null)
399-
args.Append ("-c ").Append (TrimMode).Append (" -u ").AppendLine (TrimMode);
404+
args.Append ("--trim-mode ").AppendLine (TrimMode);
405+
406+
if (DefaultAction != null)
407+
args.Append ("--action ").AppendLine (DefaultAction);
400408

401409
if (CustomSteps != null) {
402410
foreach (var customStep in CustomSteps) {

src/tools/illink/src/linker/Linker.Steps/MarkStep.cs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,6 @@ void Initialize ()
200200
{
201201
InitializeCorelibAttributeXml ();
202202

203-
foreach (AssemblyDefinition assembly in _context.GetAssemblies ())
204-
InitializeAssembly (assembly);
205-
206203
ProcessMarkedPending ();
207204
}
208205

@@ -228,13 +225,6 @@ void InitializeCorelibAttributeXml ()
228225
_context.CustomAttributes.PrimaryAttributeInfo.AddInternalAttributes (provider, annotations);
229226
}
230227

231-
protected virtual void InitializeAssembly (AssemblyDefinition assembly)
232-
{
233-
var action = _context.Annotations.GetAction (assembly);
234-
if (IsFullyPreservedAction (action))
235-
MarkAssembly (assembly, new DependencyInfo (DependencyKind.AssemblyAction, action));
236-
}
237-
238228
void Complete ()
239229
{
240230
foreach (var body in _unreachableBodies) {
@@ -386,7 +376,7 @@ bool MarkFullyPreservedAssemblies ()
386376
// Fully mark any assemblies with copy/save action.
387377

388378
// Unresolved references could get the copy/save action if this is the default action.
389-
bool scanReferences = IsFullyPreservedAction (_context.CoreAction) || IsFullyPreservedAction (_context.UserAction);
379+
bool scanReferences = IsFullyPreservedAction (_context.TrimAction) || IsFullyPreservedAction (_context.DefaultAction);
390380

391381
if (!scanReferences) {
392382
// Unresolved references could get the copy/save action if it was set explicitly
@@ -1312,7 +1302,7 @@ protected void MarkAssembly (AssemblyDefinition assembly, DependencyInfo reason)
13121302

13131303
MarkExportedTypesTarget.ProcessAssembly (assembly, _context);
13141304

1315-
if (IsFullyPreservedAction (_context.Annotations.GetAction (assembly))) {
1305+
if (ProcessReferencesStep.IsFullyPreservedAction (_context.Annotations.GetAction (assembly))) {
13161306
MarkEntireAssembly (assembly);
13171307
return;
13181308
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.IO;
7+
8+
namespace Mono.Linker.Steps
9+
{
10+
public class ProcessReferencesStep : BaseStep
11+
{
12+
protected override void Process ()
13+
{
14+
// Walk over all -reference inputs and resolve any that may need to be rooted.
15+
16+
// For example:
17+
// -reference dir/Unreferenced.dll --action copy --trim-mode copyused
18+
// In this case we need to check whether Unreferenced has the
19+
// IsTrimmable attribute, and root it if not.
20+
// -reference dir/Unreferenced.dll --action copy --trim-mode copyused --action link Unreferenced
21+
// The per-assembly action wins over the default --action or --trim-mode,
22+
// so we don't need to load the assembly to check for IsTrimmable attribute.
23+
// -reference dir/Unreferenced.dll --action link --trim-mode link
24+
// In this case, we don't need to load the assembly up-front, because it will
25+
// not get the copy/save action, regardless of the IsTrimmable attribute.
26+
27+
// Note that we don't do the same for assemblies which may be resolved from input directories - such
28+
// assemblies will only be rooted if something loads them.
29+
foreach (var assemblyPath in GetInputAssemblyPaths ()) {
30+
var assemblyName = Path.GetFileNameWithoutExtension (assemblyPath);
31+
32+
// If there's no way that this reference could have the copy/save action,
33+
// we don't need to load it up-front.
34+
if (!MaybeIsFullyPreservedAssembly (assemblyName))
35+
continue;
36+
37+
// For the remaining references, we need to resolve them (which looks for IsTrimmable attribute)
38+
// to determine the action.
39+
var assembly = Context.TryResolve (assemblyName);
40+
if (assembly == null) {
41+
Context.LogError ($"Reference assembly '{assemblyPath}' could not be loaded", 1039);
42+
continue;
43+
}
44+
45+
// If the assigned action (now taking into account the IsTrimmable attribute) requires us
46+
// to root the assembly, do so.
47+
if (IsFullyPreservedAction (Annotations.GetAction (assembly)))
48+
Annotations.Mark (assembly.MainModule, new DependencyInfo (DependencyKind.AssemblyAction, assembly));
49+
}
50+
}
51+
52+
IEnumerable<string> GetInputAssemblyPaths ()
53+
{
54+
var assemblies = new HashSet<string> ();
55+
foreach (var referencePath in Context.Resolver.GetReferencePaths ()) {
56+
var assemblyName = Path.GetFileNameWithoutExtension (referencePath);
57+
if (assemblies.Add (assemblyName))
58+
yield return referencePath;
59+
}
60+
}
61+
62+
public static bool IsFullyPreservedAction (AssemblyAction action)
63+
{
64+
return action == AssemblyAction.Copy || action == AssemblyAction.Save;
65+
}
66+
67+
bool MaybeIsFullyPreservedAssembly (string assemblyName)
68+
{
69+
if (Context.Actions.TryGetValue (assemblyName, out AssemblyAction action))
70+
return IsFullyPreservedAction (action);
71+
72+
return IsFullyPreservedAction (Context.DefaultAction) || IsFullyPreservedAction (Context.TrimAction);
73+
}
74+
}
75+
}

src/tools/illink/src/linker/Linker/AssemblyResolver.cs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
using System.Collections.Generic;
3131
using System.IO;
3232
using Mono.Cecil;
33-
using Mono.Collections.Generic;
3433

3534
namespace Mono.Linker
3635
{
@@ -42,7 +41,7 @@ public class AssemblyResolver : DirectoryAssemblyResolver
4241
HashSet<string> _unresolvedAssemblies;
4342
bool _ignoreUnresolved;
4443
LinkContext _context;
45-
readonly Collection<string> _references;
44+
readonly List<string> _references;
4645

4746

4847
public IDictionary<string, AssemblyDefinition> AssemblyCache {
@@ -57,7 +56,7 @@ public AssemblyResolver ()
5756
public AssemblyResolver (Dictionary<string, AssemblyDefinition> assembly_cache)
5857
{
5958
_assemblies = assembly_cache;
60-
_references = new Collection<string> () { };
59+
_references = new List<string> () { };
6160
}
6261

6362
public bool IgnoreUnresolved {
@@ -80,16 +79,18 @@ public string GetAssemblyFileName (AssemblyDefinition assembly)
8079
return assembly.MainModule.FileName;
8180
}
8281

83-
AssemblyDefinition ResolveFromReferences (AssemblyNameReference name, Collection<string> references, ReaderParameters parameters)
82+
AssemblyDefinition ResolveFromReferences (AssemblyNameReference name, ReaderParameters parameters)
8483
{
85-
var fileName = name.Name + ".dll";
86-
foreach (var reference in references) {
87-
if (Path.GetFileName (reference) != fileName)
88-
continue;
89-
try {
90-
return GetAssembly (reference, parameters);
91-
} catch (BadImageFormatException) {
92-
continue;
84+
foreach (var reference in _references) {
85+
foreach (var extension in DirectoryAssemblyResolver.Extensions) {
86+
var fileName = name.Name + extension;
87+
if (Path.GetFileName (reference) != fileName)
88+
continue;
89+
try {
90+
return GetAssembly (reference, parameters);
91+
} catch (BadImageFormatException) {
92+
continue;
93+
}
9394
}
9495
}
9596

@@ -107,7 +108,7 @@ public override AssemblyDefinition Resolve (AssemblyNameReference name, ReaderPa
107108
if (!_assemblies.TryGetValue (name.Name, out AssemblyDefinition asm) && (_unresolvedAssemblies == null || !_unresolvedAssemblies.Contains (name.Name))) {
108109
try {
109110
// Any full path explicit reference takes precedence over other look up logic
110-
asm = ResolveFromReferences (name, _references, parameters);
111+
asm = ResolveFromReferences (name, parameters);
111112

112113
// Fall back to the base class resolution logic
113114
if (asm == null)
@@ -139,6 +140,11 @@ public void AddReferenceAssembly (string referencePath)
139140
_references.Add (referencePath);
140141
}
141142

143+
public List<string> GetReferencePaths ()
144+
{
145+
return _references;
146+
}
147+
142148
protected override void Dispose (bool disposing)
143149
{
144150
foreach (var asm in _assemblies.Values) {

src/tools/illink/src/linker/Linker/DirectoryAssemblyResolver.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,12 @@ public virtual AssemblyDefinition Resolve (AssemblyNameReference name, ReaderPar
8383
throw new AssemblyResolutionException (name, new FileNotFoundException ($"Unable to find '{name.Name}.dll' or '{name.Name}.exe' file"));
8484
}
8585

86+
public static string[] Extensions = new[] { ".dll", ".exe" };
87+
8688
AssemblyDefinition SearchDirectory (AssemblyNameReference name, IEnumerable<string> directories, ReaderParameters parameters)
8789
{
88-
var extensions = new[] { ".dll", ".exe" };
8990
foreach (var directory in directories) {
90-
foreach (var extension in extensions) {
91+
foreach (var extension in Extensions) {
9192
string file = Path.Combine (directory, name.Name + extension);
9293
if (!File.Exists (file))
9394
continue;

0 commit comments

Comments
 (0)