Skip to content

Commit

Permalink
Generate source for .resx files on build. (#3607)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
brettfo authored Sep 22, 2017
1 parent b48f924 commit e6d46b9
Show file tree
Hide file tree
Showing 48 changed files with 400 additions and 442 deletions.
1 change: 1 addition & 0 deletions src/FSharpSource.Profiles.targets
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<DefineConstants>$(DefineConstants);FX_RESHAPED_MSBUILD</DefineConstants>
<DefineConstants>$(DefineConstants);FSI_TODO_NETCORE</DefineConstants>
<OtherFlags>$(OtherFlags) --simpleresolution</OtherFlags>
<TargetFramework Condition="'$(TargetFramework)' == ''">netstandard1.6</TargetFramework>
</PropertyGroup>

</Project>
20 changes: 17 additions & 3 deletions src/FSharpSource.targets
Original file line number Diff line number Diff line change
Expand Up @@ -360,22 +360,36 @@
<UsingTask TaskName="FSharpEmbedResourceText"
AssemblyFile="$(FSharpSourcesRoot)\..\Proto\net40\bin\FSharp.Build-proto.dll"
Condition="'$(Configuration)' != 'Proto'" />
<UsingTask TaskName="FSharpEmbedResXSource"
AssemblyFile="$(FSharpSourcesRoot)\..\Proto\net40\bin\FSharp.Build-proto.dll"
Condition="'$(Configuration)' != 'Proto'" />

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

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

<!-- Generate source for all resx files. -->
<FSharpEmbedResXSource EmbeddedResource="@(EmbeddedResource)" IntermediateOutputPath="$(IntermediateOutputPath)" TargetFramework="$(TargetFramework)">
<Output TaskParameter="GeneratedSource" ItemName="_FsGeneratedResXSource" />
</FSharpEmbedResXSource>

<ItemGroup>
<CompileBefore Include="@(_FsGeneratedResXSource)" />
<FileWrites Include="@(_FsGeneratedResXSource)" />
</ItemGroup>

<!-- Generate resx and source for all txt files. -->
<FSharpEmbedResourceText EmbeddedText="@(EmbeddedText)" IntermediateOutputPath="$(IntermediateOutputPath)">
<Output TaskParameter="GeneratedSource" ItemName="_FsGeneratedSource" />
<Output TaskParameter="GeneratedSource" ItemName="_FsGeneratedTxtSource" />
<Output TaskParameter="GeneratedResx" ItemName="_FsGeneratedResx" />
</FSharpEmbedResourceText>

<ItemGroup>
<CompileBefore Include="@(_FsGeneratedSource)" />
<CompileBefore Include="@(_FsGeneratedTxtSource)" />
<EmbeddedResource Include="@(_FsGeneratedResx)" />
<FileWrites Include="@(_FsGeneratedSource)" />
<FileWrites Include="@(_FsGeneratedTxtSource)" />
<FileWrites Include="@(_FsGeneratedResx)" />
</ItemGroup>

Expand Down
4 changes: 4 additions & 0 deletions src/fsharp/FSharp.Build-proto/FSharp.Build-proto.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
<Compile Include="..\FSharp.Build\FSharpEmbedResourceText.fs">
<Link>FSharpEmbedResourceText.fs</Link>
</Compile>
<Compile Include="..\FSharp.Build\FSharpEmbedResXSource.fs">
<Link>FSharpEmbedResXSource.fs</Link>
</Compile>
<CopyAndSubstituteText Include="..\FSharp.Build\Microsoft.FSharp.Targets">
<Link>Microsoft.FSharp-proto.targets</Link>
<TargetFilename>Microsoft.FSharp-proto.targets</TargetFilename>
Expand Down Expand Up @@ -67,6 +70,7 @@
<Reference Include="System" />
<Reference Include="System.Numerics" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetDotnetProfile)' != 'coreclr' AND '$(MonoPackaging)' != 'true' ">
<Reference Include="Microsoft.Build.Framework, Version=$(VisualStudioVersion).0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<Compile Include="Fsc.fsi" />
<Compile Include="Fsc.fs" />
<Compile Include="FSharpEmbedResourceText.fs" />
<Compile Include="FSharpEmbedResXSource.fs" />
<Compile Include="CreateFSharpManifestResourceName.fsi" />
<Compile Include="CreateFSharpManifestResourceName.fs" />
<CopyAndSubstituteText Include="Microsoft.FSharp.Targets">
Expand Down
2 changes: 2 additions & 0 deletions src/fsharp/FSharp.Build/FSharp.Build.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<Compile Include="Fsc.fsi" />
<Compile Include="Fsc.fs" />
<Compile Include="FSharpEmbedResourceText.fs" />
<Compile Include="FSharpEmbedResXSource.fs" />
<Compile Include="CreateFSharpManifestResourceName.fsi" />
<Compile Include="CreateFSharpManifestResourceName.fs" />
<CopyAndSubstituteText Include="Microsoft.FSharp.Targets">
Expand Down Expand Up @@ -59,6 +60,7 @@
<Reference Include="mscorlib" />
<Reference Include="System" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetDotnetProfile)' != 'coreclr' AND '$(MonoPackaging)' != 'true' ">
<Reference Include="Microsoft.Build.Framework, Version=$(VisualStudioVersion).0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
Expand Down
146 changes: 146 additions & 0 deletions src/fsharp/FSharp.Build/FSharpEmbedResXSource.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace Microsoft.FSharp.Build

open System
open System.Collections
open System.Globalization
open System.IO
open System.Linq
open System.Text
open System.Xml.Linq
open Microsoft.Build.Framework
open Microsoft.Build.Utilities

type FSharpEmbedResXSource() =
let mutable _buildEngine : IBuildEngine = null
let mutable _hostObject : ITaskHost = null
let mutable _embeddedText : ITaskItem[] = [||]
let mutable _generatedSource : ITaskItem[] = [||]
let mutable _outputPath : string = ""
let mutable _targetFramework : string = ""

let boilerplate = @"// <auto-generated>
namespace {0}
open System.Reflection
module internal {1} =
type private C (_dummy:System.Object) = class end
let mutable Culture = System.Globalization.CultureInfo.CurrentUICulture
let ResourceManager = new System.Resources.ResourceManager(""{2}"", C(null).GetType().GetTypeInfo().Assembly)
let GetString(name:System.String) : System.String = ResourceManager.GetString(name, Culture)"

let boilerplateGetObject = " let GetObject(name:System.String) : System.Object = ResourceManager.GetObject(name, Culture)"

let generateSource (resx:string) (fullModuleName:string) (generateLegacy:bool) (generateLiteral:bool) =
try
let printMessage = printfn "FSharpEmbedResXSource: %s"
let justFileName = Path.GetFileNameWithoutExtension(resx)
let sourcePath = Path.Combine(_outputPath, justFileName + ".fs")

// simple up-to-date check
if File.Exists(resx) && File.Exists(sourcePath) &&
File.GetLastWriteTime(resx) <= File.GetLastWriteTime(sourcePath) then
printMessage (sprintf "Skipping generation: '%s' since it is up-to-date." sourcePath)
Some(sourcePath)
else
let namespaceName, moduleName =
let parts = fullModuleName.Split('.')
if parts.Length = 1 then ("global", parts.[0])
else (String.Join(".", parts, 0, parts.Length - 1), parts.[parts.Length - 1])
let generateGetObject = not (_targetFramework.StartsWith("netstandard1.") || _targetFramework.StartsWith("netcoreapp1."))
printMessage (sprintf "Generating code for target framework %s" _targetFramework)
let sb = StringBuilder().AppendLine(String.Format(boilerplate, namespaceName, moduleName, justFileName))
if generateGetObject then sb.AppendLine(boilerplateGetObject) |> ignore
printMessage <| sprintf "Generating: %s" sourcePath
let body =
let xname = XName.op_Implicit
XDocument.Load(resx).Descendants(xname "data")
|> Seq.fold (fun (sb:StringBuilder) (node:XElement) ->
let name =
match node.Attribute(xname "name") with
| null -> failwith (sprintf "Missing resource name on element '%s'" (node.ToString()))
| attr -> attr.Value
let docComment =
match node.Elements(xname "value").FirstOrDefault() with
| null -> failwith <| sprintf "Missing resource value for '%s'" name
| element -> element.Value.Trim()
let identifier = if Char.IsLetter(name.[0]) || name.[0] = '_' then name else "_" + name
let commentBody =
XElement(xname "summary", docComment).ToString().Split([|"\r\n"; "\r"; "\n"|], StringSplitOptions.None)
|> Array.fold (fun (sb:StringBuilder) line -> sb.AppendLine(" /// " + line)) (StringBuilder())
// add the resource
let accessorBody =
match (generateLegacy, generateLiteral) with
| (true, true) -> sprintf " [<Literal>]\n let %s = \"%s\"" identifier name
| (true, false) -> sprintf " let %s = \"%s\"" identifier name // the [<Literal>] attribute can't be used for FSharp.Core
| (false, _) ->
let isStringResource = match node.Attribute(xname "type") with
| null -> true
| _ -> false
match (isStringResource, generateGetObject) with
| (true, _) -> sprintf " let %s() = GetString(\"%s\")" identifier name
| (false, true) -> sprintf " let %s() = GetObject(\"%s\")" identifier name
| (false, false) -> "" // the target runtime doesn't support non-string resources
// TODO: When calling the `GetObject` version, parse the `type` attribute to discover the proper return type
sb.AppendLine().Append(commentBody).AppendLine(accessorBody)
) sb
File.WriteAllText(sourcePath, body.ToString())
printMessage <| sprintf "Done: %s" sourcePath
Some(sourcePath)
with e ->
printf "An exception occurred when processing '%s'\n%s" resx (e.ToString())
None

[<Required>]
member this.EmbeddedResource
with get() = _embeddedText
and set(value) = _embeddedText <- value

[<Required>]
member this.IntermediateOutputPath
with get() = _outputPath
and set(value) = _outputPath <- value

member this.TargetFramework
with get() = _targetFramework
and set(value) = _targetFramework <- value

[<Output>]
member this.GeneratedSource
with get() = _generatedSource

interface ITask with
member this.BuildEngine
with get() = _buildEngine
and set(value) = _buildEngine <- value
member this.HostObject
with get() = _hostObject
and set(value) = _hostObject <- value
member this.Execute() =
let getBooleanMetadata (metadataName:string) (defaultValue:bool) (item:ITaskItem) =
match item.GetMetadata(metadataName) with
| value when String.IsNullOrWhiteSpace(value) -> defaultValue
| value ->
match value.ToLowerInvariant() with
| "true" -> true
| "false" -> false
| _ -> failwith (sprintf "Expected boolean value for '%s' found '%s'" metadataName value)
let mutable success = true
let generatedSource =
[| for item in this.EmbeddedResource do
if getBooleanMetadata "GenerateSource" false item then
let moduleName =
match item.GetMetadata("GeneratedModuleName") with
| null -> Path.GetFileNameWithoutExtension(item.ItemSpec)
| value -> value
let generateLegacy = getBooleanMetadata "GenerateLegacyCode" false item
let generateLiteral = getBooleanMetadata "GenerateLiterals" true item
match generateSource item.ItemSpec moduleName generateLegacy generateLiteral with
| Some (source) -> yield TaskItem(source) :> ITaskItem
| None -> success <- false
|]
_generatedSource <- generatedSource
success
18 changes: 15 additions & 3 deletions src/fsharp/FSharp.Build/Microsoft.FSharp.Targets
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ this file.

<UsingTask TaskName="Fsc" AssemblyFile="FSharp.Build{BuildSuffix}.dll"/>
<UsingTask TaskName="FSharpEmbedResourceText" AssemblyFile="FSharp.Build{BuildSuffix}.dll"/>
<UsingTask TaskName="FSharpEmbedResXSource" AssemblyFile="FSharp.Build{BuildSuffix}.dll"/>
<UsingTask TaskName="CreateFSharpManifestResourceName" AssemblyFile="FSharp.Build{BuildSuffix}.dll"/>

<PropertyGroup>
Expand Down Expand Up @@ -175,15 +176,26 @@ this file.

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

<!-- Generate source for all resx files. -->
<FSharpEmbedResXSource EmbeddedResource="@(EmbeddedResource)" IntermediateOutputPath="$(IntermediateOutputPath)" TargetFramework="$(TargetFramework)">
<Output TaskParameter="GeneratedSource" ItemName="_FsGeneratedResXSource" />
</FSharpEmbedResXSource>

<ItemGroup>
<CompileBefore Include="@(_FsGeneratedResXSource)" />
<FileWrites Include="@(_FsGeneratedResXSource)" />
</ItemGroup>

<!-- Generate resx and source for all txt files. -->
<FSharpEmbedResourceText EmbeddedText="@(EmbeddedText)" IntermediateOutputPath="$(IntermediateOutputPath)">
<Output TaskParameter="GeneratedSource" ItemName="_FsGeneratedSource" />
<Output TaskParameter="GeneratedSource" ItemName="_FsGeneratedTxtSource" />
<Output TaskParameter="GeneratedResx" ItemName="_FsGeneratedResx" />
</FSharpEmbedResourceText>

<ItemGroup>
<CompileBefore Include="@(_FsGeneratedSource)" />
<CompileBefore Include="@(_FsGeneratedTxtSource)" />
<EmbeddedResource Include="@(_FsGeneratedResx)" />
<FileWrites Include="@(_FsGeneratedSource)" />
<FileWrites Include="@(_FsGeneratedTxtSource)" />
<FileWrites Include="@(_FsGeneratedResx)" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion src/fsharp/FSharp.Core/FSCore.resx
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@
<data name="QtypeArgumentOutOfRange" xml:space="preserve">
<value>type argument out of range</value>
</data>
<data name="ThisValueCannotBeMutated" xml:space="preserve">
<data name="thisValueCannotBeMutated" xml:space="preserve">
<value>This value cannot be mutated</value>
</data>
<data name="optionValueWasNone" xml:space="preserve">
Expand Down
18 changes: 11 additions & 7 deletions src/fsharp/FSharp.Core/FSharp.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,20 @@
</ItemGroup>

<ItemGroup>
<CompileBefore Include="prim-types-prelude.fsi">
<Link>Primitives/prim-types-prelude.fsi</Link>
</CompileBefore>
<CompileBefore Include="prim-types-prelude.fs">
<Link>Primitives/prim-types-prelude.fs</Link>
</CompileBefore>
<EmbeddedResource Include="FSCore.resx">
<GenerateSource>true</GenerateSource>
<GenerateLegacyCode>true</GenerateLegacyCode>
<GenerateLiterals>false</GenerateLiterals>
<GeneratedModuleName>Microsoft.FSharp.Core.SR</GeneratedModuleName>
<Link>FSCore.resx</Link>
</EmbeddedResource>
<Compile Include="prim-types-prelude.fsi">
<Link>Primitives/prim-types-prelude.fsi</Link>
</Compile>
<Compile Include="prim-types-prelude.fs">
<Link>Primitives/prim-types-prelude.fs</Link>
</Compile>
<Compile Include="SR.fs">
<Compile Include="SR.fs" Condition="'$(Configuration)' == 'Proto'">
<Link>Primitives/SR.fs</Link>
</Compile>
<Compile Include="prim-types.fsi">
Expand Down
24 changes: 12 additions & 12 deletions src/fsharp/FSharp.Core/Query.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1418,41 +1418,41 @@ module Query =
TransInnerResult.Source(expr), NoConv

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

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

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

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

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

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

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

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

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

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

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

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


Expand Down
7 changes: 0 additions & 7 deletions src/fsharp/FSharp.Core/SR.fs
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,3 @@ module internal SR =

let GetString(name:System.String) : System.String =
resources.GetString(name, System.Globalization.CultureInfo.CurrentUICulture)
let GetString1(name:System.String, arg1:System.String) : System.String =
System.String.Format(resources.GetString(name, System.Globalization.CultureInfo.CurrentUICulture), arg1)
let GetString2(name:System.String, arg1:System.String, arg2:System.String) : System.String =
System.String.Format(resources.GetString(name, System.Globalization.CultureInfo.CurrentUICulture), arg1, arg2)
let GetString3(name:System.String, arg1:System.String, arg2:System.String, arg3:System.String) : System.String =
System.String.Format(resources.GetString(name, System.Globalization.CultureInfo.CurrentUICulture), arg1, arg2, arg3)

Loading

0 comments on commit e6d46b9

Please sign in to comment.