深入 Vscode 源码(3)——构建系统

1 前言

VSCode 能够在各大平台(Windows/Mac/Linux/Web/…)上遍地开花,离不开其强大的跨平台能力。而这个能力不仅依赖于 electron 与 chromium 自身的平台兼容性,还依赖于其复杂的构建系统。

本文旨在深入探索 VSCode 的构建系统,探究其构建过程、依赖管理、工具和配置等。在文末,还会介绍 vscode 实现跨平台构建的具体方案。

2 VSCode 构建系统概述

VSCode 的构建系统并没有使用前端常用的 Webpack、Rollup 等方案,也没有采用开箱即用的脚手架,而是依赖于自己从头搭建的一套工具链,基于 npm + gulpnpm 用于管理项目依赖, gulp 用于自动化构建任务。(几年前 VSCode 使用yarn管理依赖,现在已经迁移到npm

下面我们从文件目录结构的角度来总览 VSCode 的构建系统:

2.1 文件目录结构

与构建相关的源码文件位于 ./build ,与 ./src 目录平级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
./build
├─azure-pipelines # azure CI/CD相关的配置文件和脚本
├─builtin # 内置的一个基于electron开发的、能独立运行的小工具,用于管理内置扩展
├─checksums # 包含electron和node等二进制模块的预计算哈希值,用于验证完整性
├─darwin # MacOS平台特有的构建脚本,以及一些在MacOS目标平台构建完毕之后、打包之前运行的处理脚本。由于MacOS有严格的package校验策略,因此使用 electron-osx-sign 库对构建产物进行代码签名。
├─lib # 构建的主要逻辑存放在这里,是最为关键的一个目录
│ ├─mangle # mangle 库负责遍历TS语言服务提供的AST,进而实现压缩、优化代码。说的详细一点,这就是一个TS到TS的转换器,其功能是对所有类中的private和protected字段进行名称混淆(mangling),使用无意义的短字符串将其代替,以减少代码体积。
│ ├─stylelint # stylelint 库负责检查 CSS 代码的风格。
│ ├─test
│ ├─tsb
│ ├─typings
│ ├─watch
│ ├─bundle.ts
│ ├─compilation.ts
│ ├─nls.ts # i18n 国际化的处理:在编译阶段自动替换源码中的 `localize` 函数,runtime时就可以根据用户的语言偏好进行动态切换了。
├─linux # linux平台特有的构建脚本,以及包含一些在linux目标平台构建完毕之后、打包之前运行的处理脚本,主要是处理linux的各种依赖项问题。
│ ├─debian
│ └─rpm
├─monaco
├─npm
│ └─gyp
├─win32 # win32平台特有的构建脚本
└─i18n
├─gulpfile.js # gulp 任务的入口文件
├─gulpfile.vscode.js # 客户端主构建脚本
├─gulpfile.extensions.js # 扩展主构建脚本
├─gulpfile.vscode.linux.js # Linux客户端构建脚本
├─gulpfile.vscode.win32.js # Windows客户端构建脚本
├─gulpfile.vscode.web.js # web端构建脚本
├─tsconfig.base.json # 基础 TypeScript 配置文件
├─tsconfig.json # VSCode 项目 TypeScript 配置文件

构建的主要逻辑存放在 ./build/lib 目录下,

2.2 构建脚本

2.2.1 Package.json

package.json,位于项目根目录,包含项目元数据、依赖、脚本等。

重点是其 scripts 字段包含一些常用构建命令,比如 compilewatchnpmCheckJs。我们把 vscode 仓库克隆到本地后,在命令行中运行 npm install && npm run watch 命令即可编译项目,并且会自动启动 watcher 监听文件更改+文件更改后自动增量编译。这对于开发者而言无疑是非常方便的,我也会在后文中详细介绍其实现原理。

2.2.2 gulpfile.js

另一个重要的文件是 gulpfile.js,也位于项目根目录,是 gulp 任务的入口文件。它引用了 ./build/gulpfile.js,这个文件定义了一系列的 gulp 任务,用于自动化构建过程。

Gulp 是一个基于 Node.js 的并行任务运行器,常用于前端开发流程的自动化,比如编译、压缩代码、优化图片等重复性任务。其使用 streams 来处理文件。

相比于前端常见的构建系统如 Webpack、Rollup 等,Gulp 是基于任务的,而不像 Webpack 那样基于模块。

./build/gulpfile.js通过gulp.task注册以下任务:

任务名 作用
API 提案名称编译和监视 compileApiProposalNamesTask 编译 API 提案名称,watchApiProposalNamesTask 监视文件变化。
SWC 客户端转译 清理输出目录 (out),然后建立 web path,编译客户端代码,并使用 SWC 转译为 ES5 代码。
仅转译客户端代码 编译客户端代码,但不使用 SWC 转译。
快速编译(供开发时使用) 编译客户端代码,并监视文件变化,以便于开发过程中的即时反馈。 有这三个任务:compile-client,watch-client,watch-client-amd
AMD 模块格式的监视 监视 AMD 模块格式下的代码变化。
所有任务 _compileTask 是一个并行任务,它同时执行多个其他任务,包括类型检查、客户端编译、扩展编译等等。
默认任务 当直接运行 gulp 时,默认执行 _compileTask
未完待续…

从上面的任务列表中不难发现,Vscode 支持使用近年来流行的 SWC(Speedy Web Compiler)来转译代码,相比于传统的 Babel 转译,由 Rust 编写的 SWC 编译速度快的多。

这个脚本最后一行代码值得注意:

1
2
3
4
// Load all the gulpfiles only if running tasks other than the editor tasks
require("glob")
.sync("gulpfile.*.js", { cwd: __dirname })
.forEach((f) => require(`./${f}`));

鉴于 vscode 构建系统的复杂性,如果吧所有构建逻辑都塞在 gulpfile.js 中,那么该文件会很臃肿,因此 Vscode 使用了 glob 模块来加载当前目录下的所有符合特定模式(gulpfile.*.js)的 Gulp 配置文件,比如客户端的主构建脚本gulpfile.vscode.js就是通过此方式进行加载的。

2.2.3 其他构建脚本

tsconfig.build.json: 位于 build 目录下,定义了 TypeScript 编译的配置选项。

1
2
3
4
5
6
7
8
9
10
11
12
// build/tsconfig.build.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowJs": false,
"checkJs": false,
"noEmit": false,
"skipLibCheck": true
},
"include": ["**/*.ts"],
"exclude": ["lib/eslint-plugin-vscode/**/*"]
}

tsconfig.json: 位于 SRC 目录,在 TS 转 JS 的过程中会使用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/tsconfig.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"esModuleInterop": true,
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": false,
"allowJs": true,
"resolveJsonModule": true,
"isolatedModules": false,
"outDir": "../out/vs",
"types": [
"@webgpu/types",
"mocha",
"semver",
"sinon",
"trusted-types",
"winreg",
"wicg-file-system-access"
],
"plugins": [
{
"name": "tsec",
"exemptionConfig": "./tsec.exemptions.json"
}
]
},
"include": [
"./bootstrap-cli.js",
"./cli.js",
"./main.js",
"./server-cli.js",
"./vs/base/common/jsonc.js",
"./vs/workbench/contrib/issue/electron-sandbox/issueReporter.js",
"./typings",
"./vs/**/*.ts"
// 受篇幅所限,省略了很多文件
]
}

这个文件通过 "outDir": "../out/vs" 指定编译输出目录,"include" 指定编译范围, "./vs/**/*.ts" 基本上包含了所有 TypeScript 源代码。

3 构建过程概览

从源码到安装包,是一个完整的构建过程。下面我们先用一张图来说明 VSCode 的构建过程:

从上图可以看出,VSCode 的构建过程主要分为这几个阶段:编译、优化、压缩、打包、分发。gulpfile.vscode.js 是整个构建过程的主要入口,它把上面提到的每一个阶段都封装成了一个独立的 gulp 任务。

整个过程使用到的一些第三方工具库如下:

首先最重要的就是 TS 语言服务了,编译流程的大部分时间都花在了 TS 语言服务的 AST 解析、类型检查、代码生成等阶段。下面这张图展示了 TS 的架构,TS 语言服务能力正是建立在核心编译器基础之上的:

mangle 库负责遍历 TS Language Service 提供的 AST,进而实现压缩、优化代码。说的详细一点,这就是一个 TS 到 TS 的转换器,其功能是对所有类中的 private 和 protected 字段进行名称混淆(mangling),使用无意义的短字符串将其代替,以减少代码体积。

在经过 mangle 处理后,紧接着就是 PostCSS 的样式处理,进行变量替换、函数调用、条件判断等操作,最终生成目标 CSS 代码。PostCSS 的设计理念类似 webpack,本身并不提供具体的编译功能,而是暴露了一堆 API,进而派生出一系列的 plugin,开发者可以选择安装使用。其架构如下:

VSCode 这里用到了 PostCSS Nesting 插件,让开发者能以嵌套的方式编写 CSS 规则,类似于 Sass 等预处理器的写法,但它是遵循 CSS Nesting 规范的。在优化阶段则是使用了 cssnano 插件来压缩 CSS 代码。

为了压缩 JS 代码,VSCode 使用了 ESBuild。我们知道 Vite 的速度非常快, 正是因为使用了由 Go 编写的 EsBuild, 能够利用原生的性能优势。

4 development build 过程探究

从开发者键入 npm run watch 命令到 development build 完成,这之间发生了什么?本章节试图分析 VSCode 的构建过程,捋清楚构建期间 config 和构建产物的流向,进而理解 vscode 构建系统的工作原理。

先把构建系统的一些核心思想梳理一下:

  1. 文件即模块:VSCode 的构建系统是基于文件的,一个文件对应于一个模块。无论是构建过程还是依赖管理过程,都以文件为单位,并且把文件名(包括路径)作为依赖项的唯一标识符。

  2. 一切皆流:得益于 gulp,VSCode 的构建系统是流式的,绝大多数输入和输出都是流。可以很好的利用 CPU 资源,并行处理任务,提高构建速度。需注意这里提到的“流”的最小单位是文件。

  3. 高可追溯性:构建系统本身支持性能打点统计与进度报告,并有内置的日志系统。并且对 sourceMapping 的支持也很好。

4.1 初始化阶段(创建任务、创建各种流)

当开发者在命令行中键入 npm run watch 命令后,npm 读取 package.json 文件,找到其中的 watch 脚本:

1
npm-run-all -lp watch-client watch-extensions

npm-run-all 可以让我们在 Nodejs 命令行以并行或串行的方式运行多个命令,在这里-lp参数表示并行运行,它会并行运行 watch-clientwatch-extensions 两个任务。这两个任务的定义依然可以在 package.json 文件中找到:

1
2
"watch-client": "node --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js watch-client",
"watch-extensions": "node --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js watch-extensions watch-extension-media",

之所以要把任务一分为二,是因为 vscode 的主逻辑与拓展之间是解耦的,二者需要单独编译,watch-client 任务只编译客户端代码,而 watch-extensions 任务则编译扩展代码。下面我们来看一下watch-client具体实现。(watch-extensions的实现异曲同工,读者可以自行探索一下。)

watch-extensions这一步会首先设置 Nodejs 内存限制 node --max-old-space-size=8192,设置 V8 引擎的最大老生代空间大小为 8192 MB,即 8 GiB,避免出现构建时期爆内存的尴尬情况。

随后直接调用本地安装的 Gulp CLI 来运行 Gulp 任务 watch-client,它是这样定义的:

1
2
3
4
5
6
7
8
9
const watchClientTask = task.define(
"watch-client",
task.series(
util.rimraf("out"),
util.buildWebNodePaths("out"),
task.parallel(watchTask("out", false), watchApiProposalNamesTask)
)
);
gulp.task(watchClientTask); // register the task

其具体行为:

  1. 首先递归清理掉 out 目录(这个就是存放构建结果的目录),确保旧的编译结果不会干扰新的构建。

  2. 然后 call buildWebNodePaths函数在输出目录按照要求生成一个包含版权信息的 js 文件,这个函数用于在构建过程中配置 Web 环境下的 Nodejs 路径。

  3. 并行执行监视任务,包括:

    • watchTask('out', false) 开始监视源代码的变化,在检测到变化时自动重新编译。
    • watchApiProposalNamesTask 同时也会监视 API 提案名称的相关文件。

这里承担了编译的重任的自然是 watchTask,它是这样定义的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// build/lib/compilation.ts
export function watchTask(
out: string,
build: boolean,
srcPath: string = "src"
): task.StreamTask {
const task = () => {
// 调用 createCompile 函数创建一个编译任务,
// 其中 srcPath 是源代码目录的路径,
// build 表示是否进行构建,emitError: false 表示不输出错误信息
const compile = createCompile(srcPath, {
build,
emitError: false,
transpileOnly: false,
preserveEnglish: false,
});

const src = gulp.src(`${srcPath}/**`, { base: srcPath });
// 使用 watch 方法监视源代码目录下的所有文件,
// base 属性设置为 srcPath,
// readDelay: 200 表示延迟 200 毫秒读取文件,避免频繁的文件读取操作
const watchSrc = watch(`${srcPath}/**`, { base: srcPath, readDelay: 200 });

// 创建一个 MonacoGenerator 实例,用于生成 Monaco 编辑器的 API 声明文件
const generator = new MonacoGenerator(true);
generator.execute();

// 将监视到的文件变化通过管道传递给 generator.stream,
// 然后通过 util.incremental 方法进行增量编译,
// 最后将编译结果输出到指定的输出目录 out
return watchSrc
.pipe(generator.stream)
.pipe(util.incremental(compile, src, true))
.pipe(gulp.dest(out));
};
task.taskName = `watch-${path.basename(out)}`;
return task;
}

实现编译的关键在于最后一步,watchSrc 是由watch方法返回的可读Stream,紧随其后的是流的链式处理管道,依次将监视到的文件变化通过管道传递给 generator.stream,然后通过 util.incremental 方法进行增量编译,最后将编译结果输出到指定的输出目录 out

util.incremental的目的是为了创建一个新的读写流,该流可以增量的处理数据,并且根据需要可以支持取消功能,且支持状态的防抖,防止频繁的触发操作。其接受IStreamProvider作为入参,这是一个函数式接口,定义了增量编译的基本操作:

1
2
3
4
5
6
7
export interface ICancellationToken {
isCancellationRequested(): boolean;
}

export interface IStreamProvider {
(cancellationToken?: ICancellationToken): NodeJS.ReadWriteStream;
}

ICancellationToken ,用于判断是否需要取消当前的编译任务,这是一个返回布尔值的无副作用函数,可以通过轮询来实现构建操作的取消。IStreamProvider 接口定义了一个流提供者接口,用于提供一个可读可写的流。

由于util.incremental仅仅是一个工具函数,用于函数式的处理流数据的增量更新,本身不包含编译逻辑,而且内部实现比较复杂,因此不做过多分析。

4.2 编译过程(调用 tsc)

我们一层一层剥开上面提到的 watchTask 的调用栈:

watchTask调用createCompile,后者会在底层继续调用createTypeScriptBuilder,这是一个工厂函数,其接收配置信息、项目文件路径、编译命令行参数,然后返回一个ITypeScriptBuilder(顾名思义,他能够对 TypeScript 文件进行编译)。ITypeScriptBuilder 接口定义了两个主要的方法:

  • file(...): 用于添加/更新单个文件到构建系统
  • build(...): 开始构建过程,编译所有必要的文件

下面我们按照时间顺序罗列这个 build(...) 的工作步骤:

  1. 文件输出 (emitSoon) 编译生成目标文件(将 TS 编译为 JS 和 .d.ts 声明文件)。此步骤还会处理 source map 文件,以便调试时可以回溯到原始的 TS 源码。

  2. 语法检查 (checkSyntaxSoon) 对每个需要编译的文件进行语法分析,如果发现了语法错误,则立即终止后续的语义检查。

  3. 语义检查 (checkSemanticsSoon) 进行语义分析(检查类型是否正确使用,变量是否已声明等更深层次的问题)。

  4. 依赖关系管理,追踪模块之间的依赖关系。如果某个文件发生了变化,那么所有依赖该文件的其他文件也会被重新检查+编译。

  5. 顺便记录此次构建的时间消耗和内存占用情况,以便优化。

前三个步骤承担了编译的重任,它们直接依赖于 Typescript 官方 API 所提供的语言服务(Language Service),通过该服务获取文件的编译输出、获取语法检查能力等等。

简化版的代码实现如下:

1
2
3
4
5
6
7
import * as ts from "typescript";

const service = ts.createLanguageService(host, ts.createDocumentRegistry());
const output = service.getEmitOutput(fileName);
for (const file of output.outputFiles) {
// 顺利拿到了编译后的文件内容,可以进行后续处理...
}

我们逐行分析上述代码:

1
const service = ts.createLanguageService(host, ts.createDocumentRegistry());

这里调用了createLanguageService方法来创建一个语言服务实例。host参数是一个实现了ts.LanguageServiceHost接口的对象,提供对文件系统访问的能力以及其他必要的环境信息,以此来桥接构建器与 TypeScript 编译器之间的交互。

ts.createDocumentRegistry()则用于管理文档状态,确保在多线程环境中正确处理文档。语言服务可以用来执行诸如查找符号定义、自动完成、重命名符号等操作。

1
const output = service.getEmitOutput(fileName);

getEmitOutput是 LanguageService 提供的一个方法,用于获取指定文件名对应的编译结果。此方法返回一个包含多个输出文件的对象(即EmitOutput),这些输出文件包括编译后的 JS 文件、声明文件(.d.ts)、map 文件等。fileName参数指定了要编译的具体文件路径。

1
2
3
for (const file of output.outputFiles) {
// 顺利拿到了编译后的文件内容,可以进行后续处理...
}

最后一部分遍历了由getEmitOutput返回的outputFiles数组。每个元素代表一个输出文件的信息。具体而言,Vscode 构建系统在这一步会这么做:对于 .d.ts 文件,生成文件的签名;对于 .js 文件和 .js.map 文件,生成相应的 Vinyl 虚拟文件对象,通过函数式接口 out(file) 将文件推送到流中,以便该文件可以继续沿着流进行处理,直到被写入目的地。

综上,vscode 的构建系统是基于 TypeScript 官方 API 所提供的语言服务(tsc)来实现的,并未采用 babel 之类的工具。

4.2.1 补充内容:依赖管理

上面 build(...) 的工作步骤的第四、五步,即依赖管理,它也是构建器的核心功能之一。在这里稍微用一点篇幅介绍一下依赖管理的原理。

依赖管理的核心思想是:只对依赖链上发生变化的文件进行增量编译,确保当且仅当某个文件的 sha256 签名发生变化时,所有依赖于该文件的其他文件也会被重新检查。

调用 collectDependents 方法来收集依赖于当前文件的所有其他文件,并将它们推入 dependentFiles 数组内,稍后会被重新处理。与前一篇文章提到的 DI 系统类似,文件之间的依赖关系也是通过一个有向无环图来维护。

5 多平台构建原理

5.1 跨平台代码架构

VSCode的跨平台能力并不仅仅由其构建系统所赋予,还得益于其优秀的架构设计。因此我们先来看一下VSCode的核心代码架构。

5.1.1 核心分层架构

VSCode 核心代码被划分为多个层次,此分层架构使得代码可以在不同的 runtime 中尽可能多的得到复用,同时降低治理成本。官方博客对此有提及,在此引用:

  • Base 层:提供通用的工具类和用户界面组件,这些组件可以在任何其他层中使用。
  • Platform 层:定义服务注入支持和 VS Code 的基础服务,这些服务在多个层之间共享,如 workbenchcode 层。
  • Editor 层:包含 Monaco 编辑器,该编辑器可以作为一个独立的组件使用。
  • Workbench 层:负责管理编辑器、笔记本和自定义编辑器,并提供面板框架(如资源管理器、状态栏、菜单栏等)。
  • Code 层:桌面应用的入口点,负责将各个模块整合在一起,包括 Electron 主进程文件、共享进程和 CLI。
  • Server 层:远程开发的入口点,支持远程开发场景。

5.1.2 模块化设计

前面提到的分层架构对于 VSCode 这类大型 TS 项目还远远不够,每一层肯定都需要有大量模块化设计,把业务逻辑解耦到不同模块独立维护。每个模块既可以通过依赖注入的方式提供服务,也能够以 standalone 的形态存在,被其他模块所引入workbench 层通过扩展机制(Contrib)来集成各种功能模块,每个模块通过 .contribution.ts 文件注册到系统中。

5.1.3 【重点】模块的多平台支持

这些模块在编写时,内部逻辑会根据目标环境(commonbrowsernodeelectron-sandbox 等)进行划分,避免在错误的平台上加载不兼容的代码,同时也能做到细粒度的 coverage 控制(tree-shaking)。

目标环境之间详细的区别如下(同样取自官方博客):

  • Common:仅依赖基本 JavaScript API 的代码,可以在所有其他目标环境中运行。
  • Browser:依赖 Web API(如 DOM)的代码,适用于浏览器环境。
  • Node:依赖 Node.js API 的代码,适用于 Node.js 环境。
  • Electron-sandbox:依赖浏览器 API 和 Electron 渲染进程 API 的代码,适用于 Electron 渲染进程。
  • Electron-utility:依赖 Electron 工具进程 API 的代码,适用于 Electron 工具进程。
  • Electron-main:依赖 Electron 主进程 API 的代码,适用于 Electron 主进程。

5.1.4 服务抽象接口与实现分离

如第一篇文章所述,每个服务都有一个 Interface 和多个平台特定的实现。通过依赖注入,模块不需要关心具体的实现细节,只需依赖接口即可。DI 系统和构建系统会根据目标平台注入正确的实现。例如,IFileService 是一个文件操作的接口,而它的实现可以是:

  • DiskFileSystemProvider, Electron 桌面端,直接操作本地文件系统
  • RemoteFileSystemProvider, Web 端,通过远程服务操作文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// desktop.main.ts
// 文件服务接口
const fileService = this._register(new FileService(logService));
serviceCollection.set(IFileService, fileService);

// Electron 桌面端的文件系统实现
const diskFileSystemProvider = this._register(
new DiskFileSystemProvider(
mainProcessService,
utilityProcessWorkerWorkbenchService,
logService,
loggerService
)
);
fileService.registerProvider(Schemas.file, diskFileSystemProvider);

// Web 端的远程文件系统实现
this._register(
RemoteFileSystemProviderClient.register(
remoteAgentService,
fileService,
logService
)
);

5.2 差异化CI

从源码开始构建,打包出最终的安装包,vscode将其分为两大主要步骤:

  1. 主任务:编译、压缩、合并源码,处理资源文件等。为了降低心智负担,vscode的构建系统确保主任务是平台无关的,各个目标平台使用同一套构建脚本
  2. CI:运行在主任务之后,其主要职责:编译native扩展、打包安装包等。这一步与目标平台强相关,因此整个构建流程的差异性就是在CI这一步体现的

下面代码展示了各平台构建任务是如何生成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// build/gulpfile.vscode.js
const BUILD_TARGETS = [
{ platform: 'win32', arch: 'x64' },
{ platform: 'win32', arch: 'arm64' },
{ platform: 'darwin', arch: 'x64', opts: { stats: true } },
{ platform: 'darwin', arch: 'arm64', opts: { stats: true } },
{ platform: 'linux', arch: 'x64' },
{ platform: 'linux', arch: 'armhf' },
{ platform: 'linux', arch: 'arm64' },
];
BUILD_TARGETS.forEach(buildTarget => {
const dashed = (str) => (str ? `-${str}` : ``);
const platform = buildTarget.platform;
const arch = buildTarget.arch;
const opts = buildTarget.opts;

const [vscode, vscodeMin] = ['', 'min'].map(minified => {
const sourceFolderName = `out-vscode${dashed(minified)}`;
const destinationFolderName = `VSCode${dashed(platform)}${dashed(arch)}`;

const tasks = [
compileNativeExtensionsBuildTask,
util.rimraf(path.join(buildRoot, destinationFolderName)),
packageTask(platform, arch, sourceFolderName, destinationFolderName, opts)
];

if (platform === 'win32') {
tasks.push(patchWin32DependenciesTask(destinationFolderName));
}

const vscodeTaskCI = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series(...tasks));
gulp.task(vscodeTaskCI);

const vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series(
compileBuildTask,
cleanExtensionsBuildTask,
compileNonNativeExtensionsBuildTask,
compileExtensionMediaBuildTask,
minified ? minifyVSCodeTask : bundleVSCodeTask,
vscodeTaskCI
));
gulp.task(vscodeTask);

return vscodeTask;
});

if (process.platform === platform && process.arch === arch) {
gulp.task(task.define('vscode', task.series(vscode)));
gulp.task(task.define('vscode-min', task.series(vscodeMin)));
}
});

这里使用了一个数组 BUILD_TARGETS 来定义所有的构建目标,每个目标包含平台、架构和可选的配置项。然后对其进行 forEach 遍历,为每个目标生成对应的构建任务。