Skip to content

Commit 9cd6167

Browse files
authored
Embed F# metadata trimming rules in compiled assemblies (#20527)
1 parent b561c33 commit 9cd6167

21 files changed

Lines changed: 283 additions & 210 deletions

File tree

‎docs/release-notes/.FSharp.Compiler.Service/11.0.100.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* Fix exponential (2^N) compile time in pattern matching with shared guards and partial active patterns. ([Issue #18425](https://github.com/dotnet/fsharp/issues/18425), [PR #20244](https://github.com/dotnet/fsharp/pull/20244))
1414
* Fix incorrect Debug lowering of inline builders that compose low-level resumable state machines. ([Issue #20466](https://github.com/dotnet/fsharp/issues/20466), [PR #20469](https://github.com/dotnet/fsharp/pull/20469))
1515
* Fix incorrect `StructLayout(Size = 1)` emission for data-less struct unions where the compiler-generated tag field makes the actual runtime size larger. ([PR #19759](https://github.com/dotnet/fsharp/pull/19759))
16+
* Embed F# metadata-removal rules before resource collection, preserve custom rules, and combine generated rules when linking assemblies. ([PR #20527](https://github.com/dotnet/fsharp/pull/20527))
1617
* Fix FS0750 "This construct may only be used within computation expressions" incorrectly raised for `let!`/`use!`/`do!` appearing in the right-hand side of a plain `let` binding inside a computation expression. The right-hand side is now desugared as a nested computation of the same builder whose result is bound with `let!`, keeping its bindings correctly scoped. ([Issue #19457](https://github.com/dotnet/fsharp/issues/19457), [PR #19868](https://github.com/dotnet/fsharp/pull/19868))
1718
* Fix `use` bindings for `IAsyncDisposable` in `dotnet fsi` task expressions by shipping the `net10.0` FSharp.Core in the SDK toolset. ([Issue #14454](https://github.com/dotnet/fsharp/issues/14454), [PR #20555](https://github.com/dotnet/fsharp/pull/20555))
1819
* Stop leaking a `System.Diagnostics.Metrics.MeterListener` per `Cache` in DEBUG builds. Each cache created a `CacheMetrics.CacheMetricsListener` (which starts a `MeterListener` registered in the process-global metrics registry) and never disposed it, so listeners accumulated for the lifetime of the process. Because every cache hit/miss/add published to all registered listeners, the per-operation cost grew linearly with the number of leaked listeners, so repeated checks (and Debug FCS test runs) slowed down over time. The per-cache `CacheMetricsListener` and the per-instance `cacheId` tag are removed; `DebugDisplay` and tests now read the existing name-aggregated stats populated by the single `ListenToAll` listener, so no per-cache listener is created and no per-operation cost is added. ([PR #19995](https://github.com/dotnet/fsharp/pull/19995))
@@ -88,6 +89,7 @@
8889
* Avoid duplicate parameter names in closure constructors. (Issue [#17692](https://github.com/dotnet/fsharp/issues/17692), [PR #19339](https://github.com/dotnet/fsharp/pull/19339))
8990
* Improve let-rec codegen: reorder bindings to allocate lambda closures before non-lambda values that reference them. ([PR #19339](https://github.com/dotnet/fsharp/pull/19339))
9091
* Fix `YieldFromFinal`/`ReturnFromFinal` being incorrectly called in non-tail positions (`for`, `use`, `use!`, `try/with` handler). ([Issue #19402](https://github.com/dotnet/fsharp/issues/19402), [PR #19403](https://github.com/dotnet/fsharp/pull/19403))
92+
* Preserve commas in embedded resource paths, including generated trimming substitutions under custom intermediate directories. ([PR #20527](https://github.com/dotnet/fsharp/pull/20527))
9193
* Fixed how the source ranges of warn directives are reported (as trivia) in the parser output (by not reporting leading spaces). ([Issue #19405](https://github.com/dotnet/fsharp/issues/19405), [PR #19408]((https://github.com/dotnet/fsharp/pull/19408)))
9294
* Fix UoM value type `ToString()` returning garbage values when `--checknulls+` is enabled, caused by double address-taking in codegen. ([Issue #19435](https://github.com/dotnet/fsharp/issues/19435), [PR #19440](https://github.com/dotnet/fsharp/pull/19440))
9395
* Fix accessibility and type-matching for extension method lookups. ([Issue #19349](https://github.com/dotnet/fsharp/issues/19349), [PR #19536](https://github.com/dotnet/fsharp/pull/19536))

‎src/Compiler/Driver/CompilerConfig.fs‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,10 +1098,22 @@ type TcConfigBuilder =
10981098
tcConfigB.pathMap <- tcConfigB.pathMap |> PathMap.addMapping oldPrefix newPrefix
10991099

11001100
static member SplitCommandLineResourceInfo(ri: string) =
1101-
let p = ri.IndexOf ','
1101+
let quoteEnd =
1102+
if ri.StartsWithOrdinal("\"") then
1103+
ri.IndexOf('"', 1)
1104+
else
1105+
-1
1106+
1107+
let p = ri.IndexOf(',', quoteEnd + 1)
1108+
let file = if p <> -1 then ri.Substring(0, p) else ri
1109+
1110+
let file =
1111+
if quoteEnd > 0 && quoteEnd = file.Length - 1 then
1112+
file.Substring(1, file.Length - 2)
1113+
else
1114+
file
11021115

11031116
if p <> -1 then
1104-
let file = String.sub ri 0 p
11051117
let rest = String.sub ri (p + 1) (String.length ri - p - 1)
11061118
let p = rest.IndexOf ','
11071119

@@ -1118,7 +1130,7 @@ type TcConfigBuilder =
11181130
else
11191131
file, rest, ILResourceAccess.Public
11201132
else
1121-
ri, FileSystemUtils.fileNameOfPath ri, ILResourceAccess.Public
1133+
file, FileSystemUtils.fileNameOfPath file, ILResourceAccess.Public
11221134

11231135
//----------------------------------------------------------------------------
11241136
// TcConfig

‎src/Compiler/Driver/CompilerConfig.fsi‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ type TcConfigBuilder =
567567

568568
member AddPathMapping: oldPrefix: string * newPrefix: string -> unit
569569

570+
/// Parse file[,name[,access]], allowing a quoted file path to contain commas.
570571
static member SplitCommandLineResourceInfo: string -> string * string * ILResourceAccess
571572

572573
// Directories to start probing in for native DLLs for FSI dynamic loading

‎src/Compiler/Driver/CompilerImports.fsi‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ val IsOptimizationDataResource: ILResource -> bool
4545
/// Determine if an IL resource attached to an F# assembly is an F# optimization data resource (data stream B)
4646
val IsOptimizationDataResourceB: ILResource -> bool
4747

48+
val GetNameOfILModule: ILModuleDef -> string
49+
4850
/// Determine if an IL resource attached to an F# assembly is an F# quotation data resource for reflected definitions
4951
val IsReflectedDefinitionsResource: ILResource -> bool
5052

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+
module internal FSharp.Compiler.ILLinkSubstitutions
4+
5+
open System.Xml.Linq
6+
7+
let names (assemblyName: string) =
8+
seq {
9+
for kind in [ "Signature"; "Optimization" ] do
10+
for suffix in [ "Data"; "CompressedData"; "DataB"; "CompressedDataB"; "Info" ] do
11+
yield $"FSharp{kind}{suffix}.{assemblyName}"
12+
}
13+
14+
let document (assemblyName: string) (names: seq<string>) =
15+
let x = XName.Get
16+
let assembly = XElement(x "assembly", XAttribute(x "fullname", assemblyName))
17+
18+
for name in names do
19+
assembly.Add(XElement(x "resource", XAttribute(x "name", name), XAttribute(x "action", "remove"), ""))
20+
21+
XElement(x "linker", assembly)

‎src/Compiler/Driver/StaticLinking.fs‎

Lines changed: 77 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
module internal FSharp.Compiler.StaticLinking
55

66
open System
7+
open System.Xml.Linq
78
open Internal.Utilities.Collections
89
open Internal.Utilities.Library
910
open Internal.Utilities.Library.Extras
@@ -23,6 +24,57 @@ open FSharp.Compiler.TypedTree
2324
open FSharp.Compiler.TypeProviders
2425
#endif
2526

27+
let linkMetadataResources assemblyName (resources: (string * ILResource) list) =
28+
let xname = XName.Get
29+
let prefixes = ILLinkSubstitutions.names "" |> Seq.toList
30+
31+
let generated, retained =
32+
resources
33+
|> List.splitChoose (fun (owner, resource) ->
34+
match resource.Location with
35+
| ILResourceLocation.Local _ when resource.Name.Equals("ILLink.Substitutions.xml", StringComparison.OrdinalIgnoreCase) ->
36+
try
37+
use stream = resource.GetBytes().AsStream()
38+
let xml = XElement.Load stream
39+
40+
let names =
41+
[
42+
for rule in xml.Descendants(xname "resource") do
43+
if rule.IsEmpty then
44+
rule.Value <- ""
45+
46+
for name in rule.Attributes(xname "name") do
47+
if List.exists name.Value.StartsWithOrdinal prefixes then
48+
name.Value
49+
]
50+
51+
match resource.CustomAttrs.AsList(), XNode.DeepEquals(xml, ILLinkSubstitutions.document owner names) with
52+
| [], true -> Choice1Of2(resource, names)
53+
| _ -> Choice2Of2(true, resource)
54+
with :? System.Xml.XmlException ->
55+
Choice2Of2(true, resource)
56+
| _ -> Choice2Of2(false, resource))
57+
58+
let resources = List.map snd retained
59+
60+
match generated with
61+
| (resource, _) :: _ when not (List.exists fst retained) ->
62+
let present = resources |> List.map _.Name |> Set.ofList
63+
64+
let names =
65+
generated |> List.collect snd |> List.filter present.Contains |> List.distinct
66+
67+
resources
68+
@ [
69+
if not names.IsEmpty then
70+
let xml = ILLinkSubstitutions.document assemblyName names
71+
72+
{ resource with
73+
Location = ILResourceLocation.Local(ByteStorage.FromByteArray(System.Text.Encoding.UTF8.GetBytes(xml.ToString())))
74+
}
75+
]
76+
| _ -> resources
77+
2678
// Handles TypeForwarding for the generated IL model
2779
type TypeForwarding(tcImports: TcImports) =
2880

@@ -157,51 +209,30 @@ let StaticLinkILModules
157209
]
158210

159211
let savedResources =
160-
let allResources =
161-
[
162-
for ccu, m in dependentILModules do
163-
for r in m.Resources.AsList() do
164-
(ccu, r)
165-
]
166-
// Don't save interface, optimization or resource definitions for provider-generated assemblies.
167-
// These are "fake".
168-
let isProvided (ccu: CcuThunk option) =
212+
[
213+
for ccu, m in dependentILModules do
214+
let provided =
169215
#if !NO_TYPEPROVIDERS
170-
match ccu with
171-
| Some c -> c.IsProviderGenerated
172-
| None -> false
216+
ccu |> Option.exists _.IsProviderGenerated
173217
#else
174-
ignore ccu
175-
false
218+
ignore ccu
219+
false
176220
#endif
221+
for r in m.Resources.AsList() do
222+
let order, enabled =
223+
if IsSignatureDataResource r || IsSignatureDataResourceB r then
224+
0, tcConfig.GenerateSignatureData
225+
elif IsOptimizationDataResource r || IsOptimizationDataResourceB r then
226+
1, tcConfig.GenerateOptimizationData
227+
else
228+
2, true
177229

178-
// Save only the interface/optimization attributes of generated data
179-
let intfDataResources, others =
180-
allResources
181-
|> List.partition (fun (_, r) -> IsSignatureDataResource r || IsSignatureDataResourceB r)
182-
183-
let intfDataResources =
184-
[
185-
for ccu, r in intfDataResources do
186-
if tcConfig.GenerateSignatureData && not (isProvided ccu) then
187-
r
188-
]
189-
190-
let optDataResources, others =
191-
others
192-
|> List.partition (fun (_, r) -> IsOptimizationDataResource r || IsOptimizationDataResourceB r)
193-
194-
let optDataResources =
195-
[
196-
for ccu, r in optDataResources do
197-
if tcConfig.GenerateOptimizationData && not (isProvided ccu) then
198-
r
199-
]
200-
201-
let otherResources = others |> List.map snd
202-
203-
let result = intfDataResources @ optDataResources @ otherResources
204-
result
230+
if enabled && (order = 2 || not provided) then
231+
yield order, (GetNameOfILModule m, r)
232+
]
233+
|> List.groupBy fst
234+
|> List.sortBy fst
235+
|> List.collect (snd >> List.map snd)
205236

206237
let moduls = ilxMainModule :: (List.map snd dependentILModules)
207238

@@ -248,7 +279,11 @@ let StaticLinkILModules
248279
]
249280
)
250281
TypeDefs = mkILTypeDefs (topTypeDef :: List.concat normalTypeDefs)
251-
Resources = mkILResources (savedResources @ ilxMainModule.Resources.AsList())
282+
Resources =
283+
savedResources
284+
@ (ilxMainModule.Resources.AsList() |> List.map (fun r -> oldManifest.Name, r))
285+
|> linkMetadataResources oldManifest.Name
286+
|> mkILResources
252287
NativeResources = savedNativeResources
253288
}
254289

‎src/Compiler/FSharp.Compiler.Service.fsproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@
542542
<Compile Include="Driver\XmlDocFileWriter.fs" />
543543
<Compile Include="Driver\BinaryResourceFormats.fsi" />
544544
<Compile Include="Driver\BinaryResourceFormats.fs" />
545+
<Compile Include="Driver\ILLinkSubstitutions.fs" />
545546
<Compile Include="Driver\StaticLinking.fsi" />
546547
<Compile Include="Driver\StaticLinking.fs" />
547548
<Compile Include="Driver\CreateILModule.fsi" />

‎src/Compiler/Utilities/FileSystem.fs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1093,9 +1093,11 @@ type ByteStorage(getByteMemory: unit -> ReadOnlyByteMemory) =
10931093
static member FromByteMemory(bytes: ReadOnlyByteMemory) = ByteStorage(fun () -> bytes)
10941094

10951095
static member FromByteMemoryAndCopy(bytes: ReadOnlyByteMemory, useBackingMemoryMappedFile: bool) =
1096+
let length = bytes.Length
1097+
10961098
if useBackingMemoryMappedFile then
10971099
match MemoryMappedFile.TryFromByteMemory(bytes) with
1098-
| Some mmf -> ByteStorage(fun () -> ByteMemory.FromMemoryMappedFile(mmf).AsReadOnly())
1100+
| Some mmf -> ByteStorage(fun () -> ByteMemory.FromMemoryMappedFile(mmf).Slice(0, length).AsReadOnly())
10991101
| _ ->
11001102
let copiedBytes = ByteMemory.FromArray(bytes.ToArray()).AsReadOnly()
11011103
ByteStorage.FromByteMemory(copiedBytes)

‎src/FSharp.Build/FSharp.Build.fsproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
<Compile Include="CreateFSharpManifestResourceName.fs" />
4949
<Compile Include="SubstituteText.fs" />
5050
<Compile Include="MapSourceRoots.fs" />
51+
<Compile Include="../Compiler/Driver/ILLinkSubstitutions.fs" />
5152
<Compile Include="GenerateILLinkSubstitutions.fs" />
5253
<None Include="Microsoft.FSharp.Targets" CopyToOutputDirectory="PreserveNewest" />
5354
<None Include="Microsoft.Portable.FSharp.Targets" CopyToOutputDirectory="PreserveNewest" />

‎src/FSharp.Build/Fsc.fs‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,14 +250,16 @@ type public Fsc() as this =
250250

251251
// Resources
252252
for item in resources do
253+
let fileName =
254+
if item.ItemSpec.IndexOf(',') >= 0 then
255+
$"\"{item.ItemSpec}\""
256+
else
257+
item.ItemSpec
258+
253259
match useStandardResourceNames with
254260
| true ->
255-
builder.AppendSwitchIfNotNull(
256-
"--resource:",
257-
item.ItemSpec,
258-
[| item.GetMetadata("LogicalName"); item.GetMetadata("Access") |]
259-
)
260-
| false -> builder.AppendSwitchIfNotNull("--resource:", item.ItemSpec)
261+
builder.AppendSwitchIfNotNull("--resource:", fileName, [| item.GetMetadata("LogicalName"); item.GetMetadata("Access") |])
262+
| false -> builder.AppendSwitchIfNotNull("--resource:", fileName)
261263

262264
// VersionFile
263265
builder.AppendSwitchIfNotNull("--versionfile:", versionFile)

0 commit comments

Comments
 (0)