ht32-code-mcp 0.4.5__tar.gz → 0.4.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/DEPLOY.md +58 -5
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/PKG-INFO +1 -1
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/components.example.json +7 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/pyproject.toml +1 -1
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/component_exporter.py +1 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/component_source.py +66 -14
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/components.py +11 -2
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/server.py +3 -0
- ht32_code_mcp-0.4.6/tests/test_component_redact.py +125 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/uv.lock +1 -1
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/.gitignore +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/.gitkeep +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/.pytest_base_final_run.log +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/README.md +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/e2e_scenario_a.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/e2e_scenario_b.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/smoke_http.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/smoke_stdio.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/__init__.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/__main__.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/exporter.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/index.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/knowledge.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/materializer.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/release.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/src/ht32_code_mcp/usage.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/tests/conftest.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/tests/test_component_export.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/tests/test_component_source.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/tests/test_component_tools.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/tests/test_components.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/tests/test_export_cloud_mode.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/tests/test_knowledge_grounding.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/tests/test_recipe_claims.py +0 -0
- {ht32_code_mcp-0.4.5 → ht32_code_mcp-0.4.6}/tests/test_tools.py +0 -0
|
@@ -350,10 +350,63 @@ MCP 服务端 → 仓库内登记表(默认 .work/components/components.json
|
|
|
350
350
|
| `COMPONENTS_STATE_FILE` | `<临时目录>/ht32-component-cache/tag-lock.json` | tag→commit 台账(检测 tag 被强制移动);生产建议放持久卷(compose 已指向 `/app/feedback/component-tag-lock.json`) |
|
|
351
351
|
| `COMPONENT_GIT_TIMEOUT` | `60` | 单次 Git 操作超时(秒) |
|
|
352
352
|
|
|
353
|
-
**运行时依赖 `git
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
353
|
+
**运行时依赖 `git` + 私有仓库凭据**(两者缺一,组件工具都会返回
|
|
354
|
+
`[COMPONENT_REMOTE_UNAVAILABLE]`):
|
|
355
|
+
|
|
356
|
+
1. **镜像**:官方 `supercorp/supergateway:next-uvx` 默认不带 git。同目录
|
|
357
|
+
[`.work/docker/Dockerfile`](../docker/Dockerfile) 在其上补装 git(自动适配
|
|
358
|
+
apt/apk),compose 已改为 `image: ht32-code-mcp:0.4.5-git` + `build: .`:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
cd /vol1/1000/SSD/docker/ht32-code/.work/docker
|
|
362
|
+
docker compose build ht32-code-mcp # 首次;之后同步脚本带 --build 会自动复用缓存
|
|
363
|
+
docker compose up -d --force-recreate ht32-code-mcp
|
|
364
|
+
docker exec ht32-code-mcp git --version # 必须打印版本
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
2. **凭据**:内网 Gitea 仓库非公开,匿名 `git ls-remote` 返回 **403**(可用
|
|
368
|
+
`curl -i http://100.66.1.6:3000/api/v1/repos/S621/HT32_BSP/tags` 复核)。在部署机
|
|
369
|
+
`.work/docker/.env`(已被 `.gitignore` 忽略)写入 Basic 凭据:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
# Gitea → 设置 → 应用 → 生成令牌,权限只需 read:repository
|
|
373
|
+
printf 'COMPONENTS_GIT_AUTH=%s\n' \
|
|
374
|
+
"$(printf '%s:%s' '<Gitea 用户名>' '<只读 token>' | base64 -w0)" > .env
|
|
375
|
+
chmod 600 .env
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
compose 以 `GIT_CONFIG_COUNT/KEY_0/VALUE_0` 注入 `http.extraheader`,只存在于
|
|
379
|
+
容器进程环境;不写入仓库、日志或 MCP 响应。轮换 token 后改 `.env` 并重建容器。
|
|
380
|
+
|
|
381
|
+
3. **验证**:容器内直接执行(无需再带 `-c`,凭据由环境注入):
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
docker exec ht32-code-mcp git ls-remote http://100.66.1.6:3000/S621/LwFSM.git | tail -2
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
能列出 `refs/tags/V2.0` 即表示 git 与凭据都就位。纯检索/导出模板不依赖 git,不受影响。
|
|
388
|
+
|
|
389
|
+
### 源码裁剪(`export.redact`,0.4.6+)
|
|
390
|
+
|
|
391
|
+
面向“只交付 `.lib`、不给源码”的组件。**不执行仓库里的 `script/submodule.bat`**
|
|
392
|
+
(Linux 无 cmd,且执行仓库脚本等于远程执行),改为快照层的等价内置规则:
|
|
393
|
+
|
|
394
|
+
| 模式 | 行为 |
|
|
395
|
+
|---|---|
|
|
396
|
+
| `none`(默认) | 不裁剪 |
|
|
397
|
+
| `auto` | 仅当仓库含 `script/submodule.bat` 时,按下面的规则裁剪 |
|
|
398
|
+
| `lib-only` | 无条件裁剪 |
|
|
399
|
+
|
|
400
|
+
`lib-only` 规则(路径相对导出根,大小写不敏感):
|
|
401
|
+
|
|
402
|
+
1. 移除根目录 `project/`;
|
|
403
|
+
2. `lib/<name>.lib` 存在 → 移除 `src/<name>.c`;
|
|
404
|
+
3. `tools/InfoProcess.exe` 存在 → 移除 `tools/InfoProcess.c`;
|
|
405
|
+
4. `script/` 保留。
|
|
406
|
+
|
|
407
|
+
裁剪发生在**快照构建阶段**,因此 `read_component_file`、`search_component_code`
|
|
408
|
+
与导出都看不到被移除的源文件;`manifest/components-lock.json` 会记录
|
|
409
|
+
`redaction.mode` 与逐条 `removed`(路径 + 原因),便于审计。
|
|
357
410
|
|
|
358
411
|
### 组件登记表放在哪里
|
|
359
412
|
|
|
@@ -491,4 +544,4 @@ uv lock && uv build && uv publish
|
|
|
491
544
|
| 3.7 | 云端 zip 下载通道(P1-3) | 服务器侧已上线:下载 1608157 字节 / SHA256 一致 / 解压 455 文件 / UV4 0E0W | ✅ |
|
|
492
545
|
| 3.8 | 定时同步脚本含 bundle 清理(P1-2) | `ht32-code-mcp-update.md` 第 6 步 `find … -mmin +60 -delete` | ✅ |
|
|
493
546
|
| 3.9 | 反馈与用量日志(P1-5,与 wiki 统一) | 0.2.1;云端已启用,但 `FEEDBACK_ROOT` 漏写前导 `/`(显示 `app/feedback/code`),需改绝对路径 | ⚠️ |
|
|
494
|
-
| 3.10 |
|
|
547
|
+
| 3.10 | 私有组件(0.4.5) | 登记表随仓库版本化;镜像 `ht32-code-mcp:0.4.5-git`(内置 git)+ 只读凭据注入;2026-11-02 线上验收:2 组件真实 tag、读/搜、导出 601 条目 / 组件 94 文件 / 无 `.git` | ✅ |
|
|
@@ -54,6 +54,13 @@
|
|
|
54
54
|
"exclude": ["docs/", "tests/"],
|
|
55
55
|
// 非空时只允许这些后缀,如 [".c", ".h"];空 = 不限
|
|
56
56
|
"allowed_suffixes": [],
|
|
57
|
+
// 源码裁剪(不执行仓库脚本):none / auto / lib-only
|
|
58
|
+
// none = 不裁剪(默认)
|
|
59
|
+
// auto = 仅当仓库含 script/submodule.bat 时按下面的内置规则裁剪
|
|
60
|
+
// lib-only= 无条件裁剪:移除 project/、删除有同名 lib/*.lib 的 src/*.c、
|
|
61
|
+
// 删除与 tools/InfoProcess.exe 同名的 .c;script/ 保留
|
|
62
|
+
// 裁剪在快照层生效,read/search/export 都看不到被移除的源码
|
|
63
|
+
"redact": "none",
|
|
57
64
|
// 容量上限:文件数 / 单文件字节 / 总字节
|
|
58
65
|
"max_files": 2000,
|
|
59
66
|
"max_file_bytes": 2097152,
|
|
@@ -64,6 +64,7 @@ class PreparedComponents:
|
|
|
64
64
|
"files": [{"path": f.rel, "bytes": f.size, "sha256": f.sha256}
|
|
65
65
|
for f in snap.files],
|
|
66
66
|
"excluded": snap.excluded,
|
|
67
|
+
"redaction": {"mode": snap.redact_mode, "removed": snap.redacted},
|
|
67
68
|
"license": item.entry.license,
|
|
68
69
|
"maintainer": item.entry.maintainer,
|
|
69
70
|
})
|
|
@@ -72,7 +72,8 @@ class Snapshot:
|
|
|
72
72
|
|
|
73
73
|
def __init__(self, entry: ComponentEntry, tag: str, commit: str,
|
|
74
74
|
workdir: Path, archive: Path, files: list[SnapshotFile],
|
|
75
|
-
excluded: list[dict]
|
|
75
|
+
excluded: list[dict], redact_mode: str = "none",
|
|
76
|
+
redacted: list[dict] | None = None) -> None:
|
|
76
77
|
self.entry = entry
|
|
77
78
|
self.tag = tag
|
|
78
79
|
self.commit = commit
|
|
@@ -80,6 +81,8 @@ class Snapshot:
|
|
|
80
81
|
self.archive = archive
|
|
81
82
|
self.files = files
|
|
82
83
|
self.excluded = excluded
|
|
84
|
+
self.redact_mode = redact_mode
|
|
85
|
+
self.redacted = redacted or []
|
|
83
86
|
|
|
84
87
|
@property
|
|
85
88
|
def total_bytes(self) -> int:
|
|
@@ -285,19 +288,20 @@ class ComponentSource:
|
|
|
285
288
|
"只接受发布 tag 指向的提交")
|
|
286
289
|
archive = workdir / "tree.zip"
|
|
287
290
|
self._git(["archive", "--format=zip", f"--output={archive}", head], cwd=repo)
|
|
288
|
-
files, excluded = self._plan(archive, info.tag)
|
|
289
|
-
return Snapshot(self.entry, info.tag, info.commit, workdir, archive,
|
|
291
|
+
files, excluded, redact_mode, redacted = self._plan(archive, info.tag)
|
|
292
|
+
return Snapshot(self.entry, info.tag, info.commit, workdir, archive,
|
|
293
|
+
files, excluded, redact_mode, redacted)
|
|
290
294
|
except BaseException:
|
|
291
295
|
_rmtree(workdir)
|
|
292
296
|
raise
|
|
293
297
|
|
|
294
298
|
# ---------------------------------------------------------- 文件计划
|
|
295
|
-
def _plan(self, archive: Path, tag: str
|
|
299
|
+
def _plan(self, archive: Path, tag: str
|
|
300
|
+
) -> tuple[list[SnapshotFile], list[dict], str, list[dict]]:
|
|
296
301
|
export = self.entry.export
|
|
297
302
|
root_prefix = f"{export.root}/" if export.root else ""
|
|
298
|
-
|
|
303
|
+
candidates: list[SnapshotFile] = []
|
|
299
304
|
excluded: list[dict] = []
|
|
300
|
-
total = 0
|
|
301
305
|
with zipfile.ZipFile(archive) as zf:
|
|
302
306
|
for info in zf.infolist():
|
|
303
307
|
if info.is_dir():
|
|
@@ -323,20 +327,39 @@ class ComponentSource:
|
|
|
323
327
|
if export.allowed_suffixes and not name.lower().endswith(export.allowed_suffixes):
|
|
324
328
|
raise _path_rejected(
|
|
325
329
|
f"组件文件类型不在登记允许列表({list(export.allowed_suffixes)}):{name}")
|
|
326
|
-
|
|
327
|
-
raise ComponentError(
|
|
328
|
-
COMPONENT_LIMIT_EXCEEDED,
|
|
329
|
-
f"组件 '{self.entry.id}' 的 {name} 为 {info.file_size} 字节,"
|
|
330
|
-
f"超过单文件上限 {export.max_file_bytes}")
|
|
331
|
-
total += info.file_size
|
|
332
|
-
files.append(SnapshotFile(
|
|
330
|
+
candidates.append(SnapshotFile(
|
|
333
331
|
rel=name, size=info.file_size,
|
|
334
332
|
sha256=hashlib.sha256(zf.read(info)).hexdigest(), zip_name=info.filename))
|
|
333
|
+
if not candidates:
|
|
334
|
+
raise ComponentError(
|
|
335
|
+
COMPONENT_TAG_NOT_FOUND,
|
|
336
|
+
f"组件 '{self.entry.id}' 在 tag '{tag}' 的导出根 "
|
|
337
|
+
f"'{export.root or '.'}' 下没有可导出文件")
|
|
338
|
+
|
|
339
|
+
redact_mode, removed = _redaction_plan(export.redact, [f.rel for f in candidates])
|
|
340
|
+
files: list[SnapshotFile] = []
|
|
341
|
+
redacted: list[dict] = []
|
|
342
|
+
for item in candidates:
|
|
343
|
+
reason = removed.get(item.rel.lower())
|
|
344
|
+
if reason:
|
|
345
|
+
redacted.append({"path": item.rel, "reason": reason})
|
|
346
|
+
excluded.append({"path": item.rel, "reason": reason})
|
|
347
|
+
else:
|
|
348
|
+
files.append(item)
|
|
335
349
|
if not files:
|
|
336
350
|
raise ComponentError(
|
|
337
351
|
COMPONENT_TAG_NOT_FOUND,
|
|
338
352
|
f"组件 '{self.entry.id}' 在 tag '{tag}' 的导出根 "
|
|
339
353
|
f"'{export.root or '.'}' 下没有可导出文件")
|
|
354
|
+
|
|
355
|
+
total = 0
|
|
356
|
+
for item in files:
|
|
357
|
+
if item.size > export.max_file_bytes:
|
|
358
|
+
raise ComponentError(
|
|
359
|
+
COMPONENT_LIMIT_EXCEEDED,
|
|
360
|
+
f"组件 '{self.entry.id}' 的 {item.rel} 为 {item.size} 字节,"
|
|
361
|
+
f"超过单文件上限 {export.max_file_bytes}")
|
|
362
|
+
total += item.size
|
|
340
363
|
if len(files) > export.max_files:
|
|
341
364
|
raise ComponentError(
|
|
342
365
|
COMPONENT_LIMIT_EXCEEDED,
|
|
@@ -345,7 +368,36 @@ class ComponentSource:
|
|
|
345
368
|
raise ComponentError(
|
|
346
369
|
COMPONENT_LIMIT_EXCEEDED,
|
|
347
370
|
f"组件 '{self.entry.id}' 总字节 {total} 超过上限 {export.max_total_bytes}")
|
|
348
|
-
return sorted(files, key=lambda f: f.rel), excluded
|
|
371
|
+
return sorted(files, key=lambda f: f.rel), excluded, redact_mode, redacted
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _redaction_plan(mode: str, names: list[str]) -> tuple[str, dict[str, str]]:
|
|
375
|
+
"""源码裁剪:等价于组件仓库的 `script/submodule.bat`,但**不执行**任何脚本。
|
|
376
|
+
|
|
377
|
+
规则(路径相对组件导出根,大小写不敏感):
|
|
378
|
+
- 移除根目录 `project/`(示例/测试工程);
|
|
379
|
+
- `lib/<name>.lib` 存在时,移除 `src/<name>.c`;
|
|
380
|
+
- `tools/InfoProcess.exe` 存在时,移除 `tools/InfoProcess.c`;
|
|
381
|
+
- `script/` 保留。
|
|
382
|
+
`auto` 仅当检测到 `script/submodule.bat` 时生效;`lib-only` 无条件生效。
|
|
383
|
+
返回 (生效模式, {小写路径: 原因})。
|
|
384
|
+
"""
|
|
385
|
+
if mode == "none":
|
|
386
|
+
return "none", {}
|
|
387
|
+
lowered = [n.lower() for n in names]
|
|
388
|
+
if mode == "auto" and "script/submodule.bat" not in lowered:
|
|
389
|
+
return "none", {}
|
|
390
|
+
libs = {Path(n).stem for n in lowered if re.fullmatch(r"lib/[^/]+\.lib", n)}
|
|
391
|
+
has_tool_exe = "tools/infoprocess.exe" in lowered
|
|
392
|
+
removed: dict[str, str] = {}
|
|
393
|
+
for name in lowered:
|
|
394
|
+
if name.startswith("project/"):
|
|
395
|
+
removed[name] = "redact:project-dir"
|
|
396
|
+
elif re.fullmatch(r"src/[^/]+\.c", name) and Path(name).stem in libs:
|
|
397
|
+
removed[name] = "redact:source-replaced-by-lib"
|
|
398
|
+
elif has_tool_exe and name == "tools/infoprocess.c":
|
|
399
|
+
removed[name] = "redact:tool-source"
|
|
400
|
+
return "lib-only", removed
|
|
349
401
|
|
|
350
402
|
|
|
351
403
|
def _unsafe_member(name: str) -> bool:
|
|
@@ -53,9 +53,11 @@ _COMPONENT_KEYS = {
|
|
|
53
53
|
_TAGS_KEYS = {"include", "exclude"}
|
|
54
54
|
_EXPORT_KEYS = {
|
|
55
55
|
"root", "target", "exclude", "allowed_suffixes",
|
|
56
|
-
"max_files", "max_file_bytes", "max_total_bytes",
|
|
56
|
+
"max_files", "max_file_bytes", "max_total_bytes", "redact",
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
REDACT_MODES = ("none", "auto", "lib-only")
|
|
60
|
+
|
|
59
61
|
DEFAULT_MAX_FILES = 2000
|
|
60
62
|
DEFAULT_MAX_FILE_BYTES = 2 * 1024 * 1024
|
|
61
63
|
DEFAULT_MAX_TOTAL_BYTES = 64 * 1024 * 1024
|
|
@@ -82,6 +84,8 @@ class ExportPolicy:
|
|
|
82
84
|
max_files: int = DEFAULT_MAX_FILES
|
|
83
85
|
max_file_bytes: int = DEFAULT_MAX_FILE_BYTES
|
|
84
86
|
max_total_bytes: int = DEFAULT_MAX_TOTAL_BYTES
|
|
87
|
+
# 源码裁剪:none / auto(检测到 script/submodule.bat 才裁剪)/ lib-only
|
|
88
|
+
redact: str = "none"
|
|
85
89
|
|
|
86
90
|
|
|
87
91
|
@dataclass(frozen=True)
|
|
@@ -308,6 +312,10 @@ def _parse_export(data: dict, where: str, component_id: str) -> ExportPolicy:
|
|
|
308
312
|
else:
|
|
309
313
|
target = f"components/{component_id}"
|
|
310
314
|
excludes = _str_list(block, "exclude", f"{where}.export")
|
|
315
|
+
redact = (_optional_str(block, "redact", f"{where}.export") or "none").strip().lower()
|
|
316
|
+
if redact not in REDACT_MODES:
|
|
317
|
+
raise _fail(f"{where}.export.redact",
|
|
318
|
+
f"只支持 {' / '.join(REDACT_MODES)},当前为 '{redact}'")
|
|
311
319
|
suffixes = tuple(s if s.startswith(".") else "." + s
|
|
312
320
|
for s in _str_list(block, "allowed_suffixes", f"{where}.export"))
|
|
313
321
|
max_files = _int_field(block, "max_files", f"{where}.export", DEFAULT_MAX_FILES)
|
|
@@ -319,7 +327,8 @@ def _parse_export(data: dict, where: str, component_id: str) -> ExportPolicy:
|
|
|
319
327
|
raise _fail(f"{where}.export", "max_files 超出合理上限(100000)")
|
|
320
328
|
return ExportPolicy(root=root, target=target, exclude=excludes,
|
|
321
329
|
allowed_suffixes=suffixes, max_files=max_files,
|
|
322
|
-
max_file_bytes=max_file, max_total_bytes=max_total
|
|
330
|
+
max_file_bytes=max_file, max_total_bytes=max_total,
|
|
331
|
+
redact=redact)
|
|
323
332
|
|
|
324
333
|
|
|
325
334
|
def _parse_component(raw: object, index: int, remotes: dict[str, str],
|
|
@@ -702,6 +702,9 @@ def get_component_info(component_id: str) -> str:
|
|
|
702
702
|
lines.append(f"- tag 规则:include={list(entry.tag_include)},"
|
|
703
703
|
f"exclude={list(entry.tag_exclude)}")
|
|
704
704
|
lines.extend(describe_tags(tags, err, limit=50))
|
|
705
|
+
if entry.export.redact != "none":
|
|
706
|
+
lines.append(f"- 源码裁剪:{entry.export.redact}"
|
|
707
|
+
"(移除 project/、有同名 .lib 的 src/*.c、InfoProcess.c;读/搜/导出统一生效)")
|
|
705
708
|
export = entry.export
|
|
706
709
|
lines += [
|
|
707
710
|
f"- 导出落点:{entry.target}/(仓库导出根:{export.root or '.'})",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""组件源码裁剪:等价 `script/submodule.bat`,但不在服务端执行任何脚本。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from conftest import catalog_document, commit_all, git, write
|
|
7
|
+
from ht32_code_mcp.component_exporter import ComponentService
|
|
8
|
+
from ht32_code_mcp.component_source import (ComponentSource, TagLockStore,
|
|
9
|
+
_redaction_plan)
|
|
10
|
+
from ht32_code_mcp.components import ComponentError, parse_catalog
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def redact_repo(tmp_path):
|
|
15
|
+
if not __import__("shutil").which("git"):
|
|
16
|
+
pytest.skip("环境无 git,跳过源码裁剪测试")
|
|
17
|
+
repo = tmp_path / "iap-repo"
|
|
18
|
+
repo.mkdir()
|
|
19
|
+
git(repo, "init", "-q")
|
|
20
|
+
write(repo, "README.md", "# HT32 IAP\n")
|
|
21
|
+
write(repo, "inc/ht32_iap.h", "void iap_init(void);\n")
|
|
22
|
+
write(repo, "src/ht32_iap.c", "void iap_init(void) {}\n")
|
|
23
|
+
write(repo, "src/ht32_iap_cache.c", "void cache_init(void) {}\n")
|
|
24
|
+
write(repo, "src/extra.c", "void extra(void) {}\n")
|
|
25
|
+
write(repo, "project/test/main.c", "int main(void) { return 0; }\n")
|
|
26
|
+
write(repo, "tools/InfoProcess.c", "int main(void) { return 0; }\n")
|
|
27
|
+
(repo / "tools" / "InfoProcess.exe").write_bytes(b"MZ")
|
|
28
|
+
(repo / "lib").mkdir()
|
|
29
|
+
(repo / "lib" / "ht32_iap.lib").write_bytes(b"LIB-1")
|
|
30
|
+
(repo / "lib" / "ht32_iap_cache.lib").write_bytes(b"LIB-2")
|
|
31
|
+
write(repo, "script/submodule.bat", "@echo off\r\n")
|
|
32
|
+
commit = commit_all(repo, "iap v1")
|
|
33
|
+
git(repo, "tag", "v1.0.0")
|
|
34
|
+
return {"path": repo, "commit": commit}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _catalog(redact: str, remote: str = "https://git.example.invalid/org/iap.git"):
|
|
38
|
+
doc = catalog_document(remote)
|
|
39
|
+
doc["components"][0]["export"] = {"target": "", "redact": redact}
|
|
40
|
+
return parse_catalog(doc)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _source(repo, tmp_path, redact: str) -> ComponentSource:
|
|
44
|
+
catalog = _catalog(redact)
|
|
45
|
+
return ComponentSource(catalog.get("ht32_bsp"), str(repo),
|
|
46
|
+
work_root=tmp_path / "work", timeout=120,
|
|
47
|
+
tag_locks=TagLockStore(None))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_redaction_plan_auto_requires_script():
|
|
51
|
+
names = ["src/a.c", "lib/a.lib", "project/main.c"]
|
|
52
|
+
assert _redaction_plan("auto", names) == ("none", {})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_redaction_plan_lib_only_rules():
|
|
56
|
+
names = ["src/a.c", "src/b.c", "lib/a.lib", "project/x/main.c",
|
|
57
|
+
"tools/InfoProcess.c", "tools/InfoProcess.exe",
|
|
58
|
+
"script/submodule.bat"]
|
|
59
|
+
mode, removed = _redaction_plan("lib-only", names)
|
|
60
|
+
assert mode == "lib-only"
|
|
61
|
+
assert set(removed) == {"src/a.c", "project/x/main.c", "tools/infoprocess.c"}
|
|
62
|
+
assert removed["src/a.c"] == "redact:source-replaced-by-lib"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_snapshot_redacts_sources_but_keeps_lib_header_and_script(redact_repo, tmp_path):
|
|
66
|
+
src = _source(redact_repo["path"], tmp_path, "auto")
|
|
67
|
+
snap = src.snapshot("v1.0.0")
|
|
68
|
+
try:
|
|
69
|
+
rels = {f.rel for f in snap.files}
|
|
70
|
+
assert snap.redact_mode == "lib-only"
|
|
71
|
+
assert "project/test/main.c" not in rels
|
|
72
|
+
assert "src/ht32_iap.c" not in rels
|
|
73
|
+
assert "src/ht32_iap_cache.c" not in rels
|
|
74
|
+
assert "tools/InfoProcess.c" not in rels
|
|
75
|
+
assert "src/extra.c" in rels
|
|
76
|
+
assert "lib/ht32_iap.lib" in rels
|
|
77
|
+
assert "inc/ht32_iap.h" in rels
|
|
78
|
+
assert "tools/InfoProcess.exe" in rels
|
|
79
|
+
assert "script/submodule.bat" in rels
|
|
80
|
+
reasons = {r["path"]: r["reason"] for r in snap.redacted}
|
|
81
|
+
assert reasons["src/ht32_iap.c"] == "redact:source-replaced-by-lib"
|
|
82
|
+
assert reasons["project/test/main.c"] == "redact:project-dir"
|
|
83
|
+
assert reasons["tools/InfoProcess.c"] == "redact:tool-source"
|
|
84
|
+
finally:
|
|
85
|
+
snap.close()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_snapshot_redact_none_keeps_everything(redact_repo, tmp_path):
|
|
89
|
+
src = _source(redact_repo["path"], tmp_path, "none")
|
|
90
|
+
snap = src.snapshot("v1.0.0")
|
|
91
|
+
try:
|
|
92
|
+
rels = {f.rel for f in snap.files}
|
|
93
|
+
assert "project/test/main.c" in rels
|
|
94
|
+
assert "src/ht32_iap.c" in rels
|
|
95
|
+
assert snap.redact_mode == "none"
|
|
96
|
+
assert snap.redacted == []
|
|
97
|
+
finally:
|
|
98
|
+
snap.close()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_export_lock_records_redaction(redact_repo, tmp_path):
|
|
102
|
+
catalog = _catalog("auto", str(redact_repo["path"]))
|
|
103
|
+
svc = ComponentService(lambda: catalog, work_root=tmp_path / "work", timeout=120)
|
|
104
|
+
prepared = svc.prepare(["ht32_bsp@v1.0.0"])
|
|
105
|
+
try:
|
|
106
|
+
lock = prepared.lock_document()
|
|
107
|
+
row = lock["components"][0]
|
|
108
|
+
assert row["redaction"]["mode"] == "lib-only"
|
|
109
|
+
removed = {r["path"] for r in row["redaction"]["removed"]}
|
|
110
|
+
assert removed == {"project/test/main.c", "src/ht32_iap.c",
|
|
111
|
+
"src/ht32_iap_cache.c", "tools/InfoProcess.c"}
|
|
112
|
+
exported = {f["path"] for f in row["files"]}
|
|
113
|
+
assert "src/ht32_iap.c" not in exported
|
|
114
|
+
assert "lib/ht32_iap.lib" in exported
|
|
115
|
+
finally:
|
|
116
|
+
prepared.close()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_registry_rejects_unknown_redact_mode():
|
|
120
|
+
doc = catalog_document("https://git.example.invalid/org/repo.git")
|
|
121
|
+
doc["components"][0]["export"]["redact"] = "all"
|
|
122
|
+
with pytest.raises(ComponentError) as err:
|
|
123
|
+
parse_catalog(doc)
|
|
124
|
+
assert err.value.code == "COMPONENTS_CONFIG_INVALID"
|
|
125
|
+
assert "redact" in err.value.message
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|