|
| 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 |
0 commit comments