Skip to content

Commit e6d46b9

Browse files
authored
Generate source for .resx files on build. (#3607)
* add build task to generate *.fs from *.resx files * generate source for embedded resources in tests * generate source for embedded resources in FSharp.Editor * generate source for embedded resources in FSharp.LanguageService * generate source for embedded resources in FSharp.ProjectSystem.FSharp * generate source for embedded resources in FSharp.VS.FSI * don't generate non-string resources when <=netstandard1.6 * update baseline error message for tests The error output should be the exception message, not the exception type. * perform up-to-date check before generating *.fs from *.resx * remove non-idiomatic fold for an array comprehension * correct newline replacement * output more friendly error message * throw if boolean value isn't explicitly `true` or `false` * only generate object resource code on non `netstandard1.*` and `netcoreapp1.*` platforms * ensure FSharp.Core specifies a target framework for resource generaton * rename attributes to be non-ambiguous and properly include them
1 parent b48f924 commit e6d46b9

File tree

48 files changed

+400
-442
lines changed

Some content is hidden

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

48 files changed

+400
-442
lines changed

src/FSharpSource.Profiles.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
<DefineConstants>$(DefineConstants);FX_RESHAPED_MSBUILD</DefineConstants>
5757
<DefineConstants>$(DefineConstants);FSI_TODO_NETCORE</DefineConstants>
5858
<OtherFlags>$(OtherFlags) --simpleresolution</OtherFlags>
59+
<TargetFramework Condition="'$(TargetFramework)' == ''">netstandard1.6</TargetFramework>
5960
</PropertyGroup>
6061

6162
</Project>

src/FSharpSource.targets

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -360,22 +360,36 @@
360360
<UsingTask TaskName="FSharpEmbedResourceText"
361361
AssemblyFile="$(FSharpSourcesRoot)\..\Proto\net40\bin\FSharp.Build-proto.dll"
362362
Condition="'$(Configuration)' != 'Proto'" />
363+
<UsingTask TaskName="FSharpEmbedResXSource"
364+
AssemblyFile="$(FSharpSourcesRoot)\..\Proto\net40\bin\FSharp.Build-proto.dll"
365+
Condition="'$(Configuration)' != 'Proto'" />
363366

364367
<Target Name="GenerateFSharpTextResources"
365368
BeforeTargets="CoreResGen;PrepareForBuild"
366369
Condition="'$(Configuration)' != 'Proto'">
367370

368371
<MakeDir Directories="$(IntermediateOutputPath)" />
369372

373+
<!-- Generate source for all resx files. -->
374+
<FSharpEmbedResXSource EmbeddedResource="@(EmbeddedResource)" IntermediateOutputPath="$(IntermediateOutputPath)" TargetFramework="$(TargetFramework)">
375+
<Output TaskParameter="GeneratedSource" ItemName="_FsGeneratedResXSource" />
376+
</FSharpEmbedResXSource>
377+
378+
<ItemGroup>
379+
<CompileBefore Include="@(_FsGeneratedResXSource)" />
380+
<FileWrites Include="@(_FsGeneratedResXSource)" />
381+
</ItemGroup>
382+
383+
<!-- Generate resx and source for all txt files. -->
370384
<FSharpEmbedResourceText EmbeddedText="@(EmbeddedText)" IntermediateOutputPath="$(IntermediateOutputPath)">
371-
<Output TaskParameter="GeneratedSource" ItemName="_FsGeneratedSource" />
385+
<Output TaskParameter="GeneratedSource" ItemName="_FsGeneratedTxtSource" />
372386
<Output TaskParameter="GeneratedResx" ItemName="_FsGeneratedResx" />
373387
</FSharpEmbedResourceText>
374388

375389
<ItemGroup>
376-
<CompileBefore Include="@(_FsGeneratedSource)" />
390+
<CompileBefore Include="@(_FsGeneratedTxtSource)" />
377391
<EmbeddedResource Include="@(_FsGeneratedResx)" />
378-
<FileWrites Include="@(_FsGeneratedSource)" />
392+
<FileWrites Include="@(_FsGeneratedTxtSource)" />
379393
<FileWrites Include="@(_FsGeneratedResx)" />
380394
</ItemGroup>
381395

src/fsharp/FSharp.Build-proto/FSharp.Build-proto.fsproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
<Compile Include="..\FSharp.Build\FSharpEmbedResourceText.fs">
3838
<Link>FSharpEmbedResourceText.fs</Link>
3939
</Compile>
40+
<Compile Include="..\FSharp.Build\FSharpEmbedResXSource.fs">
41+
<Link>FSharpEmbedResXSource.fs</Link>
42+
</Compile>
4043
<CopyAndSubstituteText Include="..\FSharp.Build\Microsoft.FSharp.Targets">
4144
<Link>Microsoft.FSharp-proto.targets</Link>
4245
<TargetFilename>Microsoft.FSharp-proto.targets</TargetFilename>
@@ -67,6 +70,7 @@
6770
<Reference Include="System" />
6871
<Reference Include="System.Numerics" />
6972
<Reference Include="System.Xml" />
73+
<Reference Include="System.Xml.Linq" />
7074
</ItemGroup>
7175
<ItemGroup Condition=" '$(TargetDotnetProfile)' != 'coreclr' AND '$(MonoPackaging)' != 'true' ">
7276
<Reference Include="Microsoft.Build.Framework, Version=$(VisualStudioVersion).0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">

src/fsharp/FSharp.Build/FSharp.Build.BuildFromSource.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<Compile Include="Fsc.fsi" />
2323
<Compile Include="Fsc.fs" />
2424
<Compile Include="FSharpEmbedResourceText.fs" />
25+
<Compile Include="FSharpEmbedResXSource.fs" />
2526
<Compile Include="CreateFSharpManifestResourceName.fsi" />
2627
<Compile Include="CreateFSharpManifestResourceName.fs" />
2728
<CopyAndSubstituteText Include="Microsoft.FSharp.Targets">

src/fsharp/FSharp.Build/FSharp.Build.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<Compile Include="Fsc.fsi" />
3232
<Compile Include="Fsc.fs" />
3333
<Compile Include="FSharpEmbedResourceText.fs" />
34+
<Compile Include="FSharpEmbedResXSource.fs" />
3435
<Compile Include="CreateFSharpManifestResourceName.fsi" />
3536
<Compile Include="CreateFSharpManifestResourceName.fs" />
3637
<CopyAndSubstituteText Include="Microsoft.FSharp.Targets">
@@ -59,6 +60,7 @@
5960
<Reference Include="mscorlib" />
6061
<Reference Include="System" />
6162
<Reference Include="System.Xml" />
63+
<Reference Include="System.Xml.Linq" />
6264
</ItemGroup>
6365
<ItemGroup Condition=" '$(TargetDotnetProfile)' != 'coreclr' AND '$(MonoPackaging)' != 'true' ">
6466
<Reference Include="Microsoft.Build.Framework, Version=$(VisualStudioVersion).0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+
namespace Microsoft.FSharp.Build
4+
5+
open System
6+
open System.Collections
7+
open System.Globalization
8+
open System.IO
9+
open System.Linq
10+
open System.Text
11+
open System.Xml.Linq
12+
open Microsoft.Build.Framework
13+
open Microsoft.Build.Utilities
14+
15+
type FSharpEmbedResXSource() =
16+
let mutable _buildEngine : IBuildEngine = null
17+
let mutable _hostObject : ITaskHost = null
18+
let mutable _embeddedText : ITaskItem[] = [||]
19+
let mutable _generatedSource : ITaskItem[] = [||]
20+
let mutable _outputPath : string = ""
21+
let mutable _targetFramework : string = ""
22+
23+
let boilerplate = @"// <auto-generated>
24+
25+
namespace {0}
26+
27+
open System.Reflection
28+
29+
module internal {1} =
30+
type private C (_dummy:System.Object) = class end
31+
let mutable Culture = System.Globalization.CultureInfo.CurrentUICulture
32+
let ResourceManager = new System.Resources.ResourceManager(""{2}"", C(null).GetType().GetTypeInfo().Assembly)
33+
let GetString(name:System.String) : System.String = ResourceManager.GetString(name, Culture)"
34+
35+
let boilerplateGetObject = " let GetObject(name:System.String) : System.Object = ResourceManager.GetObject(name, Culture)"
36+
37+
let generateSource (resx:string) (fullModuleName:string) (generateLegacy:bool) (generateLiteral:bool) =
38+
try
39+
let printMessage = printfn "FSharpEmbedResXSource: %s"
40+
let justFileName = Path.GetFileNameWithoutExtension(resx)
41+
let sourcePath = Path.Combine(_outputPath, justFileName + ".fs")
42+
43+
// simple up-to-date check
44+
if File.Exists(resx) && File.Exists(sourcePath) &&
45+
File.GetLastWriteTime(resx) <= File.GetLastWriteTime(sourcePath) then
46+
printMessage (sprintf "Skipping generation: '%s' since it is up-to-date." sourcePath)
47+
Some(sourcePath)
48+
else
49+
let namespaceName, moduleName =
50+
let parts = fullModuleName.Split('.')
51+
if parts.Length = 1 then ("global", parts.[0])
52+
else (String.Join(".", parts, 0, parts.Length - 1), parts.[parts.Length - 1])
53+
let generateGetObject = not (_targetFramework.StartsWith("netstandard1.") || _targetFramework.StartsWith("netcoreapp1."))
54+
printMessage (sprintf "Generating code for target framework %s" _targetFramework)
55+
let sb = StringBuilder().AppendLine(String.Format(boilerplate, namespaceName, moduleName, justFileName))
56+
if generateGetObject then sb.AppendLine(boilerplateGetObject) |> ignore
57+
printMessage <| sprintf "Generating: %s" sourcePath
58+
let body =
59+
let xname = XName.op_Implicit
60+
XDocument.Load(resx).Descendants(xname "data")
61+
|> Seq.fold (fun (sb:StringBuilder) (node:XElement) ->
62+
let name =
63+
match node.Attribute(xname "name") with
64+
| null -> failwith (sprintf "Missing resource name on element '%s'" (node.ToString()))
65+
| attr -> attr.Value
66+
let docComment =
67+
match node.Elements(xname "value").FirstOrDefault() with
68+
| null -> failwith <| sprintf "Missing resource value for '%s'" name
69+
| element -> element.Value.Trim()
70+
let identifier = if Char.IsLetter(name.[0]) || name.[0] = '_' then name else "_" + name
71+
let commentBody =
72+
XElement(xname "summary", docComment).ToString().Split([|"\r\n"; "\r"; "\n"|], StringSplitOptions.None)
73+
|> Array.fold (fun (sb:StringBuilder) line -> sb.AppendLine(" /// " + line)) (StringBuilder())
74+
// add the resource
75+
let accessorBody =
76+
match (generateLegacy, generateLiteral) with
77+
| (true, true) -> sprintf " [<Literal>]\n let %s = \"%s\"" identifier name
78+
| (true, false) -> sprintf " let %s = \"%s\"" identifier name // the [<Literal>] attribute can't be used for FSharp.Core
79+
| (false, _) ->
80+
let isStringResource = match node.Attribute(xname "type") with
81+
| null -> true
82+
| _ -> false
83+
match (isStringResource, generateGetObject) with
84+
| (true, _) -> sprintf " let %s() = GetString(\"%s\")" identifier name
85+
| (false, true) -> sprintf " let %s() = GetObject(\"%s\")" identifier name
86+
| (false, false) -> "" // the target runtime doesn't support non-string resources
87+
// TODO: When calling the `GetObject` version, parse the `type` attribute to discover the proper return type
88+
sb.AppendLine().Append(commentBody).AppendLine(accessorBody)
89+
) sb
90+
File.WriteAllText(sourcePath, body.ToString())
91+
printMessage <| sprintf "Done: %s" sourcePath
92+
Some(sourcePath)
93+
with e ->
94+
printf "An exception occurred when processing '%s'\n%s" resx (e.ToString())
95+
None
96+
97+
[<Required>]
98+
member this.EmbeddedResource
99+
with get() = _embeddedText
100+
and set(value) = _embeddedText <- value
101+
102+
[<Required>]
103+
member this.IntermediateOutputPath
104+
with get() = _outputPath
105+
and set(value) = _outputPath <- value
106+
107+
member this.TargetFramework
108+
with get() = _targetFramework
109+
and set(value) = _targetFramework <- value
110+
111+
[<Output>]
112+
member this.GeneratedSource
113+
with get() = _generatedSource
114+
115+
interface ITask with
116+
member this.BuildEngine
117+
with get() = _buildEngine
118+
and set(value) = _buildEngine <- value
119+
member this.HostObject
120+
with get() = _hostObject
121+
and set(value) = _hostObject <- value
122+
member this.Execute() =
123+
let getBooleanMetadata (metadataName:string) (defaultValue:bool) (item:ITaskItem) =
124+
match item.GetMetadata(metadataName) with
125+
| value when String.IsNullOrWhiteSpace(value) -> defaultValue
126+
| value ->
127+
match value.ToLowerInvariant() with
128+
| "true" -> true
129+
| "false" -> false
130+
| _ -> failwith (sprintf "Expected boolean value for '%s' found '%s'" metadataName value)
131+
let mutable success = true
132+
let generatedSource =
133+
[| for item in this.EmbeddedResource do
134+
if getBooleanMetadata "GenerateSource" false item then
135+
let moduleName =
136+
match item.GetMetadata("GeneratedModuleName") with
137+
| null -> Path.GetFileNameWithoutExtension(item.ItemSpec)
138+
| value -> value
139+
let generateLegacy = getBooleanMetadata "GenerateLegacyCode" false item
140+
let generateLiteral = getBooleanMetadata "GenerateLiterals" true item
141+
match generateSource item.ItemSpec moduleName generateLegacy generateLiteral with
142+
| Some (source) -> yield TaskItem(source) :> ITaskItem
143+
| None -> success <- false
144+
|]
145+
_generatedSource <- generatedSource
146+
success

src/fsharp/FSharp.Build/Microsoft.FSharp.Targets

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ this file.
2323

2424
<UsingTask TaskName="Fsc" AssemblyFile="FSharp.Build{BuildSuffix}.dll"/>
2525
<UsingTask TaskName="FSharpEmbedResourceText" AssemblyFile="FSharp.Build{BuildSuffix}.dll"/>
26+
<UsingTask TaskName="FSharpEmbedResXSource" AssemblyFile="FSharp.Build{BuildSuffix}.dll"/>
2627
<UsingTask TaskName="CreateFSharpManifestResourceName" AssemblyFile="FSharp.Build{BuildSuffix}.dll"/>
2728

2829
<PropertyGroup>
@@ -175,15 +176,26 @@ this file.
175176

176177
<MakeDir Directories="$(IntermediateOutputPath)" />
177178

179+
<!-- Generate source for all resx files. -->
180+
<FSharpEmbedResXSource EmbeddedResource="@(EmbeddedResource)" IntermediateOutputPath="$(IntermediateOutputPath)" TargetFramework="$(TargetFramework)">
181+
<Output TaskParameter="GeneratedSource" ItemName="_FsGeneratedResXSource" />
182+
</FSharpEmbedResXSource>
183+
184+
<ItemGroup>
185+
<CompileBefore Include="@(_FsGeneratedResXSource)" />
186+
<FileWrites Include="@(_FsGeneratedResXSource)" />
187+
</ItemGroup>
188+
189+
<!-- Generate resx and source for all txt files. -->
178190
<FSharpEmbedResourceText EmbeddedText="@(EmbeddedText)" IntermediateOutputPath="$(IntermediateOutputPath)">
179-
<Output TaskParameter="GeneratedSource" ItemName="_FsGeneratedSource" />
191+
<Output TaskParameter="GeneratedSource" ItemName="_FsGeneratedTxtSource" />
180192
<Output TaskParameter="GeneratedResx" ItemName="_FsGeneratedResx" />
181193
</FSharpEmbedResourceText>
182194

183195
<ItemGroup>
184-
<CompileBefore Include="@(_FsGeneratedSource)" />
196+
<CompileBefore Include="@(_FsGeneratedTxtSource)" />
185197
<EmbeddedResource Include="@(_FsGeneratedResx)" />
186-
<FileWrites Include="@(_FsGeneratedSource)" />
198+
<FileWrites Include="@(_FsGeneratedTxtSource)" />
187199
<FileWrites Include="@(_FsGeneratedResx)" />
188200
</ItemGroup>
189201

src/fsharp/FSharp.Core/FSCore.resx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@
501501
<data name="QtypeArgumentOutOfRange" xml:space="preserve">
502502
<value>type argument out of range</value>
503503
</data>
504-
<data name="ThisValueCannotBeMutated" xml:space="preserve">
504+
<data name="thisValueCannotBeMutated" xml:space="preserve">
505505
<value>This value cannot be mutated</value>
506506
</data>
507507
<data name="optionValueWasNone" xml:space="preserve">

src/fsharp/FSharp.Core/FSharp.Core.fsproj

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,20 @@
5050
</ItemGroup>
5151

5252
<ItemGroup>
53+
<CompileBefore Include="prim-types-prelude.fsi">
54+
<Link>Primitives/prim-types-prelude.fsi</Link>
55+
</CompileBefore>
56+
<CompileBefore Include="prim-types-prelude.fs">
57+
<Link>Primitives/prim-types-prelude.fs</Link>
58+
</CompileBefore>
5359
<EmbeddedResource Include="FSCore.resx">
60+
<GenerateSource>true</GenerateSource>
61+
<GenerateLegacyCode>true</GenerateLegacyCode>
62+
<GenerateLiterals>false</GenerateLiterals>
63+
<GeneratedModuleName>Microsoft.FSharp.Core.SR</GeneratedModuleName>
5464
<Link>FSCore.resx</Link>
5565
</EmbeddedResource>
56-
<Compile Include="prim-types-prelude.fsi">
57-
<Link>Primitives/prim-types-prelude.fsi</Link>
58-
</Compile>
59-
<Compile Include="prim-types-prelude.fs">
60-
<Link>Primitives/prim-types-prelude.fs</Link>
61-
</Compile>
62-
<Compile Include="SR.fs">
66+
<Compile Include="SR.fs" Condition="'$(Configuration)' == 'Proto'">
6367
<Link>Primitives/SR.fs</Link>
6468
</Compile>
6569
<Compile Include="prim-types.fsi">

src/fsharp/FSharp.Core/Query.fs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,41 +1418,41 @@ module Query =
14181418
TransInnerResult.Source(expr), NoConv
14191419

14201420
| Call (_, meth, _) when check ->
1421-
raise (NotSupportedException (SR.GetString1(SR.unsupportedQueryCall,meth.ToString())))
1421+
raise (NotSupportedException (String.Format(SR.GetString(SR.unsupportedQueryCall),meth.ToString())))
14221422

14231423
| PropertyGet (_, pinfo, _) when check ->
1424-
raise (NotSupportedException (SR.GetString1(SR.unsupportedQueryProperty,pinfo.ToString())))
1424+
raise (NotSupportedException (String.Format(SR.GetString(SR.unsupportedQueryProperty),pinfo.ToString())))
14251425

14261426
| NewObject(ty,_) when check ->
1427-
raise (NotSupportedException (SR.GetString1(SR.unsupportedQueryConstructKind,"new " + ty.ToString())))
1427+
raise (NotSupportedException (String.Format(SR.GetString(SR.unsupportedQueryConstructKind),"new " + ty.ToString())))
14281428

14291429
| NewArray(ty,_) when check ->
1430-
raise (NotSupportedException (SR.GetString1(SR.unsupportedQueryConstructKind,"NewArray(" + ty.Name + ",...)")))
1430+
raise (NotSupportedException (String.Format(SR.GetString(SR.unsupportedQueryConstructKind),"NewArray(" + ty.Name + ",...)")))
14311431

14321432
| NewTuple _ when check ->
1433-
raise (NotSupportedException (SR.GetString1(SR.unsupportedQueryConstructKind,"NewTuple(...)")))
1433+
raise (NotSupportedException (String.Format(SR.GetString(SR.unsupportedQueryConstructKind),"NewTuple(...)")))
14341434

14351435
| FieldGet (_,field) when check ->
1436-
raise (NotSupportedException (SR.GetString1(SR.unsupportedQueryConstructKind,"FieldGet(" + field.Name + ",...)")))
1436+
raise (NotSupportedException (String.Format(SR.GetString(SR.unsupportedQueryConstructKind),"FieldGet(" + field.Name + ",...)")))
14371437

14381438
| LetRecursive _ when check ->
1439-
raise (NotSupportedException (SR.GetString1(SR.unsupportedQueryConstruct,"LetRecursive(...)")))
1439+
raise (NotSupportedException (String.Format(SR.GetString(SR.unsupportedQueryConstruct),"LetRecursive(...)")))
14401440

14411441
| NewRecord _ when check ->
1442-
raise (NotSupportedException (SR.GetString1(SR.unsupportedQueryConstruct,"NewRecord(...)")))
1442+
raise (NotSupportedException (String.Format(SR.GetString(SR.unsupportedQueryConstruct),"NewRecord(...)")))
14431443

14441444
| NewDelegate _ when check ->
1445-
raise (NotSupportedException (SR.GetString1(SR.unsupportedQueryConstruct,"NewDelegate(...)")))
1445+
raise (NotSupportedException (String.Format(SR.GetString(SR.unsupportedQueryConstruct),"NewDelegate(...)")))
14461446

14471447
| NewTuple _ when check ->
1448-
raise (NotSupportedException (SR.GetString1(SR.unsupportedQueryConstruct,"NewTuple(...)")))
1448+
raise (NotSupportedException (String.Format(SR.GetString(SR.unsupportedQueryConstruct),"NewTuple(...)")))
14491449

14501450
| NewUnionCase (ucase,_) when check ->
1451-
raise (NotSupportedException (SR.GetString1(SR.unsupportedQueryConstruct,"NewUnionCase(" + ucase.Name + "...)")))
1451+
raise (NotSupportedException (String.Format(SR.GetString(SR.unsupportedQueryConstruct),"NewUnionCase(" + ucase.Name + "...)")))
14521452

14531453
// Error cases
14541454
| e ->
1455-
if check then raise (NotSupportedException (SR.GetString1(SR.unsupportedQueryConstruct,immutQuery.ToString())))
1455+
if check then raise (NotSupportedException (String.Format(SR.GetString(SR.unsupportedQueryConstruct),immutQuery.ToString())))
14561456
else TransInnerResult.Source(e),NoConv
14571457

14581458

0 commit comments

Comments
 (0)