guarantee-based-coding 0.2.0__py3-none-any.whl
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.
- gbc/__init__.py +21 -0
- gbc/app/__init__.py +14 -0
- gbc/app/assets.py +38 -0
- gbc/app/config/__init__.py +14 -0
- gbc/app/config/backups.py +28 -0
- gbc/app/config/base.py +29 -0
- gbc/app/config/executor.py +132 -0
- gbc/app/config/project.py +35 -0
- gbc/app/core/__init__.py +14 -0
- gbc/app/core/env.py +64 -0
- gbc/app/core/executor.py +116 -0
- gbc/app/core/guarantee.py +338 -0
- gbc/app/i18n/__init__.py +43 -0
- gbc/app/i18n/lang.py +88 -0
- gbc/app/i18n/translate.py +80 -0
- gbc/app/intent/__init__.py +20 -0
- gbc/app/intent/base.py +308 -0
- gbc/app/intent/cli.py +124 -0
- gbc/app/intent/editor.py +93 -0
- gbc/app/interface/__init__.py +14 -0
- gbc/app/interface/base.py +851 -0
- gbc/app/interface/cli.py +585 -0
- gbc/app/interface/mcp.py +616 -0
- gbc/app/models/__init__.py +14 -0
- gbc/app/models/errors.py +179 -0
- gbc/app/models/meta.py +92 -0
- gbc/app/models/verify.py +63 -0
- gbc/app/utils/__init__.py +14 -0
- gbc/app/utils/file_utils.py +24 -0
- gbc/app/utils/gbc_md.py +121 -0
- gbc/app/utils/json_model_operator.py +85 -0
- gbc/app/utils/safe_file_writer.py +158 -0
- gbc/assets/editor/index.html +299 -0
- gbc/assets/i18n/catalog/en.json +52 -0
- gbc/assets/i18n/catalog/zh.json +52 -0
- gbc/assets/i18n/texts/rules.en.md +30 -0
- gbc/assets/i18n/texts/rules.zh.md +24 -0
- gbc/assets/i18n/texts/setup.en.md +74 -0
- gbc/assets/i18n/texts/setup.zh.md +69 -0
- gbc/assets/skills/README.md +16 -0
- gbc/assets/skills/gbc-cli/SKILL.md +143 -0
- gbc/entry.py +126 -0
- guarantee_based_coding-0.2.0.dist-info/METADATA +108 -0
- guarantee_based_coding-0.2.0.dist-info/RECORD +48 -0
- guarantee_based_coding-0.2.0.dist-info/WHEEL +5 -0
- guarantee_based_coding-0.2.0.dist-info/entry_points.txt +2 -0
- guarantee_based_coding-0.2.0.dist-info/licenses/LICENSE +202 -0
- guarantee_based_coding-0.2.0.dist-info/top_level.txt +1 -0
gbc/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copyright 2026 Jesse-x86
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""GBC — Guarantee-Based Coding.
|
|
16
|
+
|
|
17
|
+
让代码在不被完全理解的情况下也能被安全修改:把模块间依赖从隐含变成
|
|
18
|
+
显式、可执行、可验证的保证。本包是该方法的实现,也是它的第一个使用者。
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__version__ = "0.2.0"
|
gbc/app/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright 2026 Jesse-x86
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
gbc/app/assets.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright 2026 Jesse-x86
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""静态资源定位的单一入口。
|
|
16
|
+
|
|
17
|
+
所有随包分发的**数据**(i18n catalog/texts、editor 前端)集中在 `gbc/assets/`
|
|
18
|
+
下,与**代码**彻底分离。任何模块要读资源都从这里取路径,不各自
|
|
19
|
+
`Path(__file__).parent` ——加一类资源只需在此登记一处 + 打包声明一条。
|
|
20
|
+
|
|
21
|
+
打包:`pyproject.toml` 的 `[tool.setuptools.package-data]` 以 `"gbc" = ["assets/**"]`
|
|
22
|
+
把整个 assets 树纳入 wheel(散在各子包时易漏声明,这正是集中的理由)。
|
|
23
|
+
"""
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
# assets 根:本文件在 gbc/app/assets.py,上溯到 gbc/ 再进 assets/。
|
|
27
|
+
ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets"
|
|
28
|
+
|
|
29
|
+
# i18n 短消息查表 与 长文本资源。
|
|
30
|
+
I18N_CATALOG_DIR = ASSETS_DIR / "i18n" / "catalog"
|
|
31
|
+
I18N_TEXTS_DIR = ASSETS_DIR / "i18n" / "texts"
|
|
32
|
+
|
|
33
|
+
# 意图编辑器 web 前端静态页。
|
|
34
|
+
EDITOR_FRONTEND_DIR = ASSETS_DIR / "editor"
|
|
35
|
+
|
|
36
|
+
# 给 CLI-only agent 的预组 skills(教 agent 怎么调 gbc 子命令,等价于 MCP 工具描述)。
|
|
37
|
+
# gbc setup 只告知此目录坐标,由用户自行放到其 agent 读 skill 的位置。
|
|
38
|
+
SKILLS_DIR = ASSETS_DIR / "skills"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright 2026 Jesse-x86
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright 2026 Jesse-x86
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from gbc.app.config.base import PROJECT_ROOT
|
|
19
|
+
|
|
20
|
+
# 初始化备份数量
|
|
21
|
+
def _init_meta_backups() -> int:
|
|
22
|
+
var = os.environ.get("GBC_META_BACKUPS", None)
|
|
23
|
+
if var and var.isdigit():
|
|
24
|
+
return int(var)
|
|
25
|
+
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
META_BACKUPS = _init_meta_backups()
|
gbc/app/config/base.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copyright 2026 Jesse-x86
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_project_root() -> Path:
|
|
20
|
+
"""获取工具仓根目录。
|
|
21
|
+
|
|
22
|
+
注意:文件已迁入 `gbc/app/config/base.py`,比旧布局(`app/config/base.py`)多一层
|
|
23
|
+
`gbc/`,故上溯四层(base.py -> config -> app -> gbc -> 仓根)。
|
|
24
|
+
"""
|
|
25
|
+
return Path(__file__).resolve().parent.parent.parent.parent
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
PROJECT_ROOT = get_project_root()
|
|
29
|
+
CONFIG_DIR = PROJECT_ROOT / "config"
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Copyright 2026 Jesse-x86
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Literal
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, ValidationError
|
|
19
|
+
|
|
20
|
+
from gbc.app.config.project import get_current_project
|
|
21
|
+
from gbc.app.utils.json_model_operator import load_model_from_json, save_model_to_json
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---- 落盘位置 --------------------------------------------------------------
|
|
25
|
+
# executor 配置是**每台机器都不同**的可变状态(解释器路径、项目路径),因此:
|
|
26
|
+
# - 落在**目标项目**的 `.gbc/` 下(而非工具仓),让工具仓保持只读、可 pip 安装;
|
|
27
|
+
# - 私有真配置 `executors.json` 应进 gitignore;
|
|
28
|
+
# - 可提交的清单 `executors.example.json` 脱敏(只留名字 + 结构骨架),供协作者
|
|
29
|
+
# 知道"本项目需要配哪些 executor",而绝不泄露任何机器相关值或密钥。
|
|
30
|
+
EXECUTORS_FILENAME = "executors.json"
|
|
31
|
+
EXECUTORS_EXAMPLE_FILENAME = "executors.example.json"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def executors_config_path() -> Path:
|
|
35
|
+
"""当前目标项目的私有 executor 配置路径。随 `get_current_project()` 走。"""
|
|
36
|
+
return get_current_project() / ".gbc" / EXECUTORS_FILENAME
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def executors_example_path() -> Path:
|
|
40
|
+
"""当前目标项目的脱敏 executor 清单路径(可提交进 git)。"""
|
|
41
|
+
return get_current_project() / ".gbc" / EXECUTORS_EXAMPLE_FILENAME
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class EnvAction(BaseModel):
|
|
45
|
+
# key: 环境变量名
|
|
46
|
+
key: str
|
|
47
|
+
# action: 支持 set (覆盖), append (后补), prepend (前缀), remove (删除)
|
|
48
|
+
action: Literal["set", "append", "prepend", "remove"]
|
|
49
|
+
# value: 操作的值 (remove 时可为 None)
|
|
50
|
+
value: str | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# 单个执行器
|
|
54
|
+
class ExecutorModel(BaseModel):
|
|
55
|
+
command: list[str]
|
|
56
|
+
cwd: str
|
|
57
|
+
timeout: int = -1
|
|
58
|
+
env_ops: None | list[EnvAction] = None
|
|
59
|
+
# 人类可读说明:example 清单里唯一保留的"内容",由登记方**显式撰写**,
|
|
60
|
+
# 用来告诉协作者这个 executor 是什么、该怎么填。绝不从 command 自动推导(可能含密钥)。
|
|
61
|
+
comment: str | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# 执行器配置
|
|
65
|
+
class ExecutorsConfig(BaseModel):
|
|
66
|
+
executors: dict[str, ExecutorModel]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---- 惰性单例 --------------------------------------------------------------
|
|
70
|
+
# 关键:**不在 import 时读盘**。import 时项目根往往还没经 argv/set_current_project
|
|
71
|
+
# 设定好(MCP server 场景),此刻读盘只会命中错误位置。改为首次访问时按当前项目定位。
|
|
72
|
+
_cache: ExecutorsConfig | None = None
|
|
73
|
+
_cache_path: Path | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _load(path: Path) -> ExecutorsConfig:
|
|
77
|
+
try:
|
|
78
|
+
return load_model_from_json(path, ExecutorsConfig)
|
|
79
|
+
except FileNotFoundError:
|
|
80
|
+
return ExecutorsConfig(executors={})
|
|
81
|
+
except (ValueError, ValidationError):
|
|
82
|
+
return ExecutorsConfig(executors={})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_executors_config() -> ExecutorsConfig:
|
|
86
|
+
"""惰性获取当前项目的 executor 配置单例。
|
|
87
|
+
|
|
88
|
+
随目标项目变化自动重载:当 `get_current_project()` 指向的项目变了(缓存路径
|
|
89
|
+
与当前路径不一致),重新按新路径加载——保证一个进程内切换项目时不串配置。
|
|
90
|
+
"""
|
|
91
|
+
global _cache, _cache_path
|
|
92
|
+
path = executors_config_path()
|
|
93
|
+
if _cache is None or _cache_path != path:
|
|
94
|
+
_cache = _load(path)
|
|
95
|
+
_cache_path = path
|
|
96
|
+
return _cache
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _to_example(config: ExecutorsConfig) -> dict:
|
|
100
|
+
"""把真配置脱敏成可提交的清单。
|
|
101
|
+
|
|
102
|
+
约定(已拍板):**全删机器相关字段**——只留 executor 名字(作 key)与显式
|
|
103
|
+
`comment`。command/cwd/timeout/env_ops 一律**不出现**(不是置空、而是缺失),
|
|
104
|
+
因为无注释的空占位 ≈ 字段缺失,那就选更简单的「缺失」;缺失不静默——
|
|
105
|
+
Pydantic 必填校验与 `gbc doctor` 会替它嗊出「你需要配 X」。绝不拷任何原始值。
|
|
106
|
+
"""
|
|
107
|
+
executors: dict[str, dict] = {}
|
|
108
|
+
for name, model in config.executors.items():
|
|
109
|
+
entry: dict = {}
|
|
110
|
+
if model.comment is not None:
|
|
111
|
+
entry["comment"] = model.comment
|
|
112
|
+
executors[name] = entry
|
|
113
|
+
return {"executors": executors}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def save_executors_config() -> None:
|
|
117
|
+
"""保存当前项目的 executor 配置,并同步重算脱敏清单 example。
|
|
118
|
+
|
|
119
|
+
二者始终一起写,避免 example 腐烂。example 是宽松 dict(非 ExecutorModel),
|
|
120
|
+
因为它故意缺字段。
|
|
121
|
+
"""
|
|
122
|
+
import json
|
|
123
|
+
|
|
124
|
+
config = get_executors_config()
|
|
125
|
+
save_model_to_json(config, executors_config_path(), num_backups=0)
|
|
126
|
+
|
|
127
|
+
example_path = executors_example_path()
|
|
128
|
+
example_path.parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
example_path.write_text(
|
|
130
|
+
json.dumps(_to_example(config), ensure_ascii=False, indent=4),
|
|
131
|
+
encoding="utf-8",
|
|
132
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright 2026 Jesse-x86
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 初始化当前路径:进程启动时的 cwd。CLI 单次命令默认就用这个——“当前项目 = 跑命令时所在目录”,
|
|
19
|
+
# 无需环境变量,不会静默回退到包安装位置。常驻服务(mcp up / editor up)cwd 不可靠,
|
|
20
|
+
# 它们自己收显式参数覆盖,不依赖这个默认值。
|
|
21
|
+
def _init_current_project() -> Path:
|
|
22
|
+
return Path.cwd()
|
|
23
|
+
|
|
24
|
+
# 会被使用的当前路径变量
|
|
25
|
+
CURRENT_PROJECT = _init_current_project()
|
|
26
|
+
|
|
27
|
+
# 更新当前路径
|
|
28
|
+
def set_current_project(new_path: str):
|
|
29
|
+
new_path = Path(new_path)
|
|
30
|
+
|
|
31
|
+
global CURRENT_PROJECT
|
|
32
|
+
CURRENT_PROJECT = new_path
|
|
33
|
+
|
|
34
|
+
def get_current_project():
|
|
35
|
+
return CURRENT_PROJECT
|
gbc/app/core/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright 2026 Jesse-x86
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
gbc/app/core/env.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Copyright 2026 Jesse-x86
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
from gbc.app.config.executor import EnvAction
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_clean_python_path():
|
|
21
|
+
env = os.environ.copy()
|
|
22
|
+
pp_list = env.get("PYTHONPATH", "").split(os.pathsep)
|
|
23
|
+
pp_list = pp_list[1:]
|
|
24
|
+
env["PYTHONPATH"] = os.pathsep.join(pp_list)
|
|
25
|
+
|
|
26
|
+
return env
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def apply_env_action(env: dict[str, str], op: EnvAction, sep: str = os.pathsep) -> dict[str, str]:
|
|
30
|
+
"""
|
|
31
|
+
对传入的 env 字典执行单个 EnvAction 操作。
|
|
32
|
+
注:为了安全,建议传入 env.copy() 以避免副作用。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
key = op.key
|
|
36
|
+
val = op.value or ""
|
|
37
|
+
|
|
38
|
+
match op.action:
|
|
39
|
+
case "set":
|
|
40
|
+
env[key] = val
|
|
41
|
+
|
|
42
|
+
case "remove":
|
|
43
|
+
env.pop(key, None)
|
|
44
|
+
|
|
45
|
+
case "append":
|
|
46
|
+
current = env.get(key, "")
|
|
47
|
+
# 如果当前有值,则用分隔符连接;否则直接设为 val
|
|
48
|
+
env[key] = f"{current}{sep}{val}" if current else val
|
|
49
|
+
|
|
50
|
+
case "prepend":
|
|
51
|
+
current = env.get(key, "")
|
|
52
|
+
# 如果当前有值,则用分隔符连接;否则直接设为 val
|
|
53
|
+
env[key] = f"{val}{sep}{current}" if current else val
|
|
54
|
+
|
|
55
|
+
return env
|
|
56
|
+
|
|
57
|
+
def apply_env_actions(env: dict[str, str], ops: None | list[EnvAction], sep: str = os.pathsep) -> dict[str, str]:
|
|
58
|
+
if not ops:
|
|
59
|
+
return env
|
|
60
|
+
|
|
61
|
+
for op in ops:
|
|
62
|
+
apply_env_action(env, op, sep)
|
|
63
|
+
|
|
64
|
+
return env
|
gbc/app/core/executor.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Copyright 2026 Jesse-x86
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import subprocess
|
|
16
|
+
|
|
17
|
+
from gbc.app.config.executor import get_executors_config, ExecutorModel, save_executors_config
|
|
18
|
+
from gbc.app.core.env import apply_env_actions, get_clean_python_path
|
|
19
|
+
from gbc.app.models.errors import ExecutorNotFoundError
|
|
20
|
+
from gbc.app.models.verify import VerifyModel
|
|
21
|
+
|
|
22
|
+
FILE_PLACEHOLDERS = ["{file}", "{f}"]
|
|
23
|
+
DEFAULT_TIMEOUT = 15
|
|
24
|
+
|
|
25
|
+
def has_exec_config(config: str) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
是否存在该 config
|
|
28
|
+
:param config:
|
|
29
|
+
:return:
|
|
30
|
+
"""
|
|
31
|
+
return config in get_executors_config().executors
|
|
32
|
+
|
|
33
|
+
def _execute(config: str, file: str, timeout: int = -1) -> subprocess.CompletedProcess[str]:
|
|
34
|
+
"""
|
|
35
|
+
使用特定配置文件,执行单个测试
|
|
36
|
+
:param config:
|
|
37
|
+
:param file:
|
|
38
|
+
:param timeout:
|
|
39
|
+
:return:
|
|
40
|
+
"""
|
|
41
|
+
if not has_exec_config(config):
|
|
42
|
+
raise ExecutorNotFoundError(config)
|
|
43
|
+
|
|
44
|
+
executor_cfg = get_executors_config().executors[config]
|
|
45
|
+
|
|
46
|
+
args = executor_cfg.command.copy()
|
|
47
|
+
args = [arg.replace("{file}", file).replace("{f}", file) for arg in args]
|
|
48
|
+
|
|
49
|
+
env = apply_env_actions(get_clean_python_path(), executor_cfg.env_ops)
|
|
50
|
+
|
|
51
|
+
if timeout < 0:
|
|
52
|
+
timeout = executor_cfg.timeout
|
|
53
|
+
|
|
54
|
+
if timeout < 0:
|
|
55
|
+
timeout = DEFAULT_TIMEOUT
|
|
56
|
+
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
args=args,
|
|
59
|
+
capture_output=True,
|
|
60
|
+
text=True,
|
|
61
|
+
timeout=timeout,
|
|
62
|
+
cwd=executor_cfg.cwd,
|
|
63
|
+
env=env,
|
|
64
|
+
# 绝不让被测进程继承父进程的 stdin。作为 MCP server 运行时父进程 stdin 是
|
|
65
|
+
# JSON-RPC 协议管道,子进程继承后会阻塞("终端能跑、当服务挂"的经典坑)。
|
|
66
|
+
stdin=subprocess.DEVNULL,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
def verify_single(config: str, file: str, *, timeout: int = -1, return_model: bool = True) -> bool | VerifyModel:
|
|
72
|
+
"""
|
|
73
|
+
验证单个测试
|
|
74
|
+
:param config:
|
|
75
|
+
:param file:
|
|
76
|
+
:param timeout:
|
|
77
|
+
:param return_model:
|
|
78
|
+
:return:
|
|
79
|
+
"""
|
|
80
|
+
result = _execute(config, file, timeout)
|
|
81
|
+
|
|
82
|
+
if not return_model:
|
|
83
|
+
return result.returncode == 0
|
|
84
|
+
|
|
85
|
+
return VerifyModel(
|
|
86
|
+
return_code=result.returncode,
|
|
87
|
+
stdout=result.stdout,
|
|
88
|
+
stderr=result.stderr
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def upsert_exec_config(config_name: str, model: ExecutorModel):
|
|
92
|
+
"""
|
|
93
|
+
插入或更新执行器配置
|
|
94
|
+
:param config_name:
|
|
95
|
+
:param model:
|
|
96
|
+
:return:
|
|
97
|
+
"""
|
|
98
|
+
get_executors_config().executors[config_name] = model
|
|
99
|
+
save_executors_config()
|
|
100
|
+
|
|
101
|
+
def remove_exec_config(config_name: str):
|
|
102
|
+
"""
|
|
103
|
+
删除执行器配置
|
|
104
|
+
:param config_name:
|
|
105
|
+
:return:
|
|
106
|
+
"""
|
|
107
|
+
if has_exec_config(config_name):
|
|
108
|
+
del get_executors_config().executors[config_name]
|
|
109
|
+
save_executors_config()
|
|
110
|
+
|
|
111
|
+
def get_all_exec_configs():
|
|
112
|
+
"""
|
|
113
|
+
获取全部执行器配置
|
|
114
|
+
:return:
|
|
115
|
+
"""
|
|
116
|
+
return get_executors_config().copy()
|