fix(m169): r3 四条就地修完 —— 只认同级 name:、纳入 .cargo/config.toml、登记 env 交接边界、校准两处数字

合并门禁只认 clean,r3 的 p2-4items 就地修掉(无行为改动,判据更严,不放松任何一条)。

P2-1(一行 + 三向量)`_name_of` 现在只认**同级** `name:`(`item.end() == key_indent`):更深缩进的
`- name:`(`env:`/`with:` 子列表、`run: |` 块标量里的行)不再被当成项名;同一项里出现两个**不同**
的 `name:` 返回 `None`(无法判定 ⇒ 默认拒绝,不去猜 YAML 重复键语义)。新向量:name 藏在 env 子项 /
藏在 `run: |` 里 / 两个不同 name ⇒ 必须红。

P2-2 `.cargo/config*.toml` 进扫描面(`CONFIG_GLOBS`:根与 `crates/*` 下的 `config.toml`/`config`),
新增 `scan_cargo_config`:feature 字面量(覆盖 `--cfg feature="…"`、`[env] RUSTFLAGS`)走原有判据,
`rustc-wrapper` / `rustc-workspace-wrapper` **一律判红**(wrapper 注入什么由程序决定,文本扫描看不见;
合法用途如 sccache 需要 review 明确同意,不能顺手加)。新向量:config.toml 用 `--cfg` 开该 feature
⇒ 红;设 rustc-wrapper ⇒ 红;普通 config.toml ⇒ 绿(防误杀)。

P2-3 运行期 env 交接写进守门自己的「抓不住」表(脚本头 + README §14):第一步把 feature 写进
`$GITHUB_ENV`、第二步只用 `--features "$FEAT"`——第二步没有字面量,**任何静态扫描都看不见**,这是本
守门的固有边界(引 YAML 解析器也修不了,因为这是数据流不是结构),不是漏判。

P2-4 两处过期数字:`release-guard.yml` 的「注入 14 条」→ 26 条注入 + 6 条防误杀/放行(另加基线,
条数以 `--self-test` 输出为准);README 的「26 条:21 红 + 6 绿」→「32 条向量:26 红 + 6 绿 + 基线」。

守门脚本 399 行(≤400,仍零第三方依赖);自测数据 32 条向量在 `packaging/tests/guard_vectors.json`。

验证(本机,未跑全量 cargo —— tower 已在合并态跑过 workspace 全量 EXIT=0/2648 passed):
- `--self-test`:26 条注入全红 + 7 条基线/防误杀绿,exit 0。
- **真仓注入 8 组**(改真 ci.yml / 新建真 .cargo/config.toml,改完即还原):env 子项里的 name ⇒ 红;
  `run: |` 里的 name ⇒ 红;两个不同 name ⇒ 红;`--cfg feature=` ⇒ 红;`rustc-wrapper` ⇒ 红;
  普通 config.toml ⇒ 绿;r2 的无名步骤 / flow 步骤回归 ⇒ 仍红;还原后基线绿。
- 正式扫描(真仓)exit 0;全仓 400 行扫描无超限;改动文件全 LF。
This commit is contained in:
worker-updatee2e committed 2026-11-08 11:27:21 +08:00
1 parent 68a1f90828
commit d7adbd1172
4 files changed
+110 -37

No files matched your search

+5 -4
View File
@@ -27,10 +27,11 @@ name: Release guard
# app-iced 各有一条 `update-test-source` feature,打开后 `core::update::job::default_source` 可以被 # app-iced 各有一条 `update-test-source` feature,打开后 `core::update::job::default_source` 可以被
# 两个环境变量指向任意本地目录——而更新链路的终点是**替换用户盘上的 exe**。它默认关,官方发布产物 # 两个环境变量指向任意本地目录——而更新链路的终点是**替换用户盘上的 exe**。它默认关,官方发布产物
# 里不许有它:守门扫 `.github/workflows/**`(含复合 action)、`packaging/**` 递归脚本、 # 里不许有它:守门扫 `.github/workflows/**`(含复合 action)、`packaging/**` 递归脚本、
# `Makefile`/`justfile`/`Taskfile` 与各 `Cargo.toml`(`default` 里带上它也红,那种开法扫 workflow # `Makefile`/`justfile`/`Taskfile`、各 `Cargo.toml`(`default` 里带上它也红)与 `.cargo/config*.toml`
# 是看不见的)。**唯一允许**是 `ci.yml` 里名为 `update-e2e` 的那一步(且必须带 `--test # (`--cfg feature="…"` 与 `rustc-wrapper` 都能绕过 `--features`)。**唯一允许**是 `ci.yml` 里名为
# update_e2e_test`,文件名/步骤名/参数任一不符即红)。`--self-test` 注入 14 条真实回归(allowlist # `update-e2e` 的那一步(且必须带 `--test update_e2e_test`,文件名/步骤名/参数任一不符即红)。
# 六向量 + 七条绕过 + Cargo 两条),该红的红、该绿的绿。 # `--self-test` 注入 26 条真实回归 + 6 条防误杀/放行用例(另加基线;条数以 `--self-test` 输出为准),
# 该红的红、该绿的绿。
on: on:
push: push:
+8 -4
View File
@@ -66,7 +66,7 @@ Git Bash 下会静默落进 Docker VM(脚本内部用 `cygpath -m` 归一)
| `packaging/check_screenshot.py` | 截图非空白判定(CI 门禁 + 本地手动跑,零第三方依赖) | ≤400 | | `packaging/check_screenshot.py` | 截图非空白判定(CI 门禁 + 本地手动跑,零第三方依赖) | ≤400 |
| `packaging/check_publish_slots.py` | 发布名单守卫(M164):名单 ⇄ 产物两个方向;`--self-test` 注入 8 条回归 | ≤400 | | `packaging/check_publish_slots.py` | 发布名单守卫(M164):名单 ⇄ 产物两个方向;`--self-test` 注入 8 条回归 | ≤400 |
| `packaging/check_test_source_off.py` | 测试通道守门(M169):workflow / 打包脚本 / `Cargo.toml default` 都不许打开 `update-test-source`;归属按 `jobs.*.steps[]` 列表项判、默认拒绝;`--self-test` 按 `tests/guard_vectors.json` 注入 | ≤400 | | `packaging/check_test_source_off.py` | 测试通道守门(M169):workflow / 打包脚本 / `Cargo.toml default` 都不许打开 `update-test-source`;归属按 `jobs.*.steps[]` 列表项判、默认拒绝;`--self-test` 按 `tests/guard_vectors.json` 注入 | ≤400 |
| `packaging/tests/guard_vectors.json` | 守门自测向量(基线 + 26 条:21 红 / 6 绿),数据与代码分开 | ≤400 | | `packaging/tests/guard_vectors.json` | 守门自测向量(基线 + 32 条:26 红 / 6 绿),数据与代码分开 | ≤400 |
| `packaging/ci-runtime-smoke.md` | §11 的细节:断言口径/阈值依据/失效模式/复跑 | ≤400 | | `packaging/ci-runtime-smoke.md` | §11 的细节:断言口径/阈值依据/失效模式/复跑 | ≤400 |
| `packaging/linux-smoke-docker.sh` | 本地复跑 `runtime-smoke`(Docker + xvfb) | ≤400 | | `packaging/linux-smoke-docker.sh` | 本地复跑 `runtime-smoke`(Docker + xvfb) | ≤400 |
| `packaging/linux-check-docker.sh` | 本地复跑 `ci.yml` 的两条 ubuntu 门禁(clippy / test,Docker) | ≤400 | | `packaging/linux-check-docker.sh` | 本地复跑 `ci.yml` 的两条 ubuntu 门禁(clippy / test,Docker) | ≤400 |
@@ -129,9 +129,13 @@ Git Bash 下会静默落进 Docker VM(脚本内部用 `cygpath -m` 归一)
(文件名 / 步骤名 / 参数任一不符即红,见脚本的 `ALLOWED_STEP`)。 (文件名 / 步骤名 / 参数任一不符即红,见脚本的 `ALLOWED_STEP`)。
归属按 **`jobs.*.steps[]` 的列表项**判(不是「最近一行 `- name:`」):落不到具名步骤上的行——`jobs:` 归属按 **`jobs.*.steps[]` 的列表项**判(不是「最近一行 `- name:`」):落不到具名步骤上的行——`jobs:`
之前的孤儿行、项外键、没有 `name:` 的步骤、flow 风格步骤——**一律判红**(默认拒绝)。自测向量在 之前的孤儿行、项外键、没有 `name:` 的步骤、flow 风格步骤——**一律判红**(默认拒绝)。自测向量在
`packaging/tests/guard_vectors.json`(26 条:21 条注入必须红 + 6 条基线/合法写法必须绿)。 `packaging/tests/guard_vectors.json`(32 条向量:26 条注入必须红 + 6 条合法写法必须绿,另加基线必须绿;
抓不住(如实记):变量间接拼接(`FEAT="update-""test-source"`)、feature 改名后接回发布、`build.rs` 条数以 `--self-test` 输出为准)。`.cargo/config*.toml` 也在扫描面里:`--cfg feature="…"` 与
里 spawn cargo、以及「把别的步骤**改名**成 `update-e2e` 并带 `--test update_e2e_test` 却干别的事」。 `rustc-wrapper` 都能绕过 `--features`(后者一律判红,合法用途要 review 明确同意)。
抓不住(如实记):变量间接拼接(`FEAT="update-""test-source"`)、**两步 `$GITHUB_ENV` 交接**
(第一步把 feature 写进 env、第二步只用 `--features "$FEAT"`——第二步没有字面量,任何静态扫描都
看不见,这是本守门的固有边界,不是漏判)、feature 改名后接回发布、`build.rs` 里 spawn cargo、以及
「把别的步骤**改名**成 `update-e2e` 并带 `--test update_e2e_test` 却干别的事」。
- 验收本体:`cargo test --release -p unlucky-cli --features update-test-source --test update_e2e_test - 验收本体:`cargo test --release -p unlucky-cli --features update-test-source --test update_e2e_test
--test update_e2e_tamper_test`(正路 + 幂等 / 篡改摘要 + 篡改包体 + 截断)。CI 里同一个命令挂在 --test update_e2e_tamper_test`(正路 + 幂等 / 篡改摘要 + 篡改包体 + 截断)。CI 里同一个命令挂在
`ci.yml` 的 `update-e2e` job(**只在 Windows**:夹具的「真形状」是 Windows 裸 `.exe` + `ci.yml` 的 `update-e2e` job(**只在 Windows**:夹具的「真形状」是 Windows 裸 `.exe` +
+55 -29
View File
@@ -15,16 +15,17 @@
2. **不许「打开所有 feature」**:`--all-features`、`cargo hack --feature-powerset|--each-feature|--all`。 2. **不许「打开所有 feature」**:`--all-features`、`cargo hack --feature-powerset|--each-feature|--all`。
3. **不许设成默认**:`Cargo.toml` 里它只许是**自己的声明**(含跨行数组);进 `default` 或别的 3. **不许设成默认**:`Cargo.toml` 里它只许是**自己的声明**(含跨行数组);进 `default` 或别的
feature 的依赖数组即红。 feature 的依赖数组即红。
4. **扫描面逐项列举**(`WORKFLOW_GLOBS` / `SCRIPT_GLOBS` / `BUILD_ENTRY_GLOBS`):含 4. **扫描面逐项列举**(`WORKFLOW_GLOBS` / `SCRIPT_GLOBS` / `BUILD_ENTRY_GLOBS` / `CONFIG_GLOBS`):
`.github/actions/**`、`packaging/**` 递归子目录、`Makefile`/`justfile`/`Taskfile`。 含 `.github/actions/**`、`packaging/**` 递归子目录、`Makefile`/`justfile`/`Taskfile`、以及
`.cargo/config*.toml`(`--cfg feature="…"` 与 `rustc-wrapper` 都能绕过 `--features`,见
`scan_cargo_config`)。
注释剥离是**引号感知**的(`echo "#"; cargo build --features …` 里那个 `#` 不是注释)。 注释剥离是**引号感知**的(`echo "#"; cargo build --features …` 里那个 `#` 不是注释)。抓不住的都是
抓得住/抓不住 的完整清单见 `packaging/README.md` §14。要点:变量间接拼接(`FEAT="update-""test-source"`)、 **故意手打**或**运行期数据流**(完整清单见 `packaging/README.md` §14):变量拼接、两步 `$GITHUB_ENV`
feature 改名后接回发布、`build.rs` 里 spawn cargo、以及「有人把别的步骤**改名**成 `update-e2e` 并带 交接(第二步没有字面量——静态扫描的固有边界)、feature 改名后接回、`build.rs` 里 spawn cargo、改名复用。
`--test update_e2e_test` 却干别的事」都抓不住(扫描只看字符串;merge review 会看到 ci.yml 那一行)。
`--self-test` 把每条断言的**真实回归**注入临时副本,断言该红的红、该绿的绿(含 r2 的三条绕过与四条 `--self-test` 把每条断言的**真实回归**注入临时副本(数据在 `packaging/tests/guard_vectors.json`),
误杀回归)。守门自己抓不住回归时它会先红。 断言该红的红、该绿的绿。守门自己抓不住回归时它会先红。
""" """
from __future__ import annotations from __future__ import annotations
@@ -59,6 +60,15 @@ BUILD_ENTRY_GLOBS = (
"packaging/Makefile", "packaging/Makefile",
) )
MANIFEST_GLOBS = ("Cargo.toml", "crates/*/Cargo.toml") MANIFEST_GLOBS = ("Cargo.toml", "crates/*/Cargo.toml")
#: cargo 配置:`--cfg feature="…"` 与 `rustc-wrapper` 都能在没有 `--features` 时打开这条通道(P2-2)。
CONFIG_GLOBS = (
".cargo/config.toml",
".cargo/config",
"crates/*/.cargo/config.toml",
"crates/*/.cargo/config",
)
#: 任意 wrapper 都能注入 `--cfg`,文本扫描看不见 ⇒ 一律判红。
WRAPPER_KEY = re.compile(r"^\s*(rustc|rustc-workspace)-wrapper\s*=")
#: 唯一允许出现该 feature 的地方:**文件 + 列表项名 + 必须同在的字符串**,三者不符即红。 #: 唯一允许出现该 feature 的地方:**文件 + 列表项名 + 必须同在的字符串**,三者不符即红。
ALLOWED_STEP = { ALLOWED_STEP = {
"workflow": ".github/workflows/ci.yml", "workflow": ".github/workflows/ci.yml",
@@ -112,10 +122,12 @@ def _lines(path: Path) -> list[tuple[int, str]]:
def _name_of(key_indent: int, body: list[tuple[int, str]]) -> str | None: def _name_of(key_indent: int, body: list[tuple[int, str]]) -> str | None:
"""列表项的名字:项内**同级**的 `name:` 键(键序无关;块标量内容不当键)。 """列表项的名字:项内**同级**的 `name:` 键(键序无关;块标量内容不当键)。
找不到 → `None` ⇒ 这一项落不到具名步骤 ⇒ 带 feature 就红(默认拒绝)。只认键位置上的 「同级」= 项首行 `- ` 之后那一列(`item.end() == key_indent`,r3 复审 P2-1):更深缩进的
`name:`:`run: echo "name: x"` 这种不算,`run: |` 块里的 `name: dummy` 也不算。 `- name:`(`with:` / `env:` 子列表、`run: |` 内容)**不算**项名。找不到、或出现两个**不同**的
`name:` 都返回 `None` ⇒ 落不到具名步骤 ⇒ 带 feature 就红(默认拒绝)。
""" """
in_block = False in_block = False
found: str | None = None
for _number, line in body: for _number, line in body:
text = line.strip() text = line.strip()
if not text or text.startswith("#"): if not text or text.startswith("#"):
@@ -125,19 +137,23 @@ def _name_of(key_indent: int, body: list[tuple[int, str]]) -> str | None:
if in_block and indent > key_indent: if in_block and indent > key_indent:
continue continue
in_block = False in_block = False
if item: if item and item.end() == key_indent:
code = strip_comment(line[item.end() :]) # 项首行:` - name: x` → `name: x` code = strip_comment(line[item.end() :]) # 项首行:` - name: x` → `name: x`
elif indent == key_indent: elif not item and indent == key_indent:
code = strip_comment(text) code = strip_comment(text)
else: else:
continue continue
for pattern in (NAME_BLOCK, NAME_FLOW): for pattern in (NAME_BLOCK, NAME_FLOW):
match = pattern.match(code.strip()) match = pattern.match(code.strip())
if match: if match:
return match.group(1).strip().rstrip("}").strip("'\"") value = match.group(1).strip().rstrip("}").strip("'\"")
if found is not None and found != value:
return None
found = value
break
if code.rstrip().endswith(BLOCK_SCALAR): if code.rstrip().endswith(BLOCK_SCALAR):
in_block = True in_block = True
return None return found
def split_items( def split_items(
@@ -145,8 +161,8 @@ def split_items(
) -> tuple[list[tuple[str | None, list[tuple[int, str]]]], list[tuple[int, str]]]: ) -> tuple[list[tuple[str | None, list[tuple[int, str]]]], list[tuple[int, str]]]:
"""按 YAML **列表项**切块,返回((项名, 该项的行), 孤儿行)。 """按 YAML **列表项**切块,返回((项名, 该项的行), 孤儿行)。
每个 `- ` 起一项;比当前项更深的行归当前项,**同级或更浅的非列表行结束当前项**(那是 job 键 / 每个 `- ` 起一项;更深的行归当前项,**同级或更浅的非列表行结束当前项**(job 键 / `steps:` /
`steps:` / 顶层键)。落不到列表项上的行是孤儿——不属于任何步骤,带 feature 就红。 顶层键)。落不到列表项上的行是孤儿——不属于任何步骤,带 feature 就红。
""" """
items: list[tuple[str | None, list[tuple[int, str]]]] = [] items: list[tuple[str | None, list[tuple[int, str]]]] = []
orphans: list[tuple[int, str]] = [] orphans: list[tuple[int, str]] = []
@@ -179,20 +195,11 @@ def split_items(
def _reason(where: str, name: str | None, text: str) -> str | None: def _reason(where: str, name: str | None, text: str) -> str | None:
"""`None` = 这一项被放行;否则返回「为什么红」的一句话(按最具体的那条给)。""" """`None` = 这一项被放行;否则返回「为什么红」的一句话(按最具体的那条给)。"""
if where != ALLOWED_STEP["workflow"]: if where != ALLOWED_STEP["workflow"]:
return ( return f"`{FEATURE}` 只许出现在 `{ALLOWED_STEP['workflow']}` 的 `{ALLOWED_STEP['name']}` 步骤里(这里是 `{where}`)"
f"`{FEATURE}` 只许出现在 `{ALLOWED_STEP['workflow']}` 的 "
f"`{ALLOWED_STEP['name']}` 步骤里(这里是 `{where}`)"
)
if name is None: if name is None:
return ( return f"这一项没有同级 `name:`,落不到具名步骤(默认拒绝);`{where}` 里只有 `{ALLOWED_STEP['name']}` 可以用它"
f"这一项没有 `name:`,落不到具名步骤(默认拒绝);`{where}` 里只有名为 "
f"`{ALLOWED_STEP['name']}` 的步骤可以用它"
)
if name != ALLOWED_STEP["name"]: if name != ALLOWED_STEP["name"]:
return ( return f"`{where}` 里只有名为 `{ALLOWED_STEP['name']}` 的步骤可以用 `{FEATURE}`(这一项叫 `{name}`)"
f"`{where}` 里只有名为 `{ALLOWED_STEP['name']}` 的步骤可以用 `{FEATURE}`"
f"(这一项叫 `{name}`)"
)
if ALLOWED_STEP["must_contain"] not in text: if ALLOWED_STEP["must_contain"] not in text:
return f"`{where}` 的 `{name}` 步骤少了 `{ALLOWED_STEP['must_contain']}`" return f"`{where}` 的 `{name}` 步骤少了 `{ALLOWED_STEP['must_contain']}`"
return None return None
@@ -238,7 +245,7 @@ def scan_workflow(path: Path, root: Path) -> list[str]:
def scan_code_file(path: Path, root: Path) -> list[str]: def scan_code_file(path: Path, root: Path) -> list[str]:
"""扫脚本 / 第二构建入口:没有 allowlist,字面量或开关出现即红。""" """扫脚本 / 构建入口 / cargo config:没有 allowlist,字面量或开关出现即红。"""
where = path.relative_to(root).as_posix() where = path.relative_to(root).as_posix()
problems: list[str] = [] problems: list[str] = []
for number, line in _lines(path): for number, line in _lines(path):
@@ -280,6 +287,24 @@ def scan_manifest(path: Path, root: Path) -> list[str]:
return problems return problems
def scan_cargo_config(path: Path, root: Path) -> list[str]:
"""扫 `.cargo/config*.toml`:`--cfg feature="…"` 与 `rustc-wrapper` 都能绕过 `--features`。
r3 复审 P2-2 实测:`RUSTFLAGS='--cfg=feature="update-test-source"'` 能让 `#[cfg(feature = …)]`
段在没有 `--features` 时编进来。`[build] rustflags` / `[env]` 靠 feature 字面量抓(走
`scan_code_file`);`rustc-wrapper` 抓不住(注入什么由程序决定)⇒ 一律判红,合法用途要 review 同意。
"""
problems = scan_code_file(path, root)
where = path.relative_to(root).as_posix()
for number, line in _lines(path):
if WRAPPER_KEY.match(strip_comment(line)):
problems.append(
f"::error file={where}:{number}::{where} 设了 rustc wrapper:wrapper 能注入任意 "
f"`--cfg`,静态扫描看不见它是否打开了 `{FEATURE}`(默认拒绝)"
)
return problems
def scan(root: Path) -> list[str]: def scan(root: Path) -> list[str]:
"""扫整个仓库,返回所有问题(空 = 绿)。""" """扫整个仓库,返回所有问题(空 = 绿)。"""
problems: list[str] = [] problems: list[str] = []
@@ -287,6 +312,7 @@ def scan(root: Path) -> list[str]:
(WORKFLOW_GLOBS, scan_workflow), (WORKFLOW_GLOBS, scan_workflow),
(SCRIPT_GLOBS, scan_code_file), (SCRIPT_GLOBS, scan_code_file),
(BUILD_ENTRY_GLOBS, scan_code_file), (BUILD_ENTRY_GLOBS, scan_code_file),
(CONFIG_GLOBS, scan_cargo_config),
(MANIFEST_GLOBS, scan_manifest), (MANIFEST_GLOBS, scan_manifest),
): ):
for path in sorted({p for pattern in patterns for p in root.glob(pattern)}): for path in sorted({p for pattern in patterns for p in root.glob(pattern)}):
+42
View File
@@ -189,6 +189,48 @@
"files": { "files": {
"Makefile": "build:\n\tcargo build --release --features update-test-source\n" "Makefile": "build:\n\tcargo build --release --features update-test-source\n"
} }
},
{
"label": "P2-1 ⑥:`name:` 写在更深缩进的 env 子项里",
"expect": "red",
"files": {
".github/workflows/ci.yml": "jobs:\n update-e2e:\n steps:\n - name: update-e2e\n if: runner.os == 'Windows'\n run: |\n cargo test --release -p unlucky-cli --features update-test-source \\\n --test update_e2e_test --test update_e2e_tamper_test\n - run: cargo build --release -p unlucky-cli --features update-test-source\n env:\n T: --test update_e2e_test\n HINT: |\n - name: update-e2e\n"
}
},
{
"label": "P2-1 ⑦:`name:` 写在 `run: |` 块标量里",
"expect": "red",
"files": {
".github/workflows/ci.yml": "jobs:\n update-e2e:\n steps:\n - name: update-e2e\n if: runner.os == 'Windows'\n run: |\n cargo test --release -p unlucky-cli --features update-test-source \\\n --test update_e2e_test --test update_e2e_tamper_test\n - run: |\n name: update-e2e\n cargo build --release --features update-test-source --test update_e2e_test\n"
}
},
{
"label": "P2-1 ⑧:同一项里两个不同的 `name:`",
"expect": "red",
"files": {
".github/workflows/ci.yml": "jobs:\n update-e2e:\n steps:\n - name: update-e2e\n run: cargo build --release --features update-test-source --test update_e2e_test\n name: release-build\n"
}
},
{
"label": "P2-2 ①:.cargo/config.toml 用 `--cfg feature=` 开该 feature",
"expect": "red",
"files": {
".cargo/config.toml": "[build]\nrustflags = [\"--cfg\", 'feature=\"update-test-source\"']\n"
}
},
{
"label": "P2-2 ②:.cargo/config.toml 设 rustc-wrapper",
"expect": "red",
"files": {
".cargo/config.toml": "[build]\nrustc-wrapper = \"./tools/wrapper\"\n"
}
},
{
"label": "误杀 ⑤:普通 .cargo/config.toml(无 feature / 无 wrapper)",
"expect": "green",
"files": {
".cargo/config.toml": "[build]\njobs = 4\n"
}
} }
] ]
} }