代码改变世界

CLR via C# 边读边想 02 - .NET 程序的编译、打包、部署和管理

2012-06-05 16:39  richardzhaoxb  阅读(327)  评论(0编辑  收藏  举报

.NET Framework Deployment Goals

一直以来,微软的的Windows和它上面的应用程序经常被诟病不稳定,以及太复杂。其中不稳定的一个主要的原因就是由于“DELL hell”,所谓的DLL Hell就是指以前的程序不方便安装,卸载和转移,因为他们都不是自解释的,需要用到注册表。 当有新的版本要更新旧版本又不能保留,但是又担心新版本不能很好的兼容旧的版本。而被大家普遍认为太复杂的一个主要原因是因为安装复杂,一般要安装一个程序,首先要往一系列不同的目录copy文件,然后更新注册表,最后还要在桌面或菜单创建快捷方式。这就导致这个应用不是一个单一的独立实体,所以你不能简单的通过简单的copy来做备份,而必须重新安装程序来使得注册表等其他配置正确。卸载程序的过程也复杂,不能通过简单的删除程序目录来卸载。.Net 程序的部署目标就是要解决这一系列问题。

 

Building Types into a Module

以下面这个简单 .Net 程序为例

public sealed class Program 
{
    public static void Main() 
    {
        System.Console.WriteLine("Hi");
    }
}

程序定义了一个类型,叫做Program,它只有一个 Main 方法,这个方法中引用了 System.Console 类。System.Console 类是由微软实现的,他的 IL 是在 MSCorLib.dll  中。

 要编译上面的代码,先把它保存为一个cs文件,例如 Program.cs。 然后执行下面的命令行。

csc.exe /out:Program.exe /t:exe /r:MSCorLib.dll Program.cs

/out: 输出的文件

/t[arget]: 编译的目标,可以是exe,控制台程序。winexe,Windows Form程序。

/r[eference]: 需要引用的已有的类的dll

MSCorLib.dll 是一个特殊的文件,它包含了所有的核心类型:Byte、Char、String、Int32 等等其他。实际上,这些类被如此频繁的使用,以至于C#的编译器会自动引用MSCorLib.dll 文件。换句话说,下面的命令行和之前的是一样的效果。

csc.exe /out:Program.exe /t:exe Program.cs

不仅仅如此,因为 /out:Program.exe 和 /t:exe 也是C#编译器的默认选项,下面的命令行和前面的两个都产生同样的效果:

csc.exe Program.cs 

如果在某种特殊的情况下,你不想要让编译器自动引用MSCorLib.dll文件,你可以使用 /nostdlib 的选项。微软使用这个选项来编译 MSCorLib.dll 本身。 

 

Response Files

一个response file 是一个包含有一组编译器指令的文本文件。例如有一个Response file包含如下内容:

/out:MyProject.exe
/target:winexe

为了能使用上这个response file,要使用@符号后面加上 response file 的名字。比如说:

csc.exe @MyProject.rsp CodeFile1.cs CodeFile2.cs

response file ,当你不想重复的在命令行指定命令行参数还是很有用的。

C#编译器还支持多个response files。除了你指定要使用的response file,编译器还会自动隐式的加载一个叫做 CSC.rsp 的 response file,但你运行csc命令时,编译器会在当前目录寻找 CSC.rsp 文件,你可以把和这个project相关的编译选项放在这个文件中。

编译器还会加载一个全局的 CSC.rsp 文件,这个文件会作用到所有本机的所有编译命令。本地的 CSC.rsp 选项会覆盖 全局的CSC.rsp 的选项,而在命令行中指定的选项又会覆盖本地的 CSC.rsp 选项。

当你安装 .Net Framework 时,全局 CSC.rsp 文件默认被安装到

%SystemRoot%\Microsoft.NET\Framework\vX.X.Xdirectory

其中 X.X.X是 .Net Framework 的版本号。

下面是 4.0 的 CSC.rsp 文件内容:

# This file contains command-line options that the C#
# command line compiler (CSC) will process as part
# of every compilation, unless the "/noconfig" option
# is specified.
# Reference the common Framework libraries
/r:Accessibility.dll
/r:Microsoft.CSharp.dll
/r:System.Configuration.dll
/r:System.Configuration.Install.dll
/r:System.Core.dll
/r:System.Data.dll
/r:System.Data.DataSetExtensions.dll
/r:System.Data.Linq.dll
.......

可能你会考虑到引用这么多的库文件,会拖慢编译的速度,其实只要你的代码中没有使用这些库中定义的类型,对编译的过程没有任何影响,对运行时也没有任何影响。

注意:如果你使用了 /reference 选项,你可以指定被引用文件的绝对路径,如果没有指定路径,只指定了文件名,那编译器会按照顺序到以下几个路径寻找你指定的文件:

  1. 工作路径,也就是执行命令行的当前路径。
  2. CSC.exe 所在的目录,比如说 %SystemRoot%\Microsoft.NET\Framework\v4.0.##### ,MSCorLib.dll 文件就在这个目录。
  3. 命令行中指定的 /Lib 的路径。
  4. 系统环境变量LIB中指定的路径

一般不鼓励修改系统全局的 CSC.rsp 文件,虽然他可以让你在开发时会轻松一些,但是一旦你的代码要放到其他环境下编译就会有麻烦。你也可以告诉编译器忽略全局和本地的 CSC.rsp 文件,你只要使用 /noconfig 选项就行了。

 

A Brief Look at Metadata

一个managed PE文件由四部分组成:

  • PE32(+) header:标准的Windows程序的头信息
  • CLR header:这个DLL生成时所依赖的CLR版本号,Strong Name的数字签名(可选)。
  • metadata:一块二进制的数据,主要有三个表
    • definetion table:包括了 ModuleDef、TypeDef、MethodDef、FieldDef、ParamDef、PropertyDef、EventDef。
    • reference table:包括了 AssemblyRef、ModuleRef、TypeRef、MemberRef
    • manifast table:包括了 AssemblyDef、FileDef、ManifestResourceDef、ExportedTypesDef
  • IL: 编译成中间语言的代码。

我们可以使用各种工具来查看PE中的 metadata,ILDasm.exe 就是.Net Framework 只带的工具。

 

Combining Modules to Form an Assembly 

上面生成的 Program.exe  文件不仅是一个带有metadata 的 PE 文件,也是一个Assembly。 Assembly 是一个或多个包含类定义和资源文件的集合。 在这些文件中的其中一个就包含了 manifest 。 manifest中包含了一个清单列出了这个Assembly中所包含的文件,还有Assembly的版本信息、culture、publisher、public exported types。

CLR加载一个Assembly时,要先加载这个Assembly的 manifest 信息,然后根据这个manifest中其他文件的信息加载其他文件。

一般的Assembly只包含一个文件,就像前面的Program.exe,但是Assembly中确实还可以包含多个问文件,特别是很多资源文件,比如 .gif 、.jpg 文件。

微软这样来设计Assembly,是给了我们很大的灵活度来控制程序的打包,比如有些类会经常用到,那就把经常用到的类放在一个Assembly中,不经常用到的放在另外一个Assembly,如果这些被用到的Assembly是通过Internet下载到程序的本地,那么就可以有效的减少网络的负载,提高程序加载的速度。 这样设计还有一个好处,就是可以把不同语言编译成的module打包到一个Assembly中。

前面介绍过 /t[arget] 选项除了可以是 exe、winexe 和 library 外,还可以是 module,这个值告诉编译器生成一个不带 manifest的 PE 文件,这个文件是以 .netmodule 为后缀的文件,在被加入到Assembly之前,它不能被CLR调用。

杯具的是 VS 不支持生成带多个PE的 Assembly,如果你想做到这个效果,只能借助于 命令行工具。

下面以一个例子说明如何用 /addmodule 选项把一个Module加入到Assembly中:

假设我们有两个代码文件:

  • RUT.cs, which contains rarely used types
  • FUT.cs, which contains frequently used types

我们把不常用到的 RUT.cs 编译成一个模块:

csc /t:module RUT.cs

这个命令会产生一个 RUT.netmodule 文件,这是一个标准的 PE 文件,但是他不能被 CLR 加载。

接着,我们把经常用的 FUT.cs 编译成一个Assembly,但是后面因为要包含其他的module,所以我们把生成的Assembly改名为 JeffTypes.dll:

csc /out:JeffTypes.dll /t:library /addmodule:RUT.netmodule FUT.cs

运行后的结果可以用下图来概述:

RUT.netmodule 文件包含了从RUT.cs 编译过来的 IL、definition matedata、reference matedate。而JeffTypes.dll 除了包含 IL、definition matedata、reference matedate外,还包含一个额外的 manifest ,就是这一点差别让它成为 Assembly 而不是 module。这个额外的 manifest 描述了这个 Assembly 是由哪些文件组成的,在这个例子中就是 JeffTypes.dll 它本身和RUT.netmodule 文件,这个 manifest metadata 还包含了所有在 JeffTypes.dll 和 RUT.netmodule 的公开类。

当一个客户端程序需要引用到 FUT.cs 中定义的类,但是没有引用 RUT.cs 中定义的类,CLR是不会加载 RUT.netmodule 文件的,就算这时 RUT.netmodule 被删除了,已不存在,CLR也不会报错。

 

Adding Assemblies to a Project by Using the Visual Studio IDE

To make your own assemblies appear in the .NET tab’s list, add the following subkey to the registry:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\AssemblyFolders\MyLibName

 

Using the Assembly Linker 

AL.exe 可以用来把各个不同的编译器生产的modules,也可以用 AL.exe 来编译生成只包含资源的 Assembly,这中程序集也叫做 Satellite Assembly。

AL.exe 甚至可以用来只包含 manifest 的 exe 或 dll,而真正的 IL 都在其他的 module 中,就好比之前的例子我们可以改为: 

csc /t:module RUT.cs
csc /t:module FUT.cs
al /out:JeffTypes.dll /t:library FUT.netmodule RUT.netmodule

 

Adding Resource Files to an Assembly 

使用 /embed[resource] 选项,可以把文件签入 PE 文件,而且更新 manifest 的 ManifestResourceDef 表。

而使用 /link[resource] 选项,则只更新 manifest 的 ManifestResourceDef 表 和 FileDef 表,而不会签入文件。

C# 也有相应的选项,/resource 和 /linksource 两个选项的作用分别对应上 AL.exe 的两个选项。

/win32res , AL.exe 和 CSC.exe 都支持这个选项,用来指定要包含的 win32 resoruce。

/win32icon,AL.exe 和 CSC.exe 都支持这个选项,用来指定图标。

 

Assembly Version Resource Information 

一个 PE 中有多个关于版本的属性,所有的版本信息都是按照下面的 scheme 来标示的:

下面是容易混淆的,各个版本信息的作用:

AssemblyFileVersion:只用来向外提供一些public 的 information,CLR 不会考察这个熟悉。

AssemblyInformationalVersion:同样,CLR 不关心这个属性,这个版本号用来标示包含这个 PE 的产品的版本号。

AssemblyVersion: CLR 通过这个版本号来唯一识别 PE,它存储在 AssemblyRef 表中。

 

Culture

就像 AssemblyVersion,culture也用来标示 assembly。

通常情况下,我们没有给 assembly 指定 culture,这样的程序集我们称他为 culture neutral。微软建议我们先创建 culture neutral 的 dll, 然后把只包含特殊culture的资源文件编译为 satellite assembly,再签入到有 IL 的程序集中。

使用 /c[ulture] text 选项来生成 satellite assembly, text 是类似 en-US 的格式。

生成的 satellite assembly,应该放在 PE 当前目录下的类似 en-US 的子目录下。

想要在运行时访问 satellite assembly 的内容,要使用 System.Resources.ResourceManager 类。

 

Simple Application Deployment (Privately Deployed Assemblies)

程序集不需要特殊的方法来做打包,把他们一起拷贝的目标目录就可以了。当然我们也可以采用其他的方式来打包和安装,例如 .cab 文件,MSI 安装文件。

这种通过把所有的 assembly 拷贝到同一个文件夹的方式叫做 privately deployed。这种方式有很多的有点:安装简单、卸载简单(删除目录即可)、不需要借助其他的工具例如注册表。