From 679eeb9453694e9967b11d175afccdfb0fcc206e Mon Sep 17 00:00:00 2001 From: perf-bundle Date: Fri, 11 Sep 2026 14:22:38 +0200 Subject: [PATCH 1/8] Fix automatic F# metadata stripping during trimming Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Driver/StaticLinking.fs | 103 +++++++++++++++++- src/Compiler/FSComp.txt | 1 + src/Compiler/xlf/FSComp.txt.cs.xlf | 5 + src/Compiler/xlf/FSComp.txt.de.xlf | 5 + src/Compiler/xlf/FSComp.txt.es.xlf | 5 + src/Compiler/xlf/FSComp.txt.fr.xlf | 5 + src/Compiler/xlf/FSComp.txt.it.xlf | 5 + src/Compiler/xlf/FSComp.txt.ja.xlf | 5 + src/Compiler/xlf/FSComp.txt.ko.xlf | 5 + src/Compiler/xlf/FSComp.txt.pl.xlf | 5 + src/Compiler/xlf/FSComp.txt.pt-BR.xlf | 5 + src/Compiler/xlf/FSComp.txt.ru.xlf | 5 + src/Compiler/xlf/FSComp.txt.tr.xlf | 5 + src/Compiler/xlf/FSComp.txt.zh-Hans.xlf | 5 + src/Compiler/xlf/FSComp.txt.zh-Hant.xlf | 5 + .../GenerateILLinkSubstitutions.fs | 62 +++++++++-- .../Microsoft.FSharp.NetSdk.targets | 9 +- .../Directory.Build.props | 13 +++ .../MetadataSubstitutions/Library/Library.fs | 5 + .../Library/MetadataLibrary.fsproj | 12 ++ .../MetadataSubstitutions.fsproj | 21 ++++ .../Trimming/MetadataSubstitutions/Program.fs | 25 +++++ .../MetadataSubstitutions/Substitutions.xml | 3 + tests/AheadOfTime/Trimming/check.ps1 | 23 +++- .../StaticLinking/StaticLinking.fs | 14 +++ 26 files changed, 340 insertions(+), 17 deletions(-) create mode 100644 tests/AheadOfTime/Trimming/MetadataSubstitutions/Directory.Build.props create mode 100644 tests/AheadOfTime/Trimming/MetadataSubstitutions/Library/Library.fs create mode 100644 tests/AheadOfTime/Trimming/MetadataSubstitutions/Library/MetadataLibrary.fsproj create mode 100644 tests/AheadOfTime/Trimming/MetadataSubstitutions/MetadataSubstitutions.fsproj create mode 100644 tests/AheadOfTime/Trimming/MetadataSubstitutions/Program.fs create mode 100644 tests/AheadOfTime/Trimming/MetadataSubstitutions/Substitutions.xml diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index ed1e382d6c7..994b4ea9d3e 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -140,6 +140,7 @@ * Fix Go-to-Definition for provided constructors that lack `TypeProviderDefinitionLocationAttribute`. Navigation now falls back to the declaring type instead of silently failing. ([Issue #5538](https://github.com/dotnet/fsharp/issues/5538), [PR #19917](https://github.com/dotnet/fsharp/pull/19917)) * Render the declaring type for C#-style extension methods in overload-resolution error messages so the suggestion no longer falsely attributes the extension to the receiver type. ([Issue #9838](https://github.com/dotnet/fsharp/issues/9838), [PR #19925](https://github.com/dotnet/fsharp/pull/19925)) * Fix FSI pretty printing to distinguish anonymous records (`{| ... |}`) from nominal records (`{ ... }`). ([Issue #6116](https://github.com/dotnet/fsharp/issues/6116), [PR #19919](https://github.com/dotnet/fsharp/pull/19919)) +* Fix automatic removal of F# metadata resources during trimming, including projects with user substitution files or statically linked F# libraries. ([Issue #18591](https://github.com/dotnet/fsharp/issues/18591)) * Fix dot-completion after indexed expressions (`a.[0].Data.`, `a[0].Data.`, `[1;2].Length.`) returning unrelated global completions instead of expression-typings members. ([Issue #4966](https://github.com/dotnet/fsharp/issues/4966), [PR #19934](https://github.com/dotnet/fsharp/pull/19934)) * Quotations of `match s with "" -> _` no longer leak the `s <> null && s.Length = 0` lowering; the empty-string optimization moved from pattern-match compilation to the optimizer so quoted expressions keep `op_Equality(s, "")`. ([Issue #19873](https://github.com/dotnet/fsharp/issues/19873)) * Parser: recover on unfinished abstract members ([PR #20070](https://github.com/dotnet/fsharp/pull/20070)) diff --git a/src/Compiler/Driver/StaticLinking.fs b/src/Compiler/Driver/StaticLinking.fs index 7379423579e..0f7bd5981ba 100644 --- a/src/Compiler/Driver/StaticLinking.fs +++ b/src/Compiler/Driver/StaticLinking.fs @@ -4,6 +4,8 @@ module internal FSharp.Compiler.StaticLinking open System +open System.Xml +open System.Xml.Linq open Internal.Utilities.Collections open Internal.Utilities.Library open Internal.Utilities.Library.Extras @@ -23,6 +25,102 @@ open FSharp.Compiler.TypedTree open FSharp.Compiler.TypeProviders #endif +let combineMetadataSubstitutions assemblyName linkedAssemblies (resources: ILResource list) = + let xname = XName.Get + + let attribute name (element: XElement) = + match element.Attribute(xname name) with + | null -> "" + | value -> value.Value + + let resourceNames = resources |> List.map _.Name |> Set.ofList + + let substitutions, others = + resources + |> List.partition (fun r -> + r.Name.Equals("ILLink.Substitutions.xml", StringComparison.OrdinalIgnoreCase) + && (match r.Location with + | ILResourceLocation.Local _ -> true + | _ -> false)) + + let documents = + substitutions + |> List.choose (fun resource -> + use stream = resource.GetBytes().AsStream() + + let document = + try + XElement.Load(stream, LoadOptions.PreserveWhitespace) + with :? XmlException as ex -> + error (Error(FSComp.SR.fscInvalidILLinkSubstitutions ex.Message, rangeStartup)) + + let isMetadataAssembly (element: XElement) = + element.Name = xname "assembly" + && (element.Attributes() |> Seq.forall (fun attr -> attr.Name = xname "fullname")) + && Set.contains (attribute "fullname" element) linkedAssemblies + && element.HasElements + && (element.Elements() + |> Seq.forall (fun node -> + let name = attribute "name" node + let namedResource = { resource with Name = name } + + node.Name = xname "resource" + && not node.HasElements + && (node.Attributes() + |> Seq.forall (fun attr -> attr.Name = xname "name" || attr.Name = xname "action")) + && attribute "action" node = "remove" + && (IsSignatureDataResource namedResource + || IsSignatureDataResourceB namedResource + || IsOptimizationDataResource namedResource + || IsOptimizationDataResourceB namedResource))) + + if document.Name = xname "linker" then + for element in document.Elements() |> Seq.filter isMetadataAssembly |> Seq.toArray do + for node in element.Elements() |> Seq.toArray do + if not (resourceNames.Contains(attribute "name" node)) then + node.Remove() + + if element.HasElements then + element.SetAttributeValue(xname "fullname", assemblyName) + else + element.Remove() + + if document.Name = xname "linker" && not document.HasElements then + None + else + Some( + resource, + document, + document.Name = xname "linker" + && not document.HasAttributes + && (document.Elements() |> Seq.forall isMetadataAssembly) + )) + + let metadata, custom = + documents |> List.partition (fun (_, _, metadata) -> metadata) + + let documents = + match custom @ metadata with + | [] -> [] + | (resource, document, _) :: rest -> + let toAppend, toKeep = rest |> List.partition (fun (_, _, metadata) -> metadata) + + for _, source, _ in toAppend do + document.Add(source.Elements() |> Seq.toArray) + + (resource, document) + :: (toKeep |> List.map (fun (resource, document, _) -> resource, document)) + + others + @ (documents + |> List.map (fun (resource, document) -> + { resource with + Location = + ILResourceLocation.Local( + ByteStorage.FromByteArray(System.Text.Encoding.UTF8.GetBytes(document.ToString(SaveOptions.DisableFormatting))) + ) + })) + // Handles TypeForwarding for the generated IL model type TypeForwarding(tcImports: TcImports) = @@ -248,7 +346,10 @@ let StaticLinkILModules ] ) TypeDefs = mkILTypeDefs (topTypeDef :: List.concat normalTypeDefs) - Resources = mkILResources (savedResources @ ilxMainModule.Resources.AsList()) + Resources = + savedResources @ ilxMainModule.Resources.AsList() + |> combineMetadataSubstitutions oldManifest.Name (assems.Add oldManifest.Name) + |> mkILResources NativeResources = savedNativeResources } diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index a50d902136b..9ecdf6eccdc 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1817,3 +1817,4 @@ featureRecordSpreads,"record type and expression spreads" 3913,tcExtendedLayoutCannotBeUsedOnUnions,"The 'ExtendedLayoutAttribute' cannot be applied to discriminated unions" 3914,tcExtendedLayoutStructMustHaveInstanceField,"A struct with the 'ExtendedLayoutAttribute' must have at least one instance field" 3915,tcTupleTypeExtensionTooManyElements,"Tuple type extensions are supported only for tuples of up to 7 elements, but this tuple type has %d elements. Extensions of larger tuples are not supported." +3916,fscInvalidILLinkSubstitutions,"Invalid embedded ILLink.Substitutions.xml: %s" diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 36028a7a60c..9c63ad2f998 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -667,6 +667,11 @@ Funkce vytváření průřezů od konce vyžaduje jazykovou verzi preview. + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' Neplatná direktiva #{0} {1} diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index 701a63c7a2b..994fddf59fb 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -667,6 +667,11 @@ Für das Feature „Vom Ende ausgehende Slicing“ ist Sprachversion „Vorschau“ erforderlich. + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' Ungültige Direktive "#{0} {1}" diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index 81b95e31980..a15f0745a17 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -667,6 +667,11 @@ La característica "desde el final del recorte" requiere la versión de lenguaje "preview" (vista previa). + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' Directiva '#{0} {1}' no válida. diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index 09fa4eee455..b2c55368f57 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -667,6 +667,11 @@ La fonctionnalité « from the end slicing » nécessite la version de langage « preview ». + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' Directive non valide '#{0} {1}' diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index f2a1d4bd1f8..191763862c0 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -667,6 +667,11 @@ La funzionalità 'sezionamento dalla fine' richiede la versione del linguaggio 'anteprima'. + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' Direttiva '#{0} {1}' non valida diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index bdda682c3f1..5f17d8079de 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -667,6 +667,11 @@ 'from the end slicing' (最後からのスライス) 機能には、言語バージョン 'preview' が必要です。 + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' 無効なディレクティブ '#{0} {1}' diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index d14f780060f..0d33760ceba 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -667,6 +667,11 @@ '끝에서부터 조각화' 기능을 사용하려면 언어 버전 '미리 보기'가 필요합니다. + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' 잘못된 지시문 '#{0} {1}' diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 79b65f951b3..431ca9ffa9a 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -667,6 +667,11 @@ Funkcja „Przycinanie od końca” wymaga „podglądu” wersji językowej. + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' Nieprawidłowa dyrektywa „#{0} {1}” diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index d01eeffa3ee..af35009a5bd 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -667,6 +667,11 @@ O recurso 'da divisão final' requer a versão de idioma 'preview'. + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' Diretriz inválida '#{0} {1}' diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index 7fec1c621d2..ca29b65cb8f 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -667,6 +667,11 @@ Для функции конечного среза требуется "предварительная" версия языка. + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' Недопустимая директива "#{0} {1}" diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index 5b5ae8760e7..fce045a8f03 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -667,6 +667,11 @@ 'Uçtan dilimleme' özelliği, 'önizleme' dil sürümünü gerektirir. + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' Geçersiz yönerge '#{0} {1}' diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index f93ff12bced..87a649b8951 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -667,6 +667,11 @@ “从末尾切片”功能需要语言版本“预览”。 + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' 无效的指令“#{0} {1}” diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index 801fa62942d..4c32c4faadd 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -667,6 +667,11 @@ 「從末端分割」功能需要語言版本「預覽」。 + + Invalid embedded ILLink.Substitutions.xml: {0} + Invalid embedded ILLink.Substitutions.xml: {0} + + Invalid directive '#{0} {1}' 無效的指示詞 '#{0} {1}' diff --git a/src/FSharp.Build/GenerateILLinkSubstitutions.fs b/src/FSharp.Build/GenerateILLinkSubstitutions.fs index 0478d36201d..17a05de6e79 100644 --- a/src/FSharp.Build/GenerateILLinkSubstitutions.fs +++ b/src/FSharp.Build/GenerateILLinkSubstitutions.fs @@ -5,6 +5,7 @@ namespace FSharp.Build open System open System.IO open System.Text +open System.Xml.Linq open Microsoft.Build.Framework open Microsoft.Build.Utilities @@ -32,8 +33,31 @@ type GenerateILLinkSubstitutions() = [] member val GeneratedItems = [||]: ITaskItem[] with get, set + member val EmbeddedResources = [||]: ITaskItem[] with get, set + + [] + member val ReplacedItems = [||]: ITaskItem[] with get, set + override this.Execute() = try + let substitutionsName = "ILLink.Substitutions.xml" + + let existing = + this.EmbeddedResources + |> Array.filter (fun item -> + let logicalName = item.GetMetadata("LogicalName") + + let name = + if logicalName = "" then + item.GetMetadata("ManifestResourceName") + else + logicalName + + String.Equals(name, substitutionsName, StringComparison.OrdinalIgnoreCase)) + + if existing.Length > 1 then + invalidOp $"Only one embedded {substitutionsName} resource is supported." + // Define the resource prefixes that need to be removed let resourcePrefixes = [| @@ -62,31 +86,51 @@ type GenerateILLinkSubstitutions() = let sb = StringBuilder(4096) // pre-allocate capacity sb.AppendLine("") |> ignore sb.AppendLine("") |> ignore - sb.AppendLine($" ") |> ignore + let assemblyName = System.Security.SecurityElement.Escape this.AssemblyName + sb.AppendLine($" ") |> ignore // Add each resource entry with proper closing tag on the same line for prefix in resourcePrefixes do - sb.AppendLine($" ") + sb.AppendLine($" ") |> ignore // Close assembly and linker tags sb.AppendLine(" ") |> ignore sb.AppendLine("") |> ignore - let xmlContent = sb.ToString() + let xmlContent = + match existing with + | [| resource |] -> + let document = XElement.Load(resource.ItemSpec, LoadOptions.PreserveWhitespace) + + if document.Name <> XName.Get "linker" then + invalidOp $"{resource.ItemSpec} must have a root element." + + document.Add(XElement.Parse(sb.ToString()).Elements()) + document.ToString(SaveOptions.DisableFormatting) + | _ -> sb.ToString() // Create a file in the intermediate output path - let outputFileName = - Path.Combine(this.IntermediateOutputPath, "ILLink.Substitutions.xml") + let outputFileName = Path.Combine(this.IntermediateOutputPath, substitutionsName) Directory.CreateDirectory(this.IntermediateOutputPath) |> ignore - File.WriteAllText(outputFileName, xmlContent) + + if + not (File.Exists outputFileName) + || File.ReadAllText(outputFileName) <> xmlContent + then + File.WriteAllText(outputFileName, xmlContent) // Create a TaskItem for the generated file - let item = TaskItem(outputFileName) :> ITaskItem - item.SetMetadata("LogicalName", "ILLink.Substitutions.xml") + let item = TaskItem(outputFileName) + + if existing.Length = 1 then + existing[0].CopyMetadataTo(item) + + item.SetMetadata("LogicalName", substitutionsName) - this.GeneratedItems <- [| item |] + this.GeneratedItems <- [| item :> ITaskItem |] + this.ReplacedItems <- existing true with ex -> this.Log.LogErrorFromException(ex, true) diff --git a/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets b/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets index d47a2757012..5081517e838 100644 --- a/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets +++ b/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets @@ -210,12 +210,17 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and - + + IntermediateOutputPath="$(IntermediateOutputPath)" + EmbeddedResources="@(EmbeddedResource)"> + + + + + false + + + + + + + diff --git a/tests/AheadOfTime/Trimming/MetadataSubstitutions/MetadataSubstitutions.fsproj b/tests/AheadOfTime/Trimming/MetadataSubstitutions/MetadataSubstitutions.fsproj new file mode 100644 index 00000000000..1141326b43b --- /dev/null +++ b/tests/AheadOfTime/Trimming/MetadataSubstitutions/MetadataSubstitutions.fsproj @@ -0,0 +1,21 @@ + + + net9.0 + Exe + true + win-x64 + true + 3879 + --staticlink:MetadataLibrary + --standalone + false + $(DefineConstants);KEEP_METADATA + $(DefineConstants);STATIC_LINKED + + + + + + + + diff --git a/tests/AheadOfTime/Trimming/MetadataSubstitutions/Program.fs b/tests/AheadOfTime/Trimming/MetadataSubstitutions/Program.fs new file mode 100644 index 00000000000..f60d92a5a9b --- /dev/null +++ b/tests/AheadOfTime/Trimming/MetadataSubstitutions/Program.fs @@ -0,0 +1,25 @@ +open System +open System.Reflection +open MetadataSubstitutions + +[] +let main _ = + if Library.Value() < 2020 then failwith "Library was not executed" + +#if STATIC_LINKED + if typeof.Assembly <> Assembly.GetExecutingAssembly() then failwith "Library was not statically linked" +#endif + + for assembly in [ Assembly.GetExecutingAssembly(); typeof.Assembly ] |> List.distinct do + let names = assembly.GetManifestResourceNames() + if Array.contains "marker" names then failwith "User substitution was not applied" + let hasMetadata = + names |> Array.exists (fun name -> name.StartsWith("FSharpSignature", StringComparison.Ordinal) || name.StartsWith("FSharpOptimization", StringComparison.Ordinal)) +#if KEEP_METADATA + if not hasMetadata then failwith "Metadata stripping was not disabled" +#else + if hasMetadata then failwith "F# metadata was not stripped" +#endif + + printfn "All tests passed" + 0 diff --git a/tests/AheadOfTime/Trimming/MetadataSubstitutions/Substitutions.xml b/tests/AheadOfTime/Trimming/MetadataSubstitutions/Substitutions.xml new file mode 100644 index 00000000000..0af2378a599 --- /dev/null +++ b/tests/AheadOfTime/Trimming/MetadataSubstitutions/Substitutions.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/AheadOfTime/Trimming/check.ps1 b/tests/AheadOfTime/Trimming/check.ps1 index 240ff643de3..cec1aa9ca16 100644 --- a/tests/AheadOfTime/Trimming/check.ps1 +++ b/tests/AheadOfTime/Trimming/check.ps1 @@ -1,13 +1,15 @@ -function CheckTrim($root, $tfm, $outputfile, $expected_len, $callerLineNumber) { +function CheckTrim($root, $tfm, $outputfile, $expected_len, $callerLineNumber, $properties = @(), $caseName = "") { Write-Host "Publish and Execute: ${tfm} - ${root}" - Write-Host "Expecting ${expected_len} for ${outputfile}" + if ($null -ne $expected_len) { Write-Host "Expecting ${expected_len} for ${outputfile}" } + $logName = if ($caseName) { "${root}_${caseName}" } else { $root } + if ($caseName) { $properties += "-p:BaseIntermediateOutputPath=obj/$caseName/" } $errors = @() $scriptFile = $PSCommandPath $cwd = Get-Location Set-Location (Join-Path $PSScriptRoot "${root}") - $build_output = dotnet publish -restore -c release -f:$tfm "${root}.fsproj" -bl:"../../../../artifacts/log/Release/AheadOfTime/Trimming/${root}_${tfm}.binlog" + $build_output = dotnet publish -restore -c release -f:$tfm "${root}.fsproj" @properties -bl:"../../../../artifacts/log/Release/AheadOfTime/Trimming/${logName}_${tfm}.binlog" Set-Location ${cwd} if ($LASTEXITCODE -ne 0) { @@ -43,7 +45,7 @@ function CheckTrim($root, $tfm, $outputfile, $expected_len, $callerLineNumber) { { Write-Host "Actual ${tfm} - trimmed ${outputfile} length: ${file_len} Bytes (expected length is placeholder -1, update test with this actual value)" } - elseif ($file_len -ne $expected_len) + elseif ($null -ne $expected_len -and $file_len -ne $expected_len) { $errors += "Test failed with unexpected ${tfm} - trimmed ${outputfile} length: Expected ${expected_len} Bytes, Actual ${file_len} Bytes" Write-Host "##vso[task.logissue type=error;sourcepath=${scriptFile};linenumber=${callerLineNumber}]Trimmed ${outputfile} size mismatch for ${root}: Expected ${expected_len} Bytes, Actual ${file_len} Bytes. Either codegen or trimming logic have changed. Please investigate and update expected dll size or report an issue." @@ -71,7 +73,18 @@ $allErrors += CheckTrim -root "SelfContained_Trimming_Test" -tfm "net9.0" -outpu $allErrors += CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net9.0" -outputfile "StaticLinkedFSharpCore_Trimming_Test.dll" -expected_len 9174528 -callerLineNumber 71 # Check net9.0 trimmed assemblies with F# metadata resources removed -$allErrors += CheckTrim -root "FSharpMetadataResource_Trimming_Test" -tfm "net9.0" -outputfile "FSharpMetadataResource_Trimming_Test.dll" -expected_len 7613440 -callerLineNumber 74 +$allErrors += CheckTrim -root "FSharpMetadataResource_Trimming_Test" -tfm "net9.0" -outputfile "FSharpMetadataResource_Trimming_Test.dll" -expected_len 7607296 -callerLineNumber 74 + +# These cases assert resource presence and user substitutions in Program.fs rather than assembly size. +foreach ($case in @( + @{ Name = "app"; Properties = @("-p:UserSubstitutions=App") }, + @{ Name = "library"; Properties = @("-p:UserSubstitutions=Library", "-p:CompressMetadata=false") }, + @{ Name = "static"; Properties = @("-p:UserSubstitutions=Library", "-p:LinkMode=Static") }, + @{ Name = "standalone"; Properties = @("-p:UserSubstitutions=Library", "-p:LinkMode=Standalone") }, + @{ Name = "disabled"; Properties = @("-p:UserSubstitutions=Library", "-p:DisableILLinkSubstitutions=true") } +)) { + $allErrors += CheckTrim -root "MetadataSubstitutions" -tfm "net9.0" -outputfile "MetadataSubstitutions.dll" -expected_len $null -callerLineNumber $MyInvocation.ScriptLineNumber -properties $case.Properties -caseName $case.Name +} # Report all errors and exit with failure if any occurred if ($allErrors.Count -gt 0) { diff --git a/tests/FSharp.Compiler.ComponentTests/StaticLinking/StaticLinking.fs b/tests/FSharp.Compiler.ComponentTests/StaticLinking/StaticLinking.fs index 1378dca0a5b..c56facdcdfc 100644 --- a/tests/FSharp.Compiler.ComponentTests/StaticLinking/StaticLinking.fs +++ b/tests/FSharp.Compiler.ComponentTests/StaticLinking/StaticLinking.fs @@ -4,9 +4,23 @@ open System.IO open Xunit open FSharp.Test open FSharp.Test.Compiler +open TestFramework module StaticLinking = + [] + let ``invalid embedded substitutions report a compiler diagnostic`` () = + let directory = createTemporaryDirectory () + let resource = Path.Combine(directory.FullName, "ILLink.Substitutions.xml") + File.WriteAllText(resource, "") + + FSharp """printfn "Hello" """ + |> asExe + |> withOptions [ "--standalone"; $"--resource:{resource},ILLink.Substitutions.xml" ] + |> compile + |> shouldFail + |> withErrorCode 3916 + let myRecordLibrary = FSharp """ module First From 910a7f75ba01563921954d69e2dc9dd79afb0c7f Mon Sep 17 00:00:00 2001 From: perf-bundle Date: Fri, 11 Sep 2026 14:25:48 +0200 Subject: [PATCH 2/8] Link the metadata stripping release note to its pull request Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 994b4ea9d3e..bd71e6c5a5b 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -140,7 +140,7 @@ * Fix Go-to-Definition for provided constructors that lack `TypeProviderDefinitionLocationAttribute`. Navigation now falls back to the declaring type instead of silently failing. ([Issue #5538](https://github.com/dotnet/fsharp/issues/5538), [PR #19917](https://github.com/dotnet/fsharp/pull/19917)) * Render the declaring type for C#-style extension methods in overload-resolution error messages so the suggestion no longer falsely attributes the extension to the receiver type. ([Issue #9838](https://github.com/dotnet/fsharp/issues/9838), [PR #19925](https://github.com/dotnet/fsharp/pull/19925)) * Fix FSI pretty printing to distinguish anonymous records (`{| ... |}`) from nominal records (`{ ... }`). ([Issue #6116](https://github.com/dotnet/fsharp/issues/6116), [PR #19919](https://github.com/dotnet/fsharp/pull/19919)) -* Fix automatic removal of F# metadata resources during trimming, including projects with user substitution files or statically linked F# libraries. ([Issue #18591](https://github.com/dotnet/fsharp/issues/18591)) +* Fix automatic removal of F# metadata resources during trimming, including projects with user substitution files or statically linked F# libraries. ([Issue #18591](https://github.com/dotnet/fsharp/issues/18591), [PR #20527](https://github.com/dotnet/fsharp/pull/20527)) * Fix dot-completion after indexed expressions (`a.[0].Data.`, `a[0].Data.`, `[1;2].Length.`) returning unrelated global completions instead of expression-typings members. ([Issue #4966](https://github.com/dotnet/fsharp/issues/4966), [PR #19934](https://github.com/dotnet/fsharp/pull/19934)) * Quotations of `match s with "" -> _` no longer leak the `s <> null && s.Length = 0` lowering; the empty-string optimization moved from pattern-match compilation to the optimizer so quoted expressions keep `op_Equality(s, "")`. ([Issue #19873](https://github.com/dotnet/fsharp/issues/19873)) * Parser: recover on unfinished abstract members ([PR #20070](https://github.com/dotnet/fsharp/pull/20070)) From e9780a182a0d1f0be968b8ef0b55f3b5596b340d Mon Sep 17 00:00:00 2001 From: perf-bundle Date: Fri, 11 Sep 2026 16:44:20 +0200 Subject: [PATCH 3/8] Limit metadata stripping to existing ILLink publish wiring Remove the compiler XML parser, task changes, diagnostic and added fixture. Reuse GenerateILLinkSubstitutions at PrepareForILLink and pass its output as trimmer substitutions. Preserve standalone compilation and user resources without changing compiler code. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 2 +- src/Compiler/Driver/StaticLinking.fs | 103 +----------------- src/Compiler/FSComp.txt | 1 - src/Compiler/xlf/FSComp.txt.cs.xlf | 5 - src/Compiler/xlf/FSComp.txt.de.xlf | 5 - src/Compiler/xlf/FSComp.txt.es.xlf | 5 - src/Compiler/xlf/FSComp.txt.fr.xlf | 5 - src/Compiler/xlf/FSComp.txt.it.xlf | 5 - src/Compiler/xlf/FSComp.txt.ja.xlf | 5 - src/Compiler/xlf/FSComp.txt.ko.xlf | 5 - src/Compiler/xlf/FSComp.txt.pl.xlf | 5 - src/Compiler/xlf/FSComp.txt.pt-BR.xlf | 5 - src/Compiler/xlf/FSComp.txt.ru.xlf | 5 - src/Compiler/xlf/FSComp.txt.tr.xlf | 5 - src/Compiler/xlf/FSComp.txt.zh-Hans.xlf | 5 - src/Compiler/xlf/FSComp.txt.zh-Hant.xlf | 5 - .../GenerateILLinkSubstitutions.fs | 62 ++--------- .../Microsoft.FSharp.NetSdk.targets | 26 +++-- .../Directory.Build.props | 13 --- .../MetadataSubstitutions/Library/Library.fs | 5 - .../Library/MetadataLibrary.fsproj | 12 -- .../MetadataSubstitutions.fsproj | 21 ---- .../Trimming/MetadataSubstitutions/Program.fs | 25 ----- .../MetadataSubstitutions/Substitutions.xml | 3 - tests/AheadOfTime/Trimming/check.ps1 | 26 ++--- .../StaticLinking/StaticLinking.fs | 14 --- 26 files changed, 38 insertions(+), 340 deletions(-) delete mode 100644 tests/AheadOfTime/Trimming/MetadataSubstitutions/Directory.Build.props delete mode 100644 tests/AheadOfTime/Trimming/MetadataSubstitutions/Library/Library.fs delete mode 100644 tests/AheadOfTime/Trimming/MetadataSubstitutions/Library/MetadataLibrary.fsproj delete mode 100644 tests/AheadOfTime/Trimming/MetadataSubstitutions/MetadataSubstitutions.fsproj delete mode 100644 tests/AheadOfTime/Trimming/MetadataSubstitutions/Program.fs delete mode 100644 tests/AheadOfTime/Trimming/MetadataSubstitutions/Substitutions.xml diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 6ad580619c9..3d2df7fe1f4 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -141,7 +141,7 @@ * Fix Go-to-Definition for provided constructors that lack `TypeProviderDefinitionLocationAttribute`. Navigation now falls back to the declaring type instead of silently failing. ([Issue #5538](https://github.com/dotnet/fsharp/issues/5538), [PR #19917](https://github.com/dotnet/fsharp/pull/19917)) * Render the declaring type for C#-style extension methods in overload-resolution error messages so the suggestion no longer falsely attributes the extension to the receiver type. ([Issue #9838](https://github.com/dotnet/fsharp/issues/9838), [PR #19925](https://github.com/dotnet/fsharp/pull/19925)) * Fix FSI pretty printing to distinguish anonymous records (`{| ... |}`) from nominal records (`{ ... }`). ([Issue #6116](https://github.com/dotnet/fsharp/issues/6116), [PR #19919](https://github.com/dotnet/fsharp/pull/19919)) -* Fix automatic removal of F# metadata resources during trimming, including projects with user substitution files or statically linked F# libraries. ([Issue #18591](https://github.com/dotnet/fsharp/issues/18591), [PR #20527](https://github.com/dotnet/fsharp/pull/20527)) +* Remove F# metadata from ordinary application and library assemblies when publishing a trimmed F# project. ([Issue #18591](https://github.com/dotnet/fsharp/issues/18591), [PR #20527](https://github.com/dotnet/fsharp/pull/20527)) * Fix dot-completion after indexed expressions (`a.[0].Data.`, `a[0].Data.`, `[1;2].Length.`) returning unrelated global completions instead of expression-typings members. ([Issue #4966](https://github.com/dotnet/fsharp/issues/4966), [PR #19934](https://github.com/dotnet/fsharp/pull/19934)) * Quotations of `match s with "" -> _` no longer leak the `s <> null && s.Length = 0` lowering; the empty-string optimization moved from pattern-match compilation to the optimizer so quoted expressions keep `op_Equality(s, "")`. ([Issue #19873](https://github.com/dotnet/fsharp/issues/19873)) * Parser: recover on unfinished abstract members ([PR #20070](https://github.com/dotnet/fsharp/pull/20070)) diff --git a/src/Compiler/Driver/StaticLinking.fs b/src/Compiler/Driver/StaticLinking.fs index 0f7bd5981ba..7379423579e 100644 --- a/src/Compiler/Driver/StaticLinking.fs +++ b/src/Compiler/Driver/StaticLinking.fs @@ -4,8 +4,6 @@ module internal FSharp.Compiler.StaticLinking open System -open System.Xml -open System.Xml.Linq open Internal.Utilities.Collections open Internal.Utilities.Library open Internal.Utilities.Library.Extras @@ -25,102 +23,6 @@ open FSharp.Compiler.TypedTree open FSharp.Compiler.TypeProviders #endif -let combineMetadataSubstitutions assemblyName linkedAssemblies (resources: ILResource list) = - let xname = XName.Get - - let attribute name (element: XElement) = - match element.Attribute(xname name) with - | null -> "" - | value -> value.Value - - let resourceNames = resources |> List.map _.Name |> Set.ofList - - let substitutions, others = - resources - |> List.partition (fun r -> - r.Name.Equals("ILLink.Substitutions.xml", StringComparison.OrdinalIgnoreCase) - && (match r.Location with - | ILResourceLocation.Local _ -> true - | _ -> false)) - - let documents = - substitutions - |> List.choose (fun resource -> - use stream = resource.GetBytes().AsStream() - - let document = - try - XElement.Load(stream, LoadOptions.PreserveWhitespace) - with :? XmlException as ex -> - error (Error(FSComp.SR.fscInvalidILLinkSubstitutions ex.Message, rangeStartup)) - - let isMetadataAssembly (element: XElement) = - element.Name = xname "assembly" - && (element.Attributes() |> Seq.forall (fun attr -> attr.Name = xname "fullname")) - && Set.contains (attribute "fullname" element) linkedAssemblies - && element.HasElements - && (element.Elements() - |> Seq.forall (fun node -> - let name = attribute "name" node - let namedResource = { resource with Name = name } - - node.Name = xname "resource" - && not node.HasElements - && (node.Attributes() - |> Seq.forall (fun attr -> attr.Name = xname "name" || attr.Name = xname "action")) - && attribute "action" node = "remove" - && (IsSignatureDataResource namedResource - || IsSignatureDataResourceB namedResource - || IsOptimizationDataResource namedResource - || IsOptimizationDataResourceB namedResource))) - - if document.Name = xname "linker" then - for element in document.Elements() |> Seq.filter isMetadataAssembly |> Seq.toArray do - for node in element.Elements() |> Seq.toArray do - if not (resourceNames.Contains(attribute "name" node)) then - node.Remove() - - if element.HasElements then - element.SetAttributeValue(xname "fullname", assemblyName) - else - element.Remove() - - if document.Name = xname "linker" && not document.HasElements then - None - else - Some( - resource, - document, - document.Name = xname "linker" - && not document.HasAttributes - && (document.Elements() |> Seq.forall isMetadataAssembly) - )) - - let metadata, custom = - documents |> List.partition (fun (_, _, metadata) -> metadata) - - let documents = - match custom @ metadata with - | [] -> [] - | (resource, document, _) :: rest -> - let toAppend, toKeep = rest |> List.partition (fun (_, _, metadata) -> metadata) - - for _, source, _ in toAppend do - document.Add(source.Elements() |> Seq.toArray) - - (resource, document) - :: (toKeep |> List.map (fun (resource, document, _) -> resource, document)) - - others - @ (documents - |> List.map (fun (resource, document) -> - { resource with - Location = - ILResourceLocation.Local( - ByteStorage.FromByteArray(System.Text.Encoding.UTF8.GetBytes(document.ToString(SaveOptions.DisableFormatting))) - ) - })) - // Handles TypeForwarding for the generated IL model type TypeForwarding(tcImports: TcImports) = @@ -346,10 +248,7 @@ let StaticLinkILModules ] ) TypeDefs = mkILTypeDefs (topTypeDef :: List.concat normalTypeDefs) - Resources = - savedResources @ ilxMainModule.Resources.AsList() - |> combineMetadataSubstitutions oldManifest.Name (assems.Add oldManifest.Name) - |> mkILResources + Resources = mkILResources (savedResources @ ilxMainModule.Resources.AsList()) NativeResources = savedNativeResources } diff --git a/src/Compiler/FSComp.txt b/src/Compiler/FSComp.txt index 9ecdf6eccdc..a50d902136b 100644 --- a/src/Compiler/FSComp.txt +++ b/src/Compiler/FSComp.txt @@ -1817,4 +1817,3 @@ featureRecordSpreads,"record type and expression spreads" 3913,tcExtendedLayoutCannotBeUsedOnUnions,"The 'ExtendedLayoutAttribute' cannot be applied to discriminated unions" 3914,tcExtendedLayoutStructMustHaveInstanceField,"A struct with the 'ExtendedLayoutAttribute' must have at least one instance field" 3915,tcTupleTypeExtensionTooManyElements,"Tuple type extensions are supported only for tuples of up to 7 elements, but this tuple type has %d elements. Extensions of larger tuples are not supported." -3916,fscInvalidILLinkSubstitutions,"Invalid embedded ILLink.Substitutions.xml: %s" diff --git a/src/Compiler/xlf/FSComp.txt.cs.xlf b/src/Compiler/xlf/FSComp.txt.cs.xlf index 9c63ad2f998..36028a7a60c 100644 --- a/src/Compiler/xlf/FSComp.txt.cs.xlf +++ b/src/Compiler/xlf/FSComp.txt.cs.xlf @@ -667,11 +667,6 @@ Funkce vytváření průřezů od konce vyžaduje jazykovou verzi preview. - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' Neplatná direktiva #{0} {1} diff --git a/src/Compiler/xlf/FSComp.txt.de.xlf b/src/Compiler/xlf/FSComp.txt.de.xlf index 994fddf59fb..701a63c7a2b 100644 --- a/src/Compiler/xlf/FSComp.txt.de.xlf +++ b/src/Compiler/xlf/FSComp.txt.de.xlf @@ -667,11 +667,6 @@ Für das Feature „Vom Ende ausgehende Slicing“ ist Sprachversion „Vorschau“ erforderlich. - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' Ungültige Direktive "#{0} {1}" diff --git a/src/Compiler/xlf/FSComp.txt.es.xlf b/src/Compiler/xlf/FSComp.txt.es.xlf index a15f0745a17..81b95e31980 100644 --- a/src/Compiler/xlf/FSComp.txt.es.xlf +++ b/src/Compiler/xlf/FSComp.txt.es.xlf @@ -667,11 +667,6 @@ La característica "desde el final del recorte" requiere la versión de lenguaje "preview" (vista previa). - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' Directiva '#{0} {1}' no válida. diff --git a/src/Compiler/xlf/FSComp.txt.fr.xlf b/src/Compiler/xlf/FSComp.txt.fr.xlf index b2c55368f57..09fa4eee455 100644 --- a/src/Compiler/xlf/FSComp.txt.fr.xlf +++ b/src/Compiler/xlf/FSComp.txt.fr.xlf @@ -667,11 +667,6 @@ La fonctionnalité « from the end slicing » nécessite la version de langage « preview ». - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' Directive non valide '#{0} {1}' diff --git a/src/Compiler/xlf/FSComp.txt.it.xlf b/src/Compiler/xlf/FSComp.txt.it.xlf index 191763862c0..f2a1d4bd1f8 100644 --- a/src/Compiler/xlf/FSComp.txt.it.xlf +++ b/src/Compiler/xlf/FSComp.txt.it.xlf @@ -667,11 +667,6 @@ La funzionalità 'sezionamento dalla fine' richiede la versione del linguaggio 'anteprima'. - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' Direttiva '#{0} {1}' non valida diff --git a/src/Compiler/xlf/FSComp.txt.ja.xlf b/src/Compiler/xlf/FSComp.txt.ja.xlf index 5f17d8079de..bdda682c3f1 100644 --- a/src/Compiler/xlf/FSComp.txt.ja.xlf +++ b/src/Compiler/xlf/FSComp.txt.ja.xlf @@ -667,11 +667,6 @@ 'from the end slicing' (最後からのスライス) 機能には、言語バージョン 'preview' が必要です。 - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' 無効なディレクティブ '#{0} {1}' diff --git a/src/Compiler/xlf/FSComp.txt.ko.xlf b/src/Compiler/xlf/FSComp.txt.ko.xlf index 0d33760ceba..d14f780060f 100644 --- a/src/Compiler/xlf/FSComp.txt.ko.xlf +++ b/src/Compiler/xlf/FSComp.txt.ko.xlf @@ -667,11 +667,6 @@ '끝에서부터 조각화' 기능을 사용하려면 언어 버전 '미리 보기'가 필요합니다. - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' 잘못된 지시문 '#{0} {1}' diff --git a/src/Compiler/xlf/FSComp.txt.pl.xlf b/src/Compiler/xlf/FSComp.txt.pl.xlf index 431ca9ffa9a..79b65f951b3 100644 --- a/src/Compiler/xlf/FSComp.txt.pl.xlf +++ b/src/Compiler/xlf/FSComp.txt.pl.xlf @@ -667,11 +667,6 @@ Funkcja „Przycinanie od końca” wymaga „podglądu” wersji językowej. - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' Nieprawidłowa dyrektywa „#{0} {1}” diff --git a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf index af35009a5bd..d01eeffa3ee 100644 --- a/src/Compiler/xlf/FSComp.txt.pt-BR.xlf +++ b/src/Compiler/xlf/FSComp.txt.pt-BR.xlf @@ -667,11 +667,6 @@ O recurso 'da divisão final' requer a versão de idioma 'preview'. - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' Diretriz inválida '#{0} {1}' diff --git a/src/Compiler/xlf/FSComp.txt.ru.xlf b/src/Compiler/xlf/FSComp.txt.ru.xlf index ca29b65cb8f..7fec1c621d2 100644 --- a/src/Compiler/xlf/FSComp.txt.ru.xlf +++ b/src/Compiler/xlf/FSComp.txt.ru.xlf @@ -667,11 +667,6 @@ Для функции конечного среза требуется "предварительная" версия языка. - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' Недопустимая директива "#{0} {1}" diff --git a/src/Compiler/xlf/FSComp.txt.tr.xlf b/src/Compiler/xlf/FSComp.txt.tr.xlf index fce045a8f03..5b5ae8760e7 100644 --- a/src/Compiler/xlf/FSComp.txt.tr.xlf +++ b/src/Compiler/xlf/FSComp.txt.tr.xlf @@ -667,11 +667,6 @@ 'Uçtan dilimleme' özelliği, 'önizleme' dil sürümünü gerektirir. - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' Geçersiz yönerge '#{0} {1}' diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf index 87a649b8951..f93ff12bced 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hans.xlf @@ -667,11 +667,6 @@ “从末尾切片”功能需要语言版本“预览”。 - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' 无效的指令“#{0} {1}” diff --git a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf index 4c32c4faadd..801fa62942d 100644 --- a/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf +++ b/src/Compiler/xlf/FSComp.txt.zh-Hant.xlf @@ -667,11 +667,6 @@ 「從末端分割」功能需要語言版本「預覽」。 - - Invalid embedded ILLink.Substitutions.xml: {0} - Invalid embedded ILLink.Substitutions.xml: {0} - - Invalid directive '#{0} {1}' 無效的指示詞 '#{0} {1}' diff --git a/src/FSharp.Build/GenerateILLinkSubstitutions.fs b/src/FSharp.Build/GenerateILLinkSubstitutions.fs index 17a05de6e79..0478d36201d 100644 --- a/src/FSharp.Build/GenerateILLinkSubstitutions.fs +++ b/src/FSharp.Build/GenerateILLinkSubstitutions.fs @@ -5,7 +5,6 @@ namespace FSharp.Build open System open System.IO open System.Text -open System.Xml.Linq open Microsoft.Build.Framework open Microsoft.Build.Utilities @@ -33,31 +32,8 @@ type GenerateILLinkSubstitutions() = [] member val GeneratedItems = [||]: ITaskItem[] with get, set - member val EmbeddedResources = [||]: ITaskItem[] with get, set - - [] - member val ReplacedItems = [||]: ITaskItem[] with get, set - override this.Execute() = try - let substitutionsName = "ILLink.Substitutions.xml" - - let existing = - this.EmbeddedResources - |> Array.filter (fun item -> - let logicalName = item.GetMetadata("LogicalName") - - let name = - if logicalName = "" then - item.GetMetadata("ManifestResourceName") - else - logicalName - - String.Equals(name, substitutionsName, StringComparison.OrdinalIgnoreCase)) - - if existing.Length > 1 then - invalidOp $"Only one embedded {substitutionsName} resource is supported." - // Define the resource prefixes that need to be removed let resourcePrefixes = [| @@ -86,51 +62,31 @@ type GenerateILLinkSubstitutions() = let sb = StringBuilder(4096) // pre-allocate capacity sb.AppendLine("") |> ignore sb.AppendLine("") |> ignore - let assemblyName = System.Security.SecurityElement.Escape this.AssemblyName - sb.AppendLine($" ") |> ignore + sb.AppendLine($" ") |> ignore // Add each resource entry with proper closing tag on the same line for prefix in resourcePrefixes do - sb.AppendLine($" ") + sb.AppendLine($" ") |> ignore // Close assembly and linker tags sb.AppendLine(" ") |> ignore sb.AppendLine("") |> ignore - let xmlContent = - match existing with - | [| resource |] -> - let document = XElement.Load(resource.ItemSpec, LoadOptions.PreserveWhitespace) - - if document.Name <> XName.Get "linker" then - invalidOp $"{resource.ItemSpec} must have a root element." - - document.Add(XElement.Parse(sb.ToString()).Elements()) - document.ToString(SaveOptions.DisableFormatting) - | _ -> sb.ToString() + let xmlContent = sb.ToString() // Create a file in the intermediate output path - let outputFileName = Path.Combine(this.IntermediateOutputPath, substitutionsName) + let outputFileName = + Path.Combine(this.IntermediateOutputPath, "ILLink.Substitutions.xml") Directory.CreateDirectory(this.IntermediateOutputPath) |> ignore - - if - not (File.Exists outputFileName) - || File.ReadAllText(outputFileName) <> xmlContent - then - File.WriteAllText(outputFileName, xmlContent) + File.WriteAllText(outputFileName, xmlContent) // Create a TaskItem for the generated file - let item = TaskItem(outputFileName) - - if existing.Length = 1 then - existing[0].CopyMetadataTo(item) - - item.SetMetadata("LogicalName", substitutionsName) + let item = TaskItem(outputFileName) :> ITaskItem + item.SetMetadata("LogicalName", "ILLink.Substitutions.xml") - this.GeneratedItems <- [| item :> ITaskItem |] - this.ReplacedItems <- existing + this.GeneratedItems <- [| item |] true with ex -> this.Log.LogErrorFromException(ex, true) diff --git a/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets b/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets index 5081517e838..086f213d7cd 100644 --- a/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets +++ b/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets @@ -209,17 +209,27 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and - - + + + + + <_FSharpAssembliesToStrip Include="@(ManagedAssemblyToLink)" Exclude="@(RuntimePackAsset)" + Condition="'%(Filename)' != 'FSharp.Core'" /> + - - + AssemblyName="%(_FSharpAssembliesToStrip.Filename)" + IntermediateOutputPath="$(IntermediateOutputPath)FSharpILLink/%(_FSharpAssembliesToStrip.Filename)" + Condition="'$(DisableILLinkSubstitutions)' != 'true' and '@(_FSharpAssembliesToStrip)' != ''"> + + + $(MSBuildAllProjects);$(IntermediateOutputPath)FSharpILLink.cache + <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) @(_FSharpILLinkSubstitutions->'--substitutions "%(Identity)"', ' ') + - + diff --git a/tests/AheadOfTime/Trimming/MetadataSubstitutions/Directory.Build.props b/tests/AheadOfTime/Trimming/MetadataSubstitutions/Directory.Build.props deleted file mode 100644 index bd1e60ae98f..00000000000 --- a/tests/AheadOfTime/Trimming/MetadataSubstitutions/Directory.Build.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - $(LocalFSharpBuildBinPath)/FSharp.Build.dll - $(LocalFSharpBuildBinPath)/fsc.dll - $(LocalFSharpBuildBinPath)/fsc.dll - true - - - - - - diff --git a/tests/AheadOfTime/Trimming/MetadataSubstitutions/Library/Library.fs b/tests/AheadOfTime/Trimming/MetadataSubstitutions/Library/Library.fs deleted file mode 100644 index ea9e5128e79..00000000000 --- a/tests/AheadOfTime/Trimming/MetadataSubstitutions/Library/Library.fs +++ /dev/null @@ -1,5 +0,0 @@ -namespace MetadataSubstitutions - -type Library = - [] - static member Value() = System.DateTime.UtcNow.Year diff --git a/tests/AheadOfTime/Trimming/MetadataSubstitutions/Library/MetadataLibrary.fsproj b/tests/AheadOfTime/Trimming/MetadataSubstitutions/Library/MetadataLibrary.fsproj deleted file mode 100644 index 9145bad00e4..00000000000 --- a/tests/AheadOfTime/Trimming/MetadataSubstitutions/Library/MetadataLibrary.fsproj +++ /dev/null @@ -1,12 +0,0 @@ - - - net9.0 - - false - - - - - - - diff --git a/tests/AheadOfTime/Trimming/MetadataSubstitutions/MetadataSubstitutions.fsproj b/tests/AheadOfTime/Trimming/MetadataSubstitutions/MetadataSubstitutions.fsproj deleted file mode 100644 index 1141326b43b..00000000000 --- a/tests/AheadOfTime/Trimming/MetadataSubstitutions/MetadataSubstitutions.fsproj +++ /dev/null @@ -1,21 +0,0 @@ - - - net9.0 - Exe - true - win-x64 - true - 3879 - --staticlink:MetadataLibrary - --standalone - false - $(DefineConstants);KEEP_METADATA - $(DefineConstants);STATIC_LINKED - - - - - - - - diff --git a/tests/AheadOfTime/Trimming/MetadataSubstitutions/Program.fs b/tests/AheadOfTime/Trimming/MetadataSubstitutions/Program.fs deleted file mode 100644 index f60d92a5a9b..00000000000 --- a/tests/AheadOfTime/Trimming/MetadataSubstitutions/Program.fs +++ /dev/null @@ -1,25 +0,0 @@ -open System -open System.Reflection -open MetadataSubstitutions - -[] -let main _ = - if Library.Value() < 2020 then failwith "Library was not executed" - -#if STATIC_LINKED - if typeof.Assembly <> Assembly.GetExecutingAssembly() then failwith "Library was not statically linked" -#endif - - for assembly in [ Assembly.GetExecutingAssembly(); typeof.Assembly ] |> List.distinct do - let names = assembly.GetManifestResourceNames() - if Array.contains "marker" names then failwith "User substitution was not applied" - let hasMetadata = - names |> Array.exists (fun name -> name.StartsWith("FSharpSignature", StringComparison.Ordinal) || name.StartsWith("FSharpOptimization", StringComparison.Ordinal)) -#if KEEP_METADATA - if not hasMetadata then failwith "Metadata stripping was not disabled" -#else - if hasMetadata then failwith "F# metadata was not stripped" -#endif - - printfn "All tests passed" - 0 diff --git a/tests/AheadOfTime/Trimming/MetadataSubstitutions/Substitutions.xml b/tests/AheadOfTime/Trimming/MetadataSubstitutions/Substitutions.xml deleted file mode 100644 index 0af2378a599..00000000000 --- a/tests/AheadOfTime/Trimming/MetadataSubstitutions/Substitutions.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/tests/AheadOfTime/Trimming/check.ps1 b/tests/AheadOfTime/Trimming/check.ps1 index cec1aa9ca16..c17e9aece41 100644 --- a/tests/AheadOfTime/Trimming/check.ps1 +++ b/tests/AheadOfTime/Trimming/check.ps1 @@ -1,15 +1,13 @@ -function CheckTrim($root, $tfm, $outputfile, $expected_len, $callerLineNumber, $properties = @(), $caseName = "") { +function CheckTrim($root, $tfm, $outputfile, $expected_len, $callerLineNumber) { Write-Host "Publish and Execute: ${tfm} - ${root}" - if ($null -ne $expected_len) { Write-Host "Expecting ${expected_len} for ${outputfile}" } - $logName = if ($caseName) { "${root}_${caseName}" } else { $root } - if ($caseName) { $properties += "-p:BaseIntermediateOutputPath=obj/$caseName/" } + Write-Host "Expecting ${expected_len} for ${outputfile}" $errors = @() $scriptFile = $PSCommandPath $cwd = Get-Location Set-Location (Join-Path $PSScriptRoot "${root}") - $build_output = dotnet publish -restore -c release -f:$tfm "${root}.fsproj" @properties -bl:"../../../../artifacts/log/Release/AheadOfTime/Trimming/${logName}_${tfm}.binlog" + $build_output = dotnet publish -restore -c release -f:$tfm "${root}.fsproj" -bl:"../../../../artifacts/log/Release/AheadOfTime/Trimming/${root}_${tfm}.binlog" Set-Location ${cwd} if ($LASTEXITCODE -ne 0) { @@ -40,12 +38,17 @@ function CheckTrim($root, $tfm, $outputfile, $expected_len, $callerLineNumber, $ # Checking that the trimmed outputfile binary is of expected size (needs adjustments if test is updated). $file = Get-Item (Join-Path $PSScriptRoot "${root}\bin\release\${tfm}\win-x64\publish\${outputfile}") + $metadataResources = [System.Reflection.Assembly]::LoadFile($file.FullName).GetManifestResourceNames() | + Where-Object { $_ -match '^FSharp(Signature|Optimization)' } + if ($metadataResources) { + $errors += "F# metadata resources remain in ${outputfile}: $($metadataResources -join ', ')" + } $file_len = $file.Length if ($expected_len -eq -1) { Write-Host "Actual ${tfm} - trimmed ${outputfile} length: ${file_len} Bytes (expected length is placeholder -1, update test with this actual value)" } - elseif ($null -ne $expected_len -and $file_len -ne $expected_len) + elseif ($file_len -ne $expected_len) { $errors += "Test failed with unexpected ${tfm} - trimmed ${outputfile} length: Expected ${expected_len} Bytes, Actual ${file_len} Bytes" Write-Host "##vso[task.logissue type=error;sourcepath=${scriptFile};linenumber=${callerLineNumber}]Trimmed ${outputfile} size mismatch for ${root}: Expected ${expected_len} Bytes, Actual ${file_len} Bytes. Either codegen or trimming logic have changed. Please investigate and update expected dll size or report an issue." @@ -75,17 +78,6 @@ $allErrors += CheckTrim -root "StaticLinkedFSharpCore_Trimming_Test" -tfm "net9. # Check net9.0 trimmed assemblies with F# metadata resources removed $allErrors += CheckTrim -root "FSharpMetadataResource_Trimming_Test" -tfm "net9.0" -outputfile "FSharpMetadataResource_Trimming_Test.dll" -expected_len 7607296 -callerLineNumber 74 -# These cases assert resource presence and user substitutions in Program.fs rather than assembly size. -foreach ($case in @( - @{ Name = "app"; Properties = @("-p:UserSubstitutions=App") }, - @{ Name = "library"; Properties = @("-p:UserSubstitutions=Library", "-p:CompressMetadata=false") }, - @{ Name = "static"; Properties = @("-p:UserSubstitutions=Library", "-p:LinkMode=Static") }, - @{ Name = "standalone"; Properties = @("-p:UserSubstitutions=Library", "-p:LinkMode=Standalone") }, - @{ Name = "disabled"; Properties = @("-p:UserSubstitutions=Library", "-p:DisableILLinkSubstitutions=true") } -)) { - $allErrors += CheckTrim -root "MetadataSubstitutions" -tfm "net9.0" -outputfile "MetadataSubstitutions.dll" -expected_len $null -callerLineNumber $MyInvocation.ScriptLineNumber -properties $case.Properties -caseName $case.Name -} - # Report all errors and exit with failure if any occurred if ($allErrors.Count -gt 0) { Write-Host "" diff --git a/tests/FSharp.Compiler.ComponentTests/StaticLinking/StaticLinking.fs b/tests/FSharp.Compiler.ComponentTests/StaticLinking/StaticLinking.fs index c56facdcdfc..1378dca0a5b 100644 --- a/tests/FSharp.Compiler.ComponentTests/StaticLinking/StaticLinking.fs +++ b/tests/FSharp.Compiler.ComponentTests/StaticLinking/StaticLinking.fs @@ -4,23 +4,9 @@ open System.IO open Xunit open FSharp.Test open FSharp.Test.Compiler -open TestFramework module StaticLinking = - [] - let ``invalid embedded substitutions report a compiler diagnostic`` () = - let directory = createTemporaryDirectory () - let resource = Path.Combine(directory.FullName, "ILLink.Substitutions.xml") - File.WriteAllText(resource, "") - - FSharp """printfn "Hello" """ - |> asExe - |> withOptions [ "--standalone"; $"--resource:{resource},ILLink.Substitutions.xml" ] - |> compile - |> shouldFail - |> withErrorCode 3916 - let myRecordLibrary = FSharp """ module First From 2a9dd154011bf30e53130957cec8579d045d0d33 Mon Sep 17 00:00:00 2001 From: perf-bundle Date: Fri, 11 Sep 2026 21:04:11 +0200 Subject: [PATCH 4/8] Clarify substitution inputs and opt-out tracking Keep descriptor generation and trimmer arguments together, remove unnecessary XML quote escaping, and explain the separate input needed when the opt-out changes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.FSharp.NetSdk.targets | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets b/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets index 086f213d7cd..365a7852220 100644 --- a/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets +++ b/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets @@ -209,27 +209,32 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and - + - - - <_FSharpAssembliesToStrip Include="@(ManagedAssemblyToLink)" Exclude="@(RuntimePackAsset)" - Condition="'%(Filename)' != 'FSharp.Core'" /> + + <_FSharpTrimAssemblies Include="@(ManagedAssemblyToLink)" Exclude="@(RuntimePackAsset)" + Condition="'%(Filename)' != 'FSharp.Core'" /> + - + AssemblyName="%(_FSharpTrimAssemblies.Filename)" + IntermediateOutputPath="$(IntermediateOutputPath)FSharpILLink/%(_FSharpTrimAssemblies.Filename)" + Condition="'@(_FSharpTrimAssemblies)' != ''"> + + + <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) @(_FSharpSubstitutionFiles->'--substitutions "%(Identity)"', ' ') + + + + $(MSBuildAllProjects);$(IntermediateOutputPath)FSharpILLink.cache - <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) @(_FSharpILLinkSubstitutions->'--substitutions "%(Identity)"', ' ') - + From 6dfda15913a59257782480945de9277c6e1ad1fd Mon Sep 17 00:00:00 2001 From: perf-bundle Date: Tue, 15 Sep 2026 12:09:17 +0200 Subject: [PATCH 5/8] Restore producer-owned F# metadata trimming rules Embed defaults before compiler resource inputs are collected and let custom descriptors take precedence. Combine only unconditional metadata-removal documents during static linking, including already-combined producer assemblies, and preserve bounded mapped resource payloads. Consolidate affected test duplication while retaining quotation and reflection coverage; add direct and chained descriptor composition checks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../.FSharp.Compiler.Service/11.0.100.md | 2 +- src/Compiler/Driver/CompilerImports.fsi | 2 + src/Compiler/Driver/ILLinkSubstitutions.fs | 21 ++++ src/Compiler/Driver/StaticLinking.fs | 119 +++++++++++------- src/Compiler/FSharp.Compiler.Service.fsproj | 1 + src/Compiler/Utilities/FileSystem.fs | 4 +- src/FSharp.Build/FSharp.Build.fsproj | 1 + .../GenerateILLinkSubstitutions.fs | 60 ++------- .../Microsoft.FSharp.NetSdk.targets | 33 ++--- .../CustomSubstitutions.xml | 3 + .../SelfContained_Trimming_Test.fsproj | 2 + tests/AheadOfTime/Trimming/check.ps1 | 10 +- .../StaticLinking/StaticLinking.fs | 61 +++++---- .../ByteMemoryTests.fs | 14 ++- .../CodeGen/EmittedIL/StaticLinkTests.fs | 102 ++++----------- 15 files changed, 208 insertions(+), 227 deletions(-) create mode 100644 src/Compiler/Driver/ILLinkSubstitutions.fs create mode 100644 tests/AheadOfTime/Trimming/SelfContained_Trimming_Test/CustomSubstitutions.xml diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 1d2912c8cc1..2deaa7b51e6 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -8,6 +8,7 @@ * 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)) * 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)) * 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)) +* 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)) * 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)) * 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)) * Fix state machine lowering dropping the side-effectful receiver of an unused unit-typed member access (e.g. inside `task { (effectful()).UnitProp }`). ([Issue #13099](https://github.com/dotnet/fsharp/issues/13099), [PR #19885](https://github.com/dotnet/fsharp/pull/19885)) @@ -142,7 +143,6 @@ * Fix Go-to-Definition for provided constructors that lack `TypeProviderDefinitionLocationAttribute`. Navigation now falls back to the declaring type instead of silently failing. ([Issue #5538](https://github.com/dotnet/fsharp/issues/5538), [PR #19917](https://github.com/dotnet/fsharp/pull/19917)) * Render the declaring type for C#-style extension methods in overload-resolution error messages so the suggestion no longer falsely attributes the extension to the receiver type. ([Issue #9838](https://github.com/dotnet/fsharp/issues/9838), [PR #19925](https://github.com/dotnet/fsharp/pull/19925)) * Fix FSI pretty printing to distinguish anonymous records (`{| ... |}`) from nominal records (`{ ... }`). ([Issue #6116](https://github.com/dotnet/fsharp/issues/6116), [PR #19919](https://github.com/dotnet/fsharp/pull/19919)) -* Remove F# metadata from ordinary application and library assemblies when publishing a trimmed F# project. ([Issue #18591](https://github.com/dotnet/fsharp/issues/18591), [PR #20527](https://github.com/dotnet/fsharp/pull/20527)) * Fix dot-completion after indexed expressions (`a.[0].Data.`, `a[0].Data.`, `[1;2].Length.`) returning unrelated global completions instead of expression-typings members. ([Issue #4966](https://github.com/dotnet/fsharp/issues/4966), [PR #19934](https://github.com/dotnet/fsharp/pull/19934)) * Quotations of `match s with "" -> _` no longer leak the `s <> null && s.Length = 0` lowering; the empty-string optimization moved from pattern-match compilation to the optimizer so quoted expressions keep `op_Equality(s, "")`. ([Issue #19873](https://github.com/dotnet/fsharp/issues/19873)) * Parser: recover on unfinished abstract members ([PR #20070](https://github.com/dotnet/fsharp/pull/20070)) diff --git a/src/Compiler/Driver/CompilerImports.fsi b/src/Compiler/Driver/CompilerImports.fsi index 5cf0f846e25..7b8875cb989 100644 --- a/src/Compiler/Driver/CompilerImports.fsi +++ b/src/Compiler/Driver/CompilerImports.fsi @@ -45,6 +45,8 @@ val IsOptimizationDataResource: ILResource -> bool /// Determine if an IL resource attached to an F# assembly is an F# optimization data resource (data stream B) val IsOptimizationDataResourceB: ILResource -> bool +val GetNameOfILModule: ILModuleDef -> string + /// Determine if an IL resource attached to an F# assembly is an F# quotation data resource for reflected definitions val IsReflectedDefinitionsResource: ILResource -> bool diff --git a/src/Compiler/Driver/ILLinkSubstitutions.fs b/src/Compiler/Driver/ILLinkSubstitutions.fs new file mode 100644 index 00000000000..6e91416d304 --- /dev/null +++ b/src/Compiler/Driver/ILLinkSubstitutions.fs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module internal FSharp.Compiler.ILLinkSubstitutions + +open System.Xml.Linq + +let names (assemblyName: string) = + seq { + for kind in [ "Signature"; "Optimization" ] do + for suffix in [ "Data"; "CompressedData"; "DataB"; "CompressedDataB"; "Info" ] do + yield $"FSharp{kind}{suffix}.{assemblyName}" + } + +let document (assemblyName: string) (names: seq) = + let x = XName.Get + let assembly = XElement(x "assembly", XAttribute(x "fullname", assemblyName)) + + for name in names do + assembly.Add(XElement(x "resource", XAttribute(x "name", name), XAttribute(x "action", "remove"), "")) + + XElement(x "linker", assembly) diff --git a/src/Compiler/Driver/StaticLinking.fs b/src/Compiler/Driver/StaticLinking.fs index 7379423579e..ae0ffb1d03a 100644 --- a/src/Compiler/Driver/StaticLinking.fs +++ b/src/Compiler/Driver/StaticLinking.fs @@ -4,6 +4,7 @@ module internal FSharp.Compiler.StaticLinking open System +open System.Xml.Linq open Internal.Utilities.Collections open Internal.Utilities.Library open Internal.Utilities.Library.Extras @@ -23,6 +24,57 @@ open FSharp.Compiler.TypedTree open FSharp.Compiler.TypeProviders #endif +let linkMetadataResources assemblyName (resources: (string * ILResource) list) = + let xname = XName.Get + + let generated, retained = + resources + |> List.splitChoose (fun (owner, resource) -> + match resource.Location with + | ILResourceLocation.Local _ when resource.Name.Equals("ILLink.Substitutions.xml", StringComparison.OrdinalIgnoreCase) -> + try + use stream = resource.GetBytes().AsStream() + let xml = XElement.Load stream + let prefixes = ILLinkSubstitutions.names "" |> Seq.toList + + let names = + [ + for rule in xml.Descendants(xname "resource") do + if rule.IsEmpty then + rule.Value <- "" + + for name in rule.Attributes(xname "name") do + if List.exists name.Value.StartsWithOrdinal prefixes then + name.Value + ] + + match resource.CustomAttrs.AsList(), XNode.DeepEquals(xml, ILLinkSubstitutions.document owner names) with + | [], true -> Choice1Of2(resource, names) + | _ -> Choice2Of2(true, resource) + with :? System.Xml.XmlException -> + Choice2Of2(true, resource) + | _ -> Choice2Of2(false, resource)) + + let resources = List.map snd retained + + match generated with + | (resource, _) :: _ when not (List.exists fst retained) -> + let present = resources |> List.map _.Name |> Set.ofList + + let names = + generated |> List.collect snd |> List.filter present.Contains |> List.distinct + + resources + @ [ + if not names.IsEmpty then + let xml = ILLinkSubstitutions.document assemblyName names + + { resource with + Location = ILResourceLocation.Local(ByteStorage.FromByteArray(System.Text.Encoding.UTF8.GetBytes(xml.ToString()))) + } + ] + | _ -> resources + // Handles TypeForwarding for the generated IL model type TypeForwarding(tcImports: TcImports) = @@ -157,51 +209,30 @@ let StaticLinkILModules ] let savedResources = - let allResources = - [ - for ccu, m in dependentILModules do - for r in m.Resources.AsList() do - (ccu, r) - ] - // Don't save interface, optimization or resource definitions for provider-generated assemblies. - // These are "fake". - let isProvided (ccu: CcuThunk option) = + [ + for ccu, m in dependentILModules do + let provided = #if !NO_TYPEPROVIDERS - match ccu with - | Some c -> c.IsProviderGenerated - | None -> false + ccu |> Option.exists _.IsProviderGenerated #else - ignore ccu - false + ignore ccu + false #endif + for r in m.Resources.AsList() do + let order, enabled = + if IsSignatureDataResource r || IsSignatureDataResourceB r then + 0, tcConfig.GenerateSignatureData + elif IsOptimizationDataResource r || IsOptimizationDataResourceB r then + 1, tcConfig.GenerateOptimizationData + else + 2, true - // Save only the interface/optimization attributes of generated data - let intfDataResources, others = - allResources - |> List.partition (fun (_, r) -> IsSignatureDataResource r || IsSignatureDataResourceB r) - - let intfDataResources = - [ - for ccu, r in intfDataResources do - if tcConfig.GenerateSignatureData && not (isProvided ccu) then - r - ] - - let optDataResources, others = - others - |> List.partition (fun (_, r) -> IsOptimizationDataResource r || IsOptimizationDataResourceB r) - - let optDataResources = - [ - for ccu, r in optDataResources do - if tcConfig.GenerateOptimizationData && not (isProvided ccu) then - r - ] - - let otherResources = others |> List.map snd - - let result = intfDataResources @ optDataResources @ otherResources - result + if enabled && (order = 2 || not provided) then + yield order, (GetNameOfILModule m, r) + ] + |> List.groupBy fst + |> List.sortBy fst + |> List.collect (snd >> List.map snd) let moduls = ilxMainModule :: (List.map snd dependentILModules) @@ -248,7 +279,11 @@ let StaticLinkILModules ] ) TypeDefs = mkILTypeDefs (topTypeDef :: List.concat normalTypeDefs) - Resources = mkILResources (savedResources @ ilxMainModule.Resources.AsList()) + Resources = + savedResources + @ (ilxMainModule.Resources.AsList() |> List.map (fun r -> oldManifest.Name, r)) + |> linkMetadataResources oldManifest.Name + |> mkILResources NativeResources = savedNativeResources } diff --git a/src/Compiler/FSharp.Compiler.Service.fsproj b/src/Compiler/FSharp.Compiler.Service.fsproj index b274796ff49..ee9b164011b 100644 --- a/src/Compiler/FSharp.Compiler.Service.fsproj +++ b/src/Compiler/FSharp.Compiler.Service.fsproj @@ -534,6 +534,7 @@ + diff --git a/src/Compiler/Utilities/FileSystem.fs b/src/Compiler/Utilities/FileSystem.fs index 5fd055b525d..d9464a96fae 100644 --- a/src/Compiler/Utilities/FileSystem.fs +++ b/src/Compiler/Utilities/FileSystem.fs @@ -1092,9 +1092,11 @@ type ByteStorage(getByteMemory: unit -> ReadOnlyByteMemory) = static member FromByteMemory(bytes: ReadOnlyByteMemory) = ByteStorage(fun () -> bytes) static member FromByteMemoryAndCopy(bytes: ReadOnlyByteMemory, useBackingMemoryMappedFile: bool) = + let length = bytes.Length + if useBackingMemoryMappedFile then match MemoryMappedFile.TryFromByteMemory(bytes) with - | Some mmf -> ByteStorage(fun () -> ByteMemory.FromMemoryMappedFile(mmf).AsReadOnly()) + | Some mmf -> ByteStorage(fun () -> ByteMemory.FromMemoryMappedFile(mmf).Slice(0, length).AsReadOnly()) | _ -> let copiedBytes = ByteMemory.FromArray(bytes.ToArray()).AsReadOnly() ByteStorage.FromByteMemory(copiedBytes) diff --git a/src/FSharp.Build/FSharp.Build.fsproj b/src/FSharp.Build/FSharp.Build.fsproj index 90912e95fe2..a66d30ea404 100644 --- a/src/FSharp.Build/FSharp.Build.fsproj +++ b/src/FSharp.Build/FSharp.Build.fsproj @@ -46,6 +46,7 @@ + diff --git a/src/FSharp.Build/GenerateILLinkSubstitutions.fs b/src/FSharp.Build/GenerateILLinkSubstitutions.fs index 0478d36201d..46c77b8eedb 100644 --- a/src/FSharp.Build/GenerateILLinkSubstitutions.fs +++ b/src/FSharp.Build/GenerateILLinkSubstitutions.fs @@ -2,9 +2,8 @@ namespace FSharp.Build -open System open System.IO -open System.Text +open FSharp.Compiler open Microsoft.Build.Framework open Microsoft.Build.Utilities @@ -34,58 +33,25 @@ type GenerateILLinkSubstitutions() = override this.Execute() = try - // Define the resource prefixes that need to be removed - let resourcePrefixes = - [| - // Signature variants - yield! - [| - for dataType in [| "Data"; "DataB" |] do - for compression in [| ""; "Compressed" |] do - yield $"FSharpSignature{compression}{dataType}" - |] + let xmlContent = + ILLinkSubstitutions.document this.AssemblyName (ILLinkSubstitutions.names this.AssemblyName) + |> string - // Optimization variants - yield! - [| - for dataType in [| "Data"; "DataB" |] do - for compression in [| ""; "Compressed" |] do - yield $"FSharpOptimization{compression}{dataType}" - |] - - // Info variants - yield "FSharpOptimizationInfo" - yield "FSharpSignatureInfo" - |] - - // Generate the XML content - let sb = StringBuilder(4096) // pre-allocate capacity - sb.AppendLine("") |> ignore - sb.AppendLine("") |> ignore - sb.AppendLine($" ") |> ignore - - // Add each resource entry with proper closing tag on the same line - for prefix in resourcePrefixes do - sb.AppendLine($" ") - |> ignore - - // Close assembly and linker tags - sb.AppendLine(" ") |> ignore - sb.AppendLine("") |> ignore - - let xmlContent = sb.ToString() - - // Create a file in the intermediate output path let outputFileName = Path.Combine(this.IntermediateOutputPath, "ILLink.Substitutions.xml") Directory.CreateDirectory(this.IntermediateOutputPath) |> ignore - File.WriteAllText(outputFileName, xmlContent) - // Create a TaskItem for the generated file - let item = TaskItem(outputFileName) :> ITaskItem - item.SetMetadata("LogicalName", "ILLink.Substitutions.xml") + if + not (File.Exists outputFileName) + || File.ReadAllText(outputFileName) <> xmlContent + then + File.WriteAllText(outputFileName, xmlContent) + let item = TaskItem(outputFileName.Replace("%", "%25")) :> ITaskItem + item.SetMetadata("LogicalName", "ILLink.Substitutions.xml") + item.SetMetadata("Type", "Non-Resx") + item.SetMetadata("WithCulture", "false") this.GeneratedItems <- [| item |] true with ex -> diff --git a/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets b/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets index 365a7852220..0a450e38698 100644 --- a/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets +++ b/src/FSharp.Build/Microsoft.FSharp.NetSdk.targets @@ -209,33 +209,16 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and - - - - <_FSharpTrimAssemblies Include="@(ManagedAssemblyToLink)" Exclude="@(RuntimePackAsset)" - Condition="'%(Filename)' != 'FSharp.Core'" /> - - + + - + AssemblyName="$(AssemblyName)" + IntermediateOutputPath="$(IntermediateOutputPath)"> + - - <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) @(_FSharpSubstitutionFiles->'--substitutions "%(Identity)"', ' ') - - - - - - $(MSBuildAllProjects);$(IntermediateOutputPath)FSharpILLink.cache - - - -