请注意,本站并不支持低于IE8的浏览器,为了获得最佳效果,请下载最新的浏览器,推荐下载 Chrome浏览器
欢迎光临。交流群:166852192

orchard2 动态编译


Summary

The needs

为了成为一个模块化的应用程序,Orchard.Web没有在其project.json文件中指定任何依赖于模块和主题,它们是单独的可移植库项目。所以,当你构建Orchard.Web时,这些模块不会被编译。您可以手动构建它们(例如从VS工具),但是当您启动Orchard.Web时,我们仍然需要加载它们的程序集。


因此,动态加载是必要的,我们需要在运行时加载所有模块依赖性的非环境组件。通过这样做,我们还可以将视图编译的所有需要的元数据引用传递给razor视图引擎。我们还需要在运行时将这些程序集存储在一些探测文件夹中,以便在不同的上下文中检索它们。


动态编译在开发上下文中非常有用,可以更新模块源文件,包或核心项目,只需点击F5,所有相关模块都将被动态重新编译。当模块具有对不是Orchard.Web的核心项目的依赖性时,如果需要,这个非环境项目也在运行时被重新编译。


在发布的上下文中,大多数时候最好使用预编译的模块,但在某些情况下可能只需要推送更新的源文件。动态编译和加载已在不同的上下文中测试,其中核心项目不存在和/或没有软件包存储。

Library assemblies

模块项目是对其他项目和/或包的其他库具有依赖性的库。当通过dotnet模型API之一解析时,每个库提供与不同类型的程序集相关的不同集合。


编译程序集:编译项目时引用所有依赖关系。


默认运行时程序集:用于实现,需要为所有依赖关系加载。


仅编译程序集:大多数时候库只有一个与默认运行时程序集相同的编译程序集。否则,它是一个只编译组件,另一个用于实现。


特定的运行时组件:某些库为不同的运行时环境提供不同的实现。


本机程序集:某些库需要为不同的运行时环境提供本机实现。因为模块不是自己发布的,所以我们不关心本地输出。


资源汇编:资源管理器使用的{project} .resources.dll文件。


环境组件:与环境项目或环境包装相关的组件。

Dotnet commands

这里我们描述dotnet命令如何输出二进制文件,这是因为我们使用相同的结构来存储在运行时在探测文件夹中的模块依赖性的所有非环境程序集,然后在不同的上下文中检索它们进行编译和加载。


dotnet compile(由dotnet build使用):输出项目程序集(及其.pdb文件)。


// {project} / bin / {config} / {framework} / {project} .dll
Orchard.Web / bin / Debug / netcoreapp1.0 / Orchard.Web.dll
当编译为可执行文件并使用其编译上下文时,还生成依赖文件并用于编译,项目组合随后更大。


// {project} / obj / {config} / {framework} / {project} dotnet-compile.deps.json
Orchard.Web / obj / Debug / netcoreapp1.0 / Orchard.Webdotnet-compile.deps.json
dotnet build:它使用dotnet编译,如果需要,所有引用的项目也被构建。


当编译为可执行文件及其编译上下文时,将输出另一个依赖文件。


// {project} / bin / {config} / {framework} / {project} .deps.json
Orchard.Web / bin / Debug / netcoreapp1.0 / Orchard.Web.deps.json
还将输出所有引用项目的运行时组件。


// {project} / bin / {config} / {framework} / {assembly} .dll
Orchard.Web / bin / Debug / netcoreapp1.0 / Orchard.Data.dll
不输出引用包的运行时程序集。


资源汇编被忽略。


// {project} / bin / {config} / {framework} / {locale} / {project} .resources.dll
Orchard.Web / bin / Debug / netcoreapp1.0 / fr / Orchard.Web.resources.dll
dotnet publish:所有上述程序集都在已发布目标的根目录中输出,加上以下内容。


还输出了不是目标框架(参见dotnet / shared / Microsoft.NETCore.App / {version})一部分的引用包的默认运行时程序集。


root / System.Data.Common.dll
root / Microsoft.Extensions.DependencyModel.dll
还输出不是目标框架的一部分的引用包的特定运行时程序集。


// runtimes / {rid} / lib / {tfm} / {assembly} .dll
root / runtimes / unix / lib / netstandard1.3 / System.IO.Pipes.dll
仅输出组件。


root / refs / System.IO.dll
针对不同目标运行时的本机程序集被忽略。


// runtimes / {rid} / native {assembly}。{lib-suffix}
root / runtimes / osx / native / lmdb.dylib
与dotnet构建一样,资源组件也被输出。


// {locale} / {project} .resources.dll
root / fr / Orchard.Web.resources.dll
dotnet restore:解析project.json文件以检索所有依赖项,更新包,如果需要,使用更新的依赖项元数据编写project.lock.json文件。

Project model API

Dotnet cli使用这个API允许创建项目上下文,例如检索所有的依赖项(引用的项目/包库)。 它是强大的,因为当一个库被标记为已解决,所有相关的编译,运行时和本地集合填充完全解析的资产(例如,完整路径)。 但是在这里,2 project.json和project.lock.json文件需要有。

Dependency model API

当一个项目被构建为一个可执行文件和它的编译上下文,根据需要执行的主要应用程序和编译razor视图,生成一个更大的程序集和一个{project} .deps.json文件。 然后我们可以使用Dependency Model API通过此文件检索依赖关系。 它不如项目模型强大,因为资产没有完全解析(例如只有相对路径)。 它不依赖于2个项目的json文件,但它需要相关的.deps.json文件。

Library contexts

我们使用项目模型API来解析模块及其依赖关系。模块本身及其依赖关系都是可以在不同上下文中的库。核心项目/包可能不在已发布的上下文中,模块可以在生产中预编译...


已解决的项目:2个项目json文件。


已解决的包:有一个包含此引用包的包存储。


未解决的项目:没有项目json文件,例如核心项目不在已发布的上下文中。


未解决的包:没有包存储(例如在生产中)或不包含此包。


预编译项目:已解决的项目,但没有任何源文件,但包含所有二进制文件。


预编译模块:未解析的项目,但所有必要的二进制文件直接在其bin文件夹中。


环境项目:主项目或属于其依赖项的项目(不是所有核心项目都是环境的)。


环境包:属于主项目依赖包的包,因此包括目标框架的依赖包。

Dynamic compilation

Roslyn编译器:我们通过引用Microsoft.Net.Compilers.netcore包来使用Roslyn csharp编译器csc.exe。然后,在运行时,我们将其复制到可执行文件csc.dll中,并自动创建一个csc.runtimeconfig.json运行时配置文件,因为dotnet核心设置会生成它的输出。然后,作为dotnet编译,我们可以执行csc.dll。


本机pdb写入程序:根据调试类型选项,编译器输出便携式或完整的.pdb调试信息文件。对于完整选项,它需要本机pdb写程序,因此我们引用Microsoft.DiaSymReader.Native包。当发布本地资产时,将其输出到运行时目录,并且在开发过程中,我们在运行时从包存储中执行。我们还创建一个假的csc.deps.json文件,仅引用这些Windows本机pdb写入程序,这允许编译器找到它们。


编译:该实现主要源于dotnet编译源代码,它使用项目模型API来解析项目依赖项的编译选项,输出路径,源文件和所有编译程序集。然后,选项和引用存储在dotnet-compile-csc.rsp响应文件中,该文件作为参数传递给csc.dll编译器。


配置:我们需要将当前配置的Debug或Release传递给项目模型API。这里我们使用的主要项目已经建成的配置。为此,我们在主项目上使用依赖模型API(因此不依赖其project.json文件)来获取其编译选项,然后检索配置。


条件:只有非环境的已解决项目可以被编译,并且每次启动只有一次,因为我们检查它是否已经被编译(例如作为依赖)。然后我们检查所有编译IO,看看是否需要编译。这里我们做dotnet构建作业的一部分。


编译IO:编译输入是2个项目json文件,源文件,最终资源输入文件,以及项目依赖关系的所有编译程序集。编译输出是生成的项目组合件(及其.pdb文件),最后是资源输出组件。请注意,当项目编译输出在另一个项目中被引用为依赖项时,它可以是编译输入。


编译IO检查:我们检查所有已解析编译IO文件的存在和最后写入时间。如果缺少IO或者输入比最早的输出新,则需要编译项目。但IO不是都以相同的方式解决,源文件分辨率已经基于它的存在。因此,我们不仅可以依赖于此,例如,当源文件被移动,删除或添加旧文件时。这就是为什么,要查看是否有其他变化,我们使用存储在dotnet-compile-csc.rsp响应文件中的上一个编译上下文。


构建:在编译项目本身之前,我们解析所有的依赖。如果依赖是一个非环境和已解决的项目(这里可以是一个核心项目),我们递归调用动态编译,依此类推,通过依赖关系图,如dotnet build do。


然后,我们从每个依赖关系解析所有编译程序集路径,并将它们添加到编译所需的引用列表。所以,这里我们可以做一个完整的编译IO检查,如果需要,项目是编译。如果编译成功并且有资源输入文件,则还会生成资源组合件。
依赖:对于每个依赖项是项目或包库,我们不需要解析自己的依赖项。它们已经属于正在编译的项目的那些,并且它的所有依赖图已经被解析。并且,大多数时候,依赖关系在其相关集合中只有一个与默认运行时组件相同的编译程序集。所以,在这里,当我们只使用装配术语时,我们正在谈论这个独特的术语。


如果依赖项是未解决的项目,我们必须通过在探测文件夹中搜索程序集可能已经存储的方式来解析自己的程序集路径。首先从环境程序集的运行时目录,然后回退到项目(正在编译)二进制文件夹和共享探测文件夹,我们尝试解析最近的程序集文件。这里,我们只需要与库标识名相同的文件名。


如果一个依赖是一个预编译的模块,因为它没有项目文件,它也被检查为一个未解决的项目,所以我们首先做上面的。然后,如果我们无法解析装配路径,我们会回到这个(可能的)预编译模块的bin文件夹。这里我们使用依赖项本身的项目路径,并且,替代使用常规输出路径,我们直接在bin文件夹中搜索。


如果依赖是一个未解决的包,我们必须从相同的探测文件夹中解析出上面的程序集路径。未解析的包仍然提供相对路径,我们可以从中提取程序集名称。然后,如果编译时程序集是一个仅编译程序集,我们还将refs sublfolder组合到文件名。


如果依赖关系是通过预编译模块间接引用的未解决的包,我们首先执行上述操作。然后,如果我们无法解析装配路径,我们尝试找到一个(可能的)父预编译模块,其中包含包装程序集。在这里,我们使用依赖父节点集合,它们也是父节点的库。因此,我们可以查找所有父项目并直接在其bin文件夹中搜索。


如果依赖项是没有源文件但仍然是项目json文件的预编译项目,那么它将被解析,但编译程序集集合为空。所以,我们需要解决自己从上面的探测文件夹的装配路径。但是在这里,我们不在运行时目录中搜索,因为环境和解决的项目旨在拥有所有源文件。然后,如果没有解决,我们回到这个预编译项目的常规输出路径。


如果依赖项是环境和已解析项目,则项目模型解析的装配路径可以按原样添加到编译引用。但是,因为例如VS可能在另一个文件夹(例如工件)中输出二进制文件,我们首先检查程序集文件是否存在,如果不是我们回退到运行时目录(因为它是一个环境程序集)。


如果依赖关系是非环境和已解决的项目或是已解析的包,则其项目模型解析的其编译程序集的所有路径都将添加到编译引用中。


并行编译:扩展加载是并行完成的,因此模块项目也是并行编译的。我们使用基于项目名称的锁对象的字典,这是为了防止同一项目的同时编译。我们还使用简单的锁来防止同一文件的同时写入。

Dynamic loading

加载:如果一个模块尚未加载,我们将解析所有的依赖关系,并使用默认的AssemblyLoadContext在内存中加载所有相关的非环境运行时程序集。我们不检查每个单独的程序集,如果它已经以这种方式加载,似乎是在内部完成,但是在这里尝试加载环境程序集将失败。


依赖关系:如果模块是已解决的项目,则所有其依赖关系的程序集路径都以与编译时相同的方式解析。但是在这里,我们使用运行时程序集集合,如果没有解决依赖关系,我们不会回退到运行时目录,因为它只包含环境程序集。


如果一个包提供不同的特定运行时程序集,每一个提供一个非空的rid(运行时标识符)。然后我们查找第一个与当前运行时环境兼容的rid,并使用它来解析正确的运行时汇编路径。


如果模块是没有项目json文件的预编译模块,我们不能使用项目模型API来解析它的依赖关系。但是所有需要的程序集都直接在模块bin文件夹中,并且通过用于存储程序集的相同结构。所以在这里,我们简单地解析模块bin文件夹,其中默认运行时程序集旨在位于顶层,特定运行时程序集在runtimes子文件夹下,并且只编译refs子文件夹中的那些。

Dynamic storing

探测文件夹:存储二进制文件的特定文件夹,因此可以在不同的上下文中检索。


结构化探测文件夹:在每个探测文件夹中,我们以结构化方式存储组件,因为dotnet publish在运行时目录中输出其二进制文件,默认运行时组件存储在顶层,只编译refs子文件夹中的程序集, {locale}子文件夹以及runtimes子文件夹下的特定运行时程序集。


nuget包存储使用相同类型的模式,但不完全,例如只编译程序集根据目标框架而不是运行时环境有所不同。所以,这里他们都在refs子文件夹中展平。


模块二进制文件夹:当在运行时编译模块项目时,其主要组件自然地在其二进制文件夹中输出。在加载它时,我们还在这里存储所有非环境程序集的所有依赖项。


{project} / bin / {config} / {framework} / {project} .dll
{project} / bin / {config} / {framework} / {a-default-runtime} .dll
{project} / bin / {config} / {framework} / refs / {a-compile-only} .dll
{project} / bin / {config} / {framework} / {locale} / {a} .resources.dll
{project} / bin / {config} / {framework} / runtimes / {rid} / lib / {tfm} / {a-specific-runtime} .dll
对于预编译的模块,所有二进制文件都已直接存储在其bin文件夹中。


{module} / bin / {module} .dll
{module} / bin / {a-default-runtime} .dll
{module} / bin / refs / {a-compile-only} .dll
{module} / bin / {locale} / {a} .resources.dll
{module} / bin / runtimes / {rid} / lib / {tfm} / {a-specific-runtime} .dll
共享依赖文件夹:Idem如上所述,但在App_Data / dependencies位置并由所有模块共享。


root / App_Data / dependencies / {a-module} .dll
root / App_Data / dependencies / {a-default-runtime} .dll
root / App_Data / dependencies / refs / {a-compile-only} .dll
root / App_Data / dependencies / {locale} / {a} .resources.dll
root / App_Data / dependencies / runtimes / {rid} / lib / {tfm} / {a-specific-runtime} .dll
运行时目录:这里,我们只存储自己的非环境资源程序集,这是资源管理器找到的。


root / {locale} / {a} .resources.dll
解决:在加载模块依赖程序集合之前,我们首先尝试从它的常规位置(例如从包存储)解决它,然后我们回到探测文件夹(模块二进制和共享探测文件夹),我们尝试找到最多最近一个。


存储:加载模块依赖性汇编程序后,不知道从哪个位置解析出来,程序集存储在探测文件夹(模块二进制和共享探测文件夹)中,我们首先检查程序集是否已经存在,或者是否有更早的日期。


更新:上面的实现的结果是我们也更新每个探测文件夹与最后完全解析的程序集或更新的一个在另一个探测文件夹中找到。这意味着例如可以通过共享探测文件夹更新模块二进制文件夹中的程序集,其中更新的程序集来自另一个模块。

Views compilation

当编译视图时,剃刀视图引擎可以引用作为正在运行的应用程序的一部分的所有环境组件。 但是,模块不是环境项目,因此我们首先需要在运行时加载模块及其依赖关系的所有非环境运行时组件。 然后,我们还必须将所有相关的编译程序集作为元数据引用传递给razor视图引擎。


razor视图引擎允许我们在启动时通过配置选项传递这些额外的编译引用。


动态加载使用与roslyn相同的默认AssemblyLoadContext,因此我们没有任何模型/视图数据类型不匹配。

Concerns

文件IO权限:例如在生产环境中,尤其是在运行时目录中写入时。


写并发的文件:例如,在具有共享文件系统的多实例上下文中。

Todo

包装:为了能够作为包共享模块,也许我们必须实现一些定制的包装过程,例如灵感来自dotnet pack和dotnet restore。
https://github.com/OrchardCMS/Orchard2/wiki/Dynamic-compilation#dotnet-commands 

作者原创内容不容易,如果觉得内容不错,请点击右侧“打赏”,赏俩给作者花花,也算是对作者付出的肯定,也可以鼓励作者原创更多更好内容。
更多详情欢迎到QQ群 166852192 交流。