Xamarin.Android.FSharp.Reso.../ResourceTypeProvider.fs

196 строки
8.2 KiB
Forth

namespace Xamarin.Android.FSharp
open System
open System.IO
open System.Reflection
open System.CodeDom.Compiler
open System.Collections.Generic
open System.Xml.Linq
open FSharp.Core.CompilerServices
open FSharp.Quotations
open Microsoft.CSharp
[<TypeProvider>]
type ResourceProvider(config : TypeProviderConfig) =
let mutable providedAssembly = None
let invalidateEvent = Event<EventHandler,EventArgs>()
let isInsideIDE = config.IsInvalidationSupported // msbuild doesn't support invalidation
let isMsBuild = not isInsideIDE
let compiler = new CSharpCodeProvider()
let (/) a b = Path.Combine(a,b)
let pathToResources = config.ResolutionFolder/"Resources"
let resourceFileName = pathToResources/"Resource.designer.cs"
// watcher doesn't trigger when I specify the filename exactly
let watcher = new FileSystemWatcher(pathToResources, "*.cs", EnableRaisingEvents=true)
let asm = sprintf "ProvidedTypes%s.dll" (Guid.NewGuid() |> string)
let outputPath = config.TemporaryFolder/asm
let generate sourceCode =
let cp = CompilerParameters(
GenerateInMemory = false,
OutputAssembly = outputPath,
TempFiles = new TempFileCollection(config.TemporaryFolder, false),
CompilerOptions = "/nostdlib /noconfig")
let addRef ref =
cp.ReferencedAssemblies.Add ref |> ignore
let addReference assemblyFileName =
printfn "Adding reference %s" assemblyFileName
let reference =
config.ReferencedAssemblies |> Array.tryFind(fun r -> r.EndsWith(sprintf "%c%s" Path.DirectorySeparatorChar assemblyFileName, StringComparison.InvariantCultureIgnoreCase)
&& r.IndexOf("Facade") = -1)
match reference with
| Some ref -> addRef ref
| None -> printfn "Did not find %s in referenced assemblies." assemblyFileName
let version = Assembly.GetExecutingAssembly().GetName().Version
printfn "F# Android resource provider %A" version
addReference "System.dll"
addReference "mscorlib.dll"
config.ReferencedAssemblies
|> Array.iter addRef
if isInsideIDE then
printfn "Running inside IDE context"
else
printfn "Running inside MsBuild context"
addReference "Mono.Android.dll"
addReference "Xamarin.Android.NUnitLite.dll"
let result = compiler.CompileAssemblyFromSource(cp, [| sourceCode |])
if result.Errors.HasErrors then
let errors = [ for e in result.Errors do yield e ]
|> List.filter (fun e -> not e.IsWarning )
if errors.Length > 0 then
printfn "%s" sourceCode
printfn "%A" errors
failwithf "%A" errors
let asm = Assembly.ReflectionOnlyLoadFrom cp.OutputAssembly
let types = asm.GetTypes()
let namespaces =
let dict = Dictionary<_,List<_>>()
for t in types do
let namespc = if isNull t.Namespace then "global" else t.Namespace
match dict.TryGetValue(namespc) with
| true, ns -> ns.Add(t)
| _, _ ->
let ns = List<_>()
ns.Add(t)
dict.Add(namespc, ns)
dict
|> Seq.map (fun kv ->
{ new IProvidedNamespace with
member x.NamespaceName = kv.Key
member x.GetNestedNamespaces() = [||] //FIXME
member x.GetTypes() = kv.Value.ToArray()
member x.ResolveTypeName(typeName: string) = null
}
)
|> Seq.toArray
providedAssembly <- Some(File.ReadAllBytes(result.PathToAssembly), namespaces)
let invalidate _ =
printfn "Invalidating resources"
invalidateEvent.Trigger(null, null)
do
printfn "Resource folder %s" config.ResolutionFolder
printfn "Resource file name %s" resourceFileName
watcher.Changed.Add invalidate
watcher.Created.Add invalidate
AppDomain.CurrentDomain.add_ReflectionOnlyAssemblyResolve(fun _ args ->
let name = AssemblyName(args.Name)
printfn "Resolving %s" args.Name
let existingAssembly =
AppDomain.CurrentDomain.GetAssemblies()
|> Seq.tryFind(fun a -> AssemblyName.ReferenceMatchesDefinition(name, a.GetName()))
let asm =
match existingAssembly with
| Some a -> printfn "Resolved to %s" a.Location
Assembly.ReflectionOnlyLoadFrom a.Location
| None -> null
asm)
let getRootNamespace() =
// Try and guess what the namespace should be...
// This will work 99%+ of the time and if it
// doesn't, a build will fix. This is only needed until the real
// resources file has been generated.
let dir = new DirectoryInfo(config.ResolutionFolder)
try
let fsproj = Directory.GetFiles(config.ResolutionFolder, "*.fsproj") |> Array.head
let nsuri = "http://schemas.microsoft.com/developer/msbuild/2003"
let ns = XNamespace.Get nsuri
let doc = XDocument.Load fsproj
let rootnamespaceNode = doc.Descendants(ns + "RootNamespace") |> Seq.head
rootnamespaceNode.Value
with
| ex -> dir.Name
/// Filter out all lines that use the global namespace. These are only used at
/// runtime and require references to Mono.Android and XF which are problematic to load
/// inside the IDE context.
///
/// The C# code also contains private static constructors that contain code that we
/// don't want to execute inside the IDE. This code is needed inside the msbuild / runtime
/// context to update resource ID values between libraries.
let shouldAddLine (line: string) =
isMsBuild ||
isInsideIDE && not (line.Contains("global::"))
let source =
if File.Exists resourceFileName then
File.ReadLines resourceFileName
|> Seq.filter shouldAddLine
|> String.concat "\n"
else
let asm = Assembly.GetExecutingAssembly()
let resourceNames = asm.GetManifestResourceNames()
use stream = asm.GetManifestResourceStream("Resource.designer.cs")
use reader = new StreamReader(stream)
let source = reader.ReadToEnd()
let namespc = getRootNamespace()
source.Replace("${Namespace}", namespc)
generate source
interface ITypeProvider with
[<CLIEvent>]
member x.Invalidate = invalidateEvent.Publish
member x.GetStaticParameters(typeWithoutArguments) = [||]
member x.GetGeneratedAssemblyContents(assembly) =
match providedAssembly with
| Some(bytes, _) -> bytes
| _ -> failwith "Generate was never called"
member x.GetNamespaces() =
match providedAssembly with
| Some(_, namespaces) -> namespaces
| _ -> failwith "Generate was never called"
member x.ApplyStaticArguments(typeWithoutArguments, typeNameWithArguments, staticArguments) = null
member x.GetInvokerExpression(methodBase, parameters) =
match methodBase with
| :? ConstructorInfo as cinfo ->
Expr.NewObject(cinfo, Array.toList parameters)
| :? MethodInfo as minfo ->
if minfo.IsStatic then
Expr.Call(minfo, Array.toList parameters)
else
Expr.Call(parameters.[0], minfo, Array.toList parameters.[1..])
| _ -> failwith ("GetInvokerExpression: not a ConstructorInfo/MethodInfo, name=" + methodBase.Name + " class=" + methodBase.GetType().FullName)
member x.Dispose() =
compiler.Dispose()
watcher.Dispose()
[<assembly: TypeProviderAssembly>]
do()