首页
/ PowerShell 构建流程内幕解析:顶层工程、Dummy 依赖、ResGen 与类型目录生成机制

PowerShell 构建流程内幕解析:顶层工程、Dummy 依赖、ResGen 与类型目录生成机制

2026-09-05 19:37:53作者:薛曦旖Francesca

本文基于 PowerShell 仓库的 docs/building/internals.md 撰写,深入剖析 PowerShell 从源码构建时的关键内部机制:为何要在顶层工程中引入"假依赖"、ResGen 强类型资源类与 CoreCLR 类型目录(Type Catalog)这两个构建前置步骤的工作方式,以及 pwrshplugin.dlllibpsl-native 等原生组件如何通过 NuGet 包实现"一次构建、多次复用"。读完本文,你可以独立手工执行 Start-PSBuild 背后的每一步构建前置动作,并能定位"missing strings"、InitializeTypeCatalog does not exist 等典型构建错误。

需要说明的前提:该文档自述"并非完整,最终事实来源是在对应 CI 系统上实际执行的 build.psm1"。本文所有函数名、命令均以当前仓库中的构建脚本与工程文件为准。

一、构建入口:只构建一个"$Top"顶层工程

PowerShell 的构建策略是:对仓库中的顶层目录执行一次 dotnet 构建,由该目录汇总拉起整个 CoreCLR 版本的所有程序集。文档明确指出顶层目录(文中称 $Top)为:

  • src/powershell-win-core:Windows 上 CoreCLR 版本的顶层工程;
  • src/powershell-unix:Linux 与 macOS 上 CoreCLR 版本的顶层工程。

两个工程都复用同一个 C# 入口 Program.cs,输出程序集名均为 pwsh。可以从 powershell-win-core.csproj 看到 Windows 版声明了 win-x86;win-x64 两个 RuntimeIdentifier,并额外打包了 Windows 专属的 Modules 内容(..\Modules\Windows\**、WSMan/诊断相关 csproj 等);而 powershell-unix.csproj 声明的是 linux-x64;osx-x64,内容项对应 ..\Modules\Unix\**。这印证了文档中"同一套核心、分平台顶层工程"的组织方式。

build.psm1 中,Start-PSBuild(约 L336 起)是对外的主构建函数,Start-PSBootstrap(约 L2848 起)负责首次安装构建前置依赖(.NET SDK 等),二者共同构成"一键构建"的入口。文档的前提也正是"你已能在本机成功从源码构建 PowerShell",internals 文档面向的是构建已经跑通、需要理解细节的读者。

二、Dummy Dependencies:让"只构建一个文件夹"成为可能

为什么要有假依赖

文档指出,各工程之间使用了 **dummy dependencies(假依赖)**来利用 dotnet 构建的功能:例如 src/powershell-win-core/powershell-win-core.csproj 声明了对 Microsoft.PowerShell.Commands.Diagnostics.csproj 的 ProjectReference,但实际上二者之间并不存在真正的构建期依赖。假依赖的作用是让构建系统沿着引用关系自动传递、编译所有相关工程——于是只需构建 $Top 一个目录,而不必逐个构建多个文件夹。

从源码结构看,这一点可以直接验证。powershell-win-core.csprojItemGroup 中引用了:

  • Microsoft.PowerShell.SDK.csproj
  • Microsoft.PowerShell.Commands.Diagnostics.csproj
  • Microsoft.Management.Infrastructure.CimCmdlets.csproj
  • Microsoft.WSMan.Management.csproj
  • (WindowsDesktop SDK 条件下)Microsoft.PowerShell.GraphicalHost.csproj

powershell-unix.csproj 只引用了 Microsoft.PowerShell.SDK.csproj 一项——因为 Unix 构建不需要诊断、CimCmdlets、WSMan 等 Windows 专属组件。SDK 工程内部再把这些组件串成引用链,最终形成从 $Top 出发的完整构建闭包。

假依赖的规则

文档给出了维护这条依赖链的规则:凡是参与 CoreCLR 构建的程序集,都必须列为 $Top 文件夹(src/powershell-unixsrc/powershell-win-core,按其所属平台)的依赖项。换言之,新增一个要编入发布的程序集时,不能只改 SDK 或某个中间工程的引用,还要确认它最终能被两个(或对应平台的)顶层工程"拉到"构建图中,否则该程序集不会出现在构建产物里。

三、构建前置步骤一:ResGen 生成强类型资源类

背景:为什么要自研 ResGen

在 .NET CLI 的 dotnet-resgen 工具支持生成强类型资源访问类之前(文档引用了上游 msbuild 的跟踪 issue),PowerShell 自行实现了一个 C# 工具,源码位于 src/ResGenStart-PSBuild 会通过 build.psm1 中的 Start-ResGen 函数(L3292-L3313,内部就是切到 src/ResGen 后执行 dotnet run)自动执行这一步;但它不依赖 PowerShell 环境,可以完全手工运行:

cd src/ResGen
dotnet restore
dotnet run

工具做了什么

运行后,工具对每个项目执行以下工作(对应 Program.csMain 逻辑):

  1. 遍历 src 下的各工程目录,凡存在 resources 文件夹的,创建同级 gen 文件夹;
  2. resources 中的每个 *.resx 文件,解析其 <data> 节点,生成一个强类型 C# 资源访问类,写入 gen 文件夹下对应的 *.cs 文件。

Program.cs 的实现可以看到一些细节:resx 文件名默认生成 internal 访问级别的类;若文件名以 public. 前缀开头,则生成 public 类并去掉该前缀(例如 public.MyResource.resxpublic class MyResource)。生成的类包含缓存的 ResourceManager 属性(资源名格式为 "{模块名}.resources.{类名}")以及每个字符串对应一个属性,属性内嵌 doc 注释给出原文提示。支持 dotnet run <path-to-resx> 单文件模式,也支持无参数的全量模式。

为什么不能每次构建都重跑

文档特别强调:这些 gen 文件不会在每次构建时自动更新,因为项目缺乏文件变更检测能力——每次构建都重新生成会破坏增量重编译。由此得到一个重要的运维结论:

如果你拉取新提交后出现"缺少字符串"(missing strings)错误,很可能需要删除 gen 文件夹并重新运行 ResGen 工具

这是"拉代码后编译失败"类问题中最常见的一种,排查时优先检查各工程的 gen 目录是否为陈旧版本。

四、构建前置步骤二:Type Catalog 生成

用途

PowerShell 在 CoreCLR 下使用一份预先生成的 C# 类型目录来辅助类型解析。其原理可以从 TypeCatalogGen.cs 的注释中得到印证:CoreCLR 中没有直接枚举"全部已加载 TPA 程序集"的 API,为了按类型名定位 .NET 类型,必须事先知道哪些 .NET 类型存在、分别位于哪个 TPA 程序集——因此构建期基于 .NET 的引用程序集生成一份"类型全名 → 程序集强名称"的目录,编译进程序集加载上下文初始化代码中。工具会遍历引用程序集中的 TypeDefinition,只收录 public/nested public 可见性的类型(TypeCatalogGen.cs)。

标准调用链与手工执行

生成类型目录是构建前置步骤,由 build.psm1 中的 Start-TypeGen(L3254-L3290)执行,并被 Start-PSBuild 调用。同样,它不要求 PowerShell 参与,可以完全手工复现,分两步:

第一步,在 src 目录下通过自定义 MSBuild 目标导出依赖 DLL 路径列表 powershell.inc

targetFile="Microsoft.PowerShell.SDK/obj/Microsoft.PowerShell.SDK.csproj.TypeCatalog.targets"
cat > $targetFile <<-"EOF"
<Project>
    <Target Name="_GetDependencies"
            DependsOnTargets="ResolveAssemblyReferencesDesignTime">
        <ItemGroup>
            <_RefAssemblyPath Include="%(_ReferencesFromRAR.HintPath)%3B"  Condition=" '%(_ReferencesFromRAR.NuGetPackageId)' != 'Microsoft.Management.Infrastructure' "/>
        </ItemGroup>
        <WriteLinesToFile File="$(_DependencyFile)" Lines="@(_RefAssemblyPath)" Overwrite="true" />
    </Target>
</Project>
EOF
dotnet msbuild Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj /t:_GetDependencies "/property:DesignTimeBuild=true;_DependencyFile=$(pwd)/TypeCatalogGen/powershell.inc" /nologo

注意该目标刻意排除了 Microsoft.Management.Infrastructure 包提供的引用——从源码结构看,这是因为它由原生组件包(见下文 libpsl/PSRP 相关包)按 RID 提供,不参与类型目录的解析输入。

第二步,运行 TypeCatalogGen 工具,以 powershell.inc 为输入生成 CorePsTypeCatalog.cs

cd ../TypeCatalogGen
dotnet restore
dotnet run ../System.Management.Automation/CoreCLR/CorePsTypeCatalog.cs powershell.inc

powershell.inc 包含 PowerShell 各依赖项 DLL 的解析后路径,作为 TypeCatalogGen 工具的输入;工具据此生成源码文件 CorePsTypeCatalog.cs,供 System.Management.Automation 工程编译(从 build.psm1 的实现看,Start-TypeGen 还支持通过 -IncFileName 参数改变 .inc 文件名,以支持 Windows 与 WSL 同时构建时的文件隔离)。

典型错误定位

文档给出了一个标志性错误:

The name 'InitializeTypeCatalog' does not exist in the current context

它意味着 CorePsTypeCatalog.cs 这个生成文件不存在,解决方法就是按上述步骤重新生成。由于 CorePsTypeCatalog.cs 属于生成产物而非签入文件,仓库中不会出现它,这一点与 ResGen 的 gen 目录性质相同:都是"构建前置步骤"的产物,缺失时按步骤重新生成即可。

五、原生组件:打包成 NuGet,一次构建多次复用

设计动机

文档"Native Components"一节说明了原生组件的依赖关系与包装理由:

  • Windows 上,PowerShell Core 依赖 WinRM 插件 pwrshplugin.dll 以提供基于 WinRM 的远程管理(remoting);
  • Linux/macOS 上,依赖 libpsl-native.so/libpsl-native.dylib 二进制提供一些必要支持。

构建这些原生组件需要额外安装依赖(对不改原生代码的构建者来说是负担),而原生组件代码又很少变动,每次都随 Start-PSBuild 重新编译没有意义。因此决策是:把原生组件打包成 NuGet 包,改动时才重新构建,之后长期复用产物二进制。对应关系为:

原生组件 NuGet 包
pwrshplugin.dll psrp.windows
libpsl-native libpsl

需要指出当前仓库状态的一个演进事实:src/libpsl-native/README.md 声明该目录下的原生代码正在迁移到独立的 PowerShell-native 仓库,后续 PR 应提交到新仓库。也就是说,文档中描述的原生构建流程代表该组件"在仓库内构建"的历史形态,当前仓库保留的 libpsl-native 目录已进入迁移过渡期。

Windows 包:PSRP.Windows 与 PowerShell.Core.Instrumentation

构建 pwrshplugin.dllPowerShell.Core.Instrumentation.dll 需要安装 Visual Studio 2017,并运行 Start-PSBootstrap -BuildWindowsNative 安装前置依赖。文档明确列出需勾选的组件:

  • VC++ 2017 v141 toolset (x86, x64)
  • Visual C++ compilers and libraries for ARM
  • Visual C++ compilers and libraries for ARM64
  • Visual C++ tools for CMake
  • Visual C++ ATL Support
  • Windows 10 SDK (10.0.16299.0) for Desktop C++ (ARM and ARM64)
  • Windows 10 SDK (10.0.16299.0) for Desktop C++ (x86 and x64)

同时需要安装 3.10.0 或更新版本、支持 VS2017 与 ARM64 生成器的 CMake。然后运行 Start-BuildNativeWindowsBinaries 构建二进制,例如构建 arm64 目标的 Release 版本:

Start-BuildNativeWindowsBinaries -Configuration Release -Arch x64_arm64

文档对架构矩阵的要点值得记录:

  • 必须为所有受支持架构构建并测试:x86x64x64_armx64_arm64
  • x64_arm/x64_arm64 表示宿主机须为 x64,交叉编译到 ARM;
  • 多架构连续构建时务必加 -clean 开关——cmake 会缓存上一次运行,不清洗会导致后续架构使用错误的编译器。

构建成功后,pwrshplugin.dll、其 PDB 与 powershell.core.instrumentation.dll 会被放到 src/powershell-win-core 下。

pwrshplugin.dll 创建新的 NuGet 包的完整流程:

  1. 获取 psrp.windows.nuspec:可以从 Windows 机器上 ~/.nuget/packages/psrp.windows(近期构建过 PowerShell 的话存在),或从 powershell-core NuGet feed 下载现有包获得;
  2. 将其复制到空文件夹并更新 <version> 元素;
  3. 把本次构建产物复制进去,还原与现有包一致的文件布局:
\---runtimes
    +---win-x64
    |   \---native
    |           pwrshplugin.dll
    |           pwrshplugin.pdb
    |
    +---win-x86
    |   \---native
    |           pwrshplugin.dll
    |           pwrshplugin.pdb
    +---win-arm
    |   \---native
    |           pwrshplugin.dll
    |           pwrshplugin.pdb
    \---win-arm64
        \---native
                pwrshplugin.dll
                pwrshplugin.pdb
  1. 使用 authenticode dual 证书为 DLL 签名,然后在 runtimes 的父目录(即 psrp.windows.nuspec 所在目录)执行 nuget pack,并使用最新版 nuget.exe;
  2. 将新 nupkg 发布到 powershell-core feed 的 psrp.windows 包。

PowerShell.Core.Instrumentation.dll 的 NuGet 包以同样方式在独立目录中创建,仅 nuspec 不同:它签入在本仓库 src/PowerShell.Core.Instrumentation 目录下(该目录同时包含 ETW 清单 PowerShell.Core.Instrumentation.manRegisterManifest.ps1),发布目标为 powershell-core feed 的 PowerShell.Core.Instrumentation 包。

libpsl 包:跨平台与 CentOS 7 可移植性约束

Unix/macOS 侧的前置与构建命令:

## 构建 linux-x64 或 macOS 目标
Start-BuildNativeUnixBinaries

## 构建 linux-arm 目标
Start-BuildNativeUnixBinaries -BuildLinuxArm

其中 linux-arm 需要先运行 Start-PSBootstrap -BuildLinuxArm 安装额外前置依赖,且当时只能在 Ubuntu 机器上构建;linux-x64 与 macOS 则只需 Start-PSBootstrap 的初始运行即可,无额外前置。

构建成功后,libpsl-native.so(macOS 上为 libpsl-native.dylib)会被放到 src/powershell-unix 下。

创建 libpsl NuGet 包的关键约束与步骤:

  1. 从现有 libpsl 包获取 libpsl.nuspec(本机 ~/.nuget/packages/libpsl,或从 powershell-core feed 下载现有包);
  2. 关键可移植性约束:为了让 linux-x64 的二进制可移植到所有 Linux 发行版,libpsl-native.so 必须在 CentOS 7 上构建——原因是 .NET Core 的 Linux 原生二进制同样在 CentOS 7 上构建,以避免对更新版本 glibc 产生依赖;
  3. 需要构建 linux-x64linux-armosx 三个目标各一份二进制,并放入与现有包一致的布局:
└── runtimes
    ├── linux-arm
    │   └── native
    │       └── libpsl-native.so
    ├── linux-x64
    │   └── native
    │       └── libpsl-native.so
    └── osx
        └── native
            └── libpsl-native.dylib
  1. 在文件夹内执行 nuget pack .(可能需要最新版 nuget.exe)。

六、排错要点汇总

把文档中分散的故障信号汇总成一张速查表:

现象 根因 处置
拉取新提交后报"missing strings" 强类型资源类 gen/*.cs 过期或缺失 删除相关工程的 gen 目录,重新执行 src/ResGendotnet run
The name 'InitializeTypeCatalog' does not exist in the current context 生成文件 CorePsTypeCatalog.cs 不存在 按第四节两步手工流程重新生成(先产出 powershell.inc,再运行 TypeCatalogGen
ARM 交叉编译产物异常 cmake 缓存了上一架构的构建状态 多架构连续构建时始终使用 -clean 开关
linux-x64 包在低版本发行版加载失败 二进制依赖了过新的 glibc 确认 linux-x64 二进制是在 CentOS 7 上构建的
新增程序集未进入发布产物 未遵守 dummy dependencies 规则 将该程序集挂入 src/powershell-unix / src/powershell-win-core 依赖链

最后重申适用前提:本文命令与路径以当前仓库快照为准(顶层工程为 src/powershell-win-coresrc/powershell-unix;构建脚本为 build.psm1)。原生组件部分描述的是组件签入仓库内的构建流程,当前 libpsl-native 代码已启动向独立 PowerShell-native 仓库的迁移,后续原生组件构建与发包请以该独立仓库的流程为准,本文对应章节仍可视为该机制的权威历史说明。

登录后查看全文
热门项目推荐
相关项目推荐