From abe6c67b259e9fe34d6eeb40a7f71ea8db14c4be Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Thu, 3 Jun 2010 17:00:28 -0700 Subject: [PATCH] Implement dynamic compilation of modules --HG-- branch : dev --- .../Loaders/CSharpExtensionBuildProvider.cs | 21 +++++ .../Loaders/CSharpExtensionCompiler.cs | 47 ++++++++++ .../Loaders/CSharpProjectFullTrustCompiler.cs | 50 +++++++++++ .../CSharpProjectMediumTrustCompiler.cs | 52 +++++++++++ .../Extensions/Loaders/CSharpProjectParser.cs | 48 ++++++++++ .../Loaders/DynamicExtensionLoader.cs | 90 ++++++++++--------- .../Extensions/Loaders/IAssemblyBuilder.cs | 22 +++++ .../Extensions/Loaders/IBuildManager.cs | 16 ++++ .../Loaders/IVirtualPathProvider.cs | 30 +++++++ src/Orchard/Orchard.Framework.csproj | 9 ++ 10 files changed, 345 insertions(+), 40 deletions(-) create mode 100644 src/Orchard/Environment/Extensions/Loaders/CSharpExtensionBuildProvider.cs create mode 100644 src/Orchard/Environment/Extensions/Loaders/CSharpExtensionCompiler.cs create mode 100644 src/Orchard/Environment/Extensions/Loaders/CSharpProjectFullTrustCompiler.cs create mode 100644 src/Orchard/Environment/Extensions/Loaders/CSharpProjectMediumTrustCompiler.cs create mode 100644 src/Orchard/Environment/Extensions/Loaders/CSharpProjectParser.cs create mode 100644 src/Orchard/Environment/Extensions/Loaders/IAssemblyBuilder.cs create mode 100644 src/Orchard/Environment/Extensions/Loaders/IBuildManager.cs create mode 100644 src/Orchard/Environment/Extensions/Loaders/IVirtualPathProvider.cs diff --git a/src/Orchard/Environment/Extensions/Loaders/CSharpExtensionBuildProvider.cs b/src/Orchard/Environment/Extensions/Loaders/CSharpExtensionBuildProvider.cs new file mode 100644 index 000000000..92ce7357f --- /dev/null +++ b/src/Orchard/Environment/Extensions/Loaders/CSharpExtensionBuildProvider.cs @@ -0,0 +1,21 @@ +using System.Web.Compilation; + +namespace Orchard.Environment.Extensions.Loaders { + public class CSharpExtensionBuildProvider : BuildProvider { + private readonly CompilerType _codeCompilerType; + + public CSharpExtensionBuildProvider() { + _codeCompilerType = GetDefaultCompilerTypeForLanguage("C#"); + } + + public override CompilerType CodeCompilerType { get { return _codeCompilerType; } } + + public override void GenerateCode(AssemblyBuilder assemblyBuilder) { + var virtualPathProvider = new AspNetVirtualPathProvider(); + var compiler = new CSharpProjectMediumTrustCompiler(virtualPathProvider); + + var aspNetAssemblyBuilder = new AspNetAssemblyBuilder(assemblyBuilder, this); + compiler.CompileProject(this.VirtualPath, aspNetAssemblyBuilder); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Loaders/CSharpExtensionCompiler.cs b/src/Orchard/Environment/Extensions/Loaders/CSharpExtensionCompiler.cs new file mode 100644 index 000000000..b4f6d380a --- /dev/null +++ b/src/Orchard/Environment/Extensions/Loaders/CSharpExtensionCompiler.cs @@ -0,0 +1,47 @@ +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Web.Compilation; +using System.Web.Hosting; + +namespace Orchard.Environment.Extensions.Loaders { + /// + /// Compile a C# extension into an assembly given a directory location + /// + public class CSharpExtensionCompiler { + public CompilerResults CompileProject(string location) { + var codeProvider = CodeDomProvider.CreateProvider("cs"); + + var references = GetAssemblyReferenceNames(); + var options = new CompilerParameters(references.ToArray()); + + var fileNames = GetSourceFileNames(location); + var results = codeProvider.CompileAssemblyFromFile(options, fileNames.ToArray()); + return results; + } + + private IEnumerable GetAssemblyReferenceNames() { + return Enumerable.Distinct(BuildManager.GetReferencedAssemblies() + .OfType() + .Select(x => x.Location) + .Where(x => !string.IsNullOrEmpty(x))); + } + + private IEnumerable GetSourceFileNames(string path) { + foreach (var file in Directory.GetFiles(path, "*.cs")) { + yield return file; + } + + foreach (var folder in Directory.GetDirectories(path)) { + if (Path.GetFileName(folder).StartsWith(".")) + continue; + + foreach (var file in GetSourceFileNames(folder)) { + yield return file; + } + } + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Loaders/CSharpProjectFullTrustCompiler.cs b/src/Orchard/Environment/Extensions/Loaders/CSharpProjectFullTrustCompiler.cs new file mode 100644 index 000000000..4977434a2 --- /dev/null +++ b/src/Orchard/Environment/Extensions/Loaders/CSharpProjectFullTrustCompiler.cs @@ -0,0 +1,50 @@ +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Orchard.Environment.Extensions.Loaders { + /// + /// Compile a C# extension into an assembly given a directory location + /// + public class CSharpProjectFullTrustCompiler { + private readonly IVirtualPathProvider _virtualPathProvider; + private readonly IBuildManager _buildManager; + + public CSharpProjectFullTrustCompiler(IVirtualPathProvider virtualPathProvider, IBuildManager buildManager) { + _virtualPathProvider = virtualPathProvider; + _buildManager = buildManager; + } + + /// + /// Compile a csproj file given its virtual path. Use the CSharp CodeDomProvider + /// class, which is only available in full trust. + /// + public CompilerResults CompileProject(string virtualPath) { + var codeProvider = CodeDomProvider.CreateProvider("cs"); + var directory = _virtualPathProvider.GetDirectoryName(virtualPath); + + using (var stream = _virtualPathProvider.OpenFile(virtualPath)) { + var descriptor = new CSharpProjectParser().Parse(stream); + + var references = GetAssemblyReferenceNames(); + var options = new CompilerParameters(references.ToArray()); + + var fileNames = descriptor.SourceFilenames + .Select(f => _virtualPathProvider.Combine(directory, f)) + .Select(f => _virtualPathProvider.MapPath(f)); + + var results = codeProvider.CompileAssemblyFromFile(options, fileNames.ToArray()); + return results; + } + } + + private IEnumerable GetAssemblyReferenceNames() { + return _buildManager.GetReferencedAssemblies() + .OfType() + .Where(a => !string.IsNullOrEmpty(a.Location)) + .Select(a => a.Location) + .Distinct(); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Loaders/CSharpProjectMediumTrustCompiler.cs b/src/Orchard/Environment/Extensions/Loaders/CSharpProjectMediumTrustCompiler.cs new file mode 100644 index 000000000..b8d611d6e --- /dev/null +++ b/src/Orchard/Environment/Extensions/Loaders/CSharpProjectMediumTrustCompiler.cs @@ -0,0 +1,52 @@ +using System; +using System.CodeDom; +using System.IO; +using System.Linq; + + +namespace Orchard.Environment.Extensions.Loaders { + /// + /// Compile a C# extension into an assembly given a directory location + /// + public class CSharpProjectMediumTrustCompiler { + private readonly IVirtualPathProvider _virtualPathProvider; + + public CSharpProjectMediumTrustCompiler(IVirtualPathProvider virtualPathProvider) { + _virtualPathProvider = virtualPathProvider; + } + /// + /// Compile a csproj file given its virtual path, a build provider and an assembly builder. + /// This method works in medium trust. + /// + public void CompileProject(string virtualPath, IAssemblyBuilder assemblyBuilder) { + using (var stream = _virtualPathProvider.OpenFile(virtualPath)) { + var descriptor = new CSharpProjectParser().Parse(stream); + + var directory = _virtualPathProvider.GetDirectoryName(virtualPath); + foreach (var filename in descriptor.SourceFilenames.Select(f => _virtualPathProvider.Combine(directory, f))) { + assemblyBuilder.AddCodeCompileUnit(CreateCompileUnit(filename)); + } + } + } + + private CodeCompileUnit CreateCompileUnit(string virtualPath) { + var contents = GetContents(virtualPath); + var unit = new CodeSnippetCompileUnit(contents); + var physicalPath = _virtualPathProvider.MapPath(virtualPath); + if (!string.IsNullOrEmpty(physicalPath)) { + unit.LinePragma = new CodeLinePragma(physicalPath, 1); + } + return unit; + } + + private string GetContents(string virtualPath) { + string contents; + using (var stream = _virtualPathProvider.OpenFile(virtualPath)) { + using (var reader = new StreamReader(stream)) { + contents = reader.ReadToEnd(); + } + } + return contents; + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Loaders/CSharpProjectParser.cs b/src/Orchard/Environment/Extensions/Loaders/CSharpProjectParser.cs new file mode 100644 index 000000000..d367c0886 --- /dev/null +++ b/src/Orchard/Environment/Extensions/Loaders/CSharpProjectParser.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using System.Xml.Linq; + +namespace Orchard.Environment.Extensions.Loaders { + public class CSharpProjectDescriptor { + public IEnumerable SourceFilenames { get; set; } + public IEnumerable References { get; set; } + } + + public class ReferenceDescriptor { + public string AssemblyName { get; set; } + } + + public class CSharpProjectParser { + public CSharpProjectDescriptor Parse(Stream stream) { + var document = XDocument.Load(XmlReader.Create(stream)); + return new CSharpProjectDescriptor { + SourceFilenames = GetSourceFilenames(document), + References = GetReferences(document) + }; + } + + private IEnumerable GetSourceFilenames(XDocument document) { + return document + .Elements(ns("Project")) + .Elements(ns("ItemGroup")) + .Elements(ns("Compile")) + .Attributes("Include") + .Select(c => c.Value); + } + + private IEnumerable GetReferences(XDocument document) { + return document + .Elements(ns("Project")) + .Elements(ns("ItemGroup")) + .Elements(ns("Reference")) + .Attributes("Include") + .Select(c => new ReferenceDescriptor { AssemblyName = c.Value }); + } + + private static XName ns(string name) { + return XName.Get(name, "http://schemas.microsoft.com/developer/msbuild/2003"); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Loaders/DynamicExtensionLoader.cs b/src/Orchard/Environment/Extensions/Loaders/DynamicExtensionLoader.cs index 3178d2780..64ca1a891 100644 --- a/src/Orchard/Environment/Extensions/Loaders/DynamicExtensionLoader.cs +++ b/src/Orchard/Environment/Extensions/Loaders/DynamicExtensionLoader.cs @@ -1,7 +1,5 @@ -using System.CodeDom.Compiler; -using System.Collections.Generic; +using System; using System.IO; -using System.Linq; using System.Reflection; using System.Web.Compilation; using System.Web.Hosting; @@ -15,45 +13,57 @@ namespace Orchard.Environment.Extensions.Loaders { if (HostingEnvironment.IsHosted == false) return null; - var codeProvider = CodeDomProvider.CreateProvider("cs"); - - var references = GetAssemblyReferenceNames(); - var options = new CompilerParameters(references.ToArray()); - - var locationPath = HostingEnvironment.MapPath(descriptor.Location); - var extensionPath = Path.Combine(locationPath, descriptor.Name); - - var fileNames = GetSourceFileNames(extensionPath); - var results = codeProvider.CompileAssemblyFromFile(options, fileNames.ToArray()); - - return new ExtensionEntry { - Descriptor = descriptor, - Assembly = results.CompiledAssembly, - ExportedTypes = results.CompiledAssembly.GetExportedTypes(), - }; - } - - private IEnumerable GetAssemblyReferenceNames() { - return BuildManager.GetReferencedAssemblies() - .OfType() - .Select(x => x.Location) - .Where(x => !string.IsNullOrEmpty(x)) - .Distinct(); - } - - private IEnumerable GetSourceFileNames(string path) { - foreach (var file in Directory.GetFiles(path, "*.cs")) { - yield return file; - } - - foreach (var folder in Directory.GetDirectories(path)) { - if (Path.GetFileName(folder).StartsWith(".")) - continue; - - foreach (var file in GetSourceFileNames(folder)) { - yield return file; + // 1) Try to load the assembly directory + // This will look in the "App_Data/Dependencies" directory if + // the probing path is correctly configured in Web.config + { + try { + Assembly assembly = Assembly.Load(descriptor.Name); + return CreateExtensionEntry(descriptor, assembly); + } + catch (FileNotFoundException e) { + // The assembly is not in one of the probing directory, + // including "App_Data/Dependencies", we need to move on + // to other strageties } } + + // 2) look for the assembly in the "Bin" directory + { + string modulePath = HostingEnvironment.MapPath(descriptor.Location); + modulePath = Path.Combine(modulePath, descriptor.Name); + string moduleBinary = Path.Combine(modulePath, "bin"); + moduleBinary = Path.Combine(moduleBinary, descriptor.Name + ".dll"); + if (File.Exists(moduleBinary)) { + // Copy file to dependencies directory + string dependenciesPath = HostingEnvironment.MapPath("~/App_Data/Dependencies"); + if (!Directory.Exists(dependenciesPath)) { + Directory.CreateDirectory(dependenciesPath); + } + string destFile = Path.Combine(dependenciesPath, descriptor.Name + ".dll"); + File.Copy(moduleBinary, destFile, true); + + // then load the assembly + Assembly assembly = Assembly.Load(descriptor.Name); + return CreateExtensionEntry(descriptor, assembly); + } + } + + // 3) look for the csproj in the module directory + { + string projfileName = Path.Combine(descriptor.Location, descriptor.Name); + projfileName = Path.Combine(projfileName, descriptor.Name + ".csproj").Replace('\\', '/'); + var assembly = BuildManager.GetCompiledAssembly(projfileName); + return CreateExtensionEntry(descriptor, assembly); + } + } + + private static ExtensionEntry CreateExtensionEntry(ExtensionDescriptor descriptor, Assembly assembly) { + return new ExtensionEntry { + Descriptor = descriptor, + Assembly = assembly, + ExportedTypes = assembly.GetExportedTypes(), + }; } } } \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Loaders/IAssemblyBuilder.cs b/src/Orchard/Environment/Extensions/Loaders/IAssemblyBuilder.cs new file mode 100644 index 000000000..b2e434f33 --- /dev/null +++ b/src/Orchard/Environment/Extensions/Loaders/IAssemblyBuilder.cs @@ -0,0 +1,22 @@ +using System.CodeDom; +using System.Web.Compilation; + +namespace Orchard.Environment.Extensions.Loaders { + public interface IAssemblyBuilder { + void AddCodeCompileUnit(CodeCompileUnit compileUnit); + } + + public class AspNetAssemblyBuilder : IAssemblyBuilder { + private readonly AssemblyBuilder _assemblyBuilder; + private readonly BuildProvider _buildProvider; + + public AspNetAssemblyBuilder(AssemblyBuilder assemblyBuilder, BuildProvider buildProvider) { + _assemblyBuilder = assemblyBuilder; + _buildProvider = buildProvider; + } + + public void AddCodeCompileUnit(CodeCompileUnit compileUnit) { + _assemblyBuilder.AddCodeCompileUnit(_buildProvider, compileUnit); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Loaders/IBuildManager.cs b/src/Orchard/Environment/Extensions/Loaders/IBuildManager.cs new file mode 100644 index 000000000..af638f9d6 --- /dev/null +++ b/src/Orchard/Environment/Extensions/Loaders/IBuildManager.cs @@ -0,0 +1,16 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Web.Compilation; + +namespace Orchard.Environment.Extensions.Loaders { + public interface IBuildManager { + IEnumerable GetReferencedAssemblies(); + } + + public class AspNetBuildManager : IBuildManager { + public IEnumerable GetReferencedAssemblies() { + return BuildManager.GetReferencedAssemblies().Cast(); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Loaders/IVirtualPathProvider.cs b/src/Orchard/Environment/Extensions/Loaders/IVirtualPathProvider.cs new file mode 100644 index 000000000..79f7d0a9e --- /dev/null +++ b/src/Orchard/Environment/Extensions/Loaders/IVirtualPathProvider.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; +using System.Web.Hosting; + +namespace Orchard.Environment.Extensions.Loaders { + public interface IVirtualPathProvider { + string GetDirectoryName(string virtualPath); + string Combine(params string[] paths); + Stream OpenFile(string virtualPath); + string MapPath(string virtualPath); + } + + public class AspNetVirtualPathProvider : IVirtualPathProvider { + public string GetDirectoryName(string virtualPath) { + return Path.GetDirectoryName(virtualPath); + } + + public string Combine(params string[] paths) { + return Path.Combine(paths).Replace('\\', '/'); + } + + public Stream OpenFile(string virtualPath) { + return VirtualPathProvider.OpenFile(virtualPath); + } + + public string MapPath(string virtualPath) { + return HostingEnvironment.MapPath(virtualPath); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index f43d1f4af..1712e764a 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -119,6 +119,7 @@ 3.5 + False ..\..\lib\yaml\Yaml.dll @@ -135,6 +136,14 @@ + + + + + + + +