tg-bot-plugin-buildkit 0.1.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.
- tg_bot_plugin_buildkit/__init__.py +19 -0
- tg_bot_plugin_buildkit/buildkit.py +307 -0
- tg_bot_plugin_buildkit/cli.py +104 -0
- tg_bot_plugin_buildkit/core_adapter.py +60 -0
- tg_bot_plugin_buildkit-0.1.0.dist-info/METADATA +68 -0
- tg_bot_plugin_buildkit-0.1.0.dist-info/RECORD +8 -0
- tg_bot_plugin_buildkit-0.1.0.dist-info/WHEEL +4 -0
- tg_bot_plugin_buildkit-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""TG-BOT 插件 bundle 的可复用构建工具集。"""
|
|
2
|
+
|
|
3
|
+
from .buildkit import (
|
|
4
|
+
benchmark_bundle,
|
|
5
|
+
build_bundle,
|
|
6
|
+
inspect_bundle,
|
|
7
|
+
install_local_bundle,
|
|
8
|
+
validate_bundle,
|
|
9
|
+
verify_bundle,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"benchmark_bundle",
|
|
14
|
+
"build_bundle",
|
|
15
|
+
"inspect_bundle",
|
|
16
|
+
"install_local_bundle",
|
|
17
|
+
"validate_bundle",
|
|
18
|
+
"verify_bundle",
|
|
19
|
+
]
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""构建在共享合同核心之上的高层 buildkit 操作。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .core_adapter import (
|
|
14
|
+
compute_artifact_digest,
|
|
15
|
+
compute_package_sha256,
|
|
16
|
+
inspect_bundle as shared_inspect_bundle,
|
|
17
|
+
normalize_manifest_payload,
|
|
18
|
+
validate_bundle_same_profile,
|
|
19
|
+
validate_bundle_target_profile,
|
|
20
|
+
verify_bundle as shared_verify_bundle,
|
|
21
|
+
write_reproducible_bundle,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
MANIFEST_FILE_NAME = "manifest.json"
|
|
25
|
+
SUPPORTED_BUNDLE_SUFFIX = ".tgpkg"
|
|
26
|
+
_BUNDLE_EXCLUDED_DIRECTORY_NAMES = {
|
|
27
|
+
".git",
|
|
28
|
+
".hg",
|
|
29
|
+
".idea",
|
|
30
|
+
".mypy_cache",
|
|
31
|
+
".nox",
|
|
32
|
+
".pytest_cache",
|
|
33
|
+
".ruff_cache",
|
|
34
|
+
".svn",
|
|
35
|
+
".tox",
|
|
36
|
+
".venv",
|
|
37
|
+
".vscode",
|
|
38
|
+
"__pycache__",
|
|
39
|
+
"node_modules",
|
|
40
|
+
}
|
|
41
|
+
_BUNDLE_EXCLUDED_TOP_LEVEL_NAMES = {
|
|
42
|
+
"build",
|
|
43
|
+
"dist",
|
|
44
|
+
"docs",
|
|
45
|
+
"scripts",
|
|
46
|
+
"tests",
|
|
47
|
+
}
|
|
48
|
+
_BUNDLE_EXCLUDED_TOP_LEVEL_FILE_NAMES = {
|
|
49
|
+
".gitignore",
|
|
50
|
+
".python-version",
|
|
51
|
+
"Makefile",
|
|
52
|
+
"README",
|
|
53
|
+
"README.md",
|
|
54
|
+
"poetry.lock",
|
|
55
|
+
"pyproject.toml",
|
|
56
|
+
"uv.lock",
|
|
57
|
+
}
|
|
58
|
+
_BUNDLE_EXCLUDED_FILE_SUFFIXES = {
|
|
59
|
+
".pyc",
|
|
60
|
+
SUPPORTED_BUNDLE_SUFFIX,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _stable_json(payload: Any) -> str:
|
|
65
|
+
return json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def build_runtime_profile_id() -> str:
|
|
69
|
+
implementation = platform.python_implementation().lower()
|
|
70
|
+
version = f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}"
|
|
71
|
+
system_name = platform.system().lower()
|
|
72
|
+
machine_name = platform.machine().lower().replace("/", "_")
|
|
73
|
+
libc_name, _libc_version = platform.libc_ver()
|
|
74
|
+
abi = libc_name.lower() if libc_name else ("darwin" if system_name == "darwin" else "unknown")
|
|
75
|
+
return "-".join((implementation, version, system_name, machine_name, abi))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_manifest_payload(source_dir: Path) -> dict[str, Any]:
|
|
79
|
+
manifest_path = source_dir / MANIFEST_FILE_NAME
|
|
80
|
+
payload = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
81
|
+
if not isinstance(payload, dict):
|
|
82
|
+
raise ValueError("manifest root must be an object")
|
|
83
|
+
return payload
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _write_manifest_payload(source_dir: Path, payload: dict[str, Any]) -> None:
|
|
87
|
+
(source_dir / MANIFEST_FILE_NAME).write_text(_stable_json(payload), encoding="utf-8")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _output_bundle_path(output_dir: Path, *, plugin_id: str, plugin_version: str, runtime_profile_id: str) -> Path:
|
|
91
|
+
return output_dir / plugin_id / f"{plugin_id}-{plugin_version}-{runtime_profile_id}{SUPPORTED_BUNDLE_SUFFIX}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _output_roots_to_ignore(source_dir: Path, output_dir: Path) -> set[str]:
|
|
95
|
+
try:
|
|
96
|
+
relpath = output_dir.relative_to(source_dir)
|
|
97
|
+
except ValueError:
|
|
98
|
+
return set()
|
|
99
|
+
if not relpath.parts:
|
|
100
|
+
return set()
|
|
101
|
+
return {relpath.parts[0]}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _should_exclude_bundle_source(path: Path, source_dir: Path, extra_top_level_names: set[str]) -> bool:
|
|
105
|
+
relpath = path.relative_to(source_dir)
|
|
106
|
+
if not relpath.parts:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
first_part = relpath.parts[0]
|
|
110
|
+
if first_part in extra_top_level_names:
|
|
111
|
+
return True
|
|
112
|
+
if first_part.startswith(".") and first_part != "META-INF":
|
|
113
|
+
return True
|
|
114
|
+
if any(part in _BUNDLE_EXCLUDED_DIRECTORY_NAMES for part in relpath.parts):
|
|
115
|
+
return True
|
|
116
|
+
if first_part in _BUNDLE_EXCLUDED_TOP_LEVEL_NAMES:
|
|
117
|
+
return True
|
|
118
|
+
if len(relpath.parts) == 1 and path.name in _BUNDLE_EXCLUDED_TOP_LEVEL_FILE_NAMES:
|
|
119
|
+
return True
|
|
120
|
+
if path.is_file() and path.suffix in _BUNDLE_EXCLUDED_FILE_SUFFIXES:
|
|
121
|
+
return True
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _build_copy_ignore(source_dir: Path, output_dir: Path):
|
|
126
|
+
extra_top_level_names = _output_roots_to_ignore(source_dir, output_dir)
|
|
127
|
+
|
|
128
|
+
def _ignore(current_dir: str, names: list[str]) -> list[str]:
|
|
129
|
+
current_path = Path(current_dir)
|
|
130
|
+
ignored: list[str] = []
|
|
131
|
+
for name in names:
|
|
132
|
+
candidate = current_path / name
|
|
133
|
+
if _should_exclude_bundle_source(candidate, source_dir, extra_top_level_names):
|
|
134
|
+
ignored.append(name)
|
|
135
|
+
return ignored
|
|
136
|
+
|
|
137
|
+
return _ignore
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_bundle(
|
|
141
|
+
*,
|
|
142
|
+
source_dir: str | Path,
|
|
143
|
+
output_dir: str | Path | None = None,
|
|
144
|
+
runtime_profile_id: str = "",
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
resolved_source_dir = Path(source_dir).expanduser().resolve()
|
|
147
|
+
resolved_output_dir = (
|
|
148
|
+
Path(output_dir).expanduser().resolve()
|
|
149
|
+
if output_dir
|
|
150
|
+
else (resolved_source_dir / "dist" / "plugins").resolve()
|
|
151
|
+
)
|
|
152
|
+
manifest_payload = _read_manifest_payload(resolved_source_dir)
|
|
153
|
+
effective_runtime_profile_id = str(runtime_profile_id or "").strip() or build_runtime_profile_id()
|
|
154
|
+
manifest_payload["runtime_profile_id"] = effective_runtime_profile_id
|
|
155
|
+
manifest_payload["artifact_digest"] = ""
|
|
156
|
+
manifest = normalize_manifest_payload(manifest_payload, require_artifact_digest=False)
|
|
157
|
+
|
|
158
|
+
with tempfile.TemporaryDirectory(prefix=f"tg-bot-buildkit-{manifest.plugin_id}-") as temp_dir_str:
|
|
159
|
+
staging_dir = Path(temp_dir_str) / manifest.plugin_id
|
|
160
|
+
shutil.copytree(
|
|
161
|
+
resolved_source_dir,
|
|
162
|
+
staging_dir,
|
|
163
|
+
ignore=_build_copy_ignore(resolved_source_dir, resolved_output_dir),
|
|
164
|
+
)
|
|
165
|
+
_write_manifest_payload(staging_dir, manifest.raw)
|
|
166
|
+
artifact_digest = compute_artifact_digest(staging_dir, manifest.raw)
|
|
167
|
+
manifest_payload = dict(manifest.raw)
|
|
168
|
+
manifest_payload["artifact_digest"] = artifact_digest
|
|
169
|
+
_write_manifest_payload(staging_dir, manifest_payload)
|
|
170
|
+
bundle_path = _output_bundle_path(
|
|
171
|
+
resolved_output_dir,
|
|
172
|
+
plugin_id=manifest.plugin_id,
|
|
173
|
+
plugin_version=manifest.plugin_version,
|
|
174
|
+
runtime_profile_id=effective_runtime_profile_id,
|
|
175
|
+
)
|
|
176
|
+
bundle_path.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
write_reproducible_bundle(staging_dir, bundle_path)
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
"build_status": "ok",
|
|
181
|
+
"plugin_id": manifest.plugin_id,
|
|
182
|
+
"plugin_version": manifest.plugin_version,
|
|
183
|
+
"runtime_profile_id": effective_runtime_profile_id,
|
|
184
|
+
"artifact_digest": artifact_digest,
|
|
185
|
+
"package_sha256": compute_package_sha256(bundle_path),
|
|
186
|
+
"bundle_path": str(bundle_path),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def inspect_bundle(bundle_path: str | Path) -> dict[str, Any]:
|
|
191
|
+
inspection = shared_inspect_bundle(bundle_path)
|
|
192
|
+
return {
|
|
193
|
+
"inspect_status": "ok",
|
|
194
|
+
"plugin_id": inspection.plugin_id,
|
|
195
|
+
"plugin_version": inspection.plugin_version,
|
|
196
|
+
"runtime_profile_id": inspection.runtime_profile_id,
|
|
197
|
+
"artifact_digest": inspection.artifact_digest,
|
|
198
|
+
"package_sha256": inspection.package_sha256,
|
|
199
|
+
"entrypoint": inspection.manifest.entrypoint,
|
|
200
|
+
"bundle_path": str(inspection.bundle_path),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def verify_bundle(bundle_path: str | Path, *, allow_unsigned_dev: bool = False) -> dict[str, Any]:
|
|
205
|
+
verification = shared_verify_bundle(bundle_path, allow_unsigned_dev=allow_unsigned_dev)
|
|
206
|
+
signer_identity = ""
|
|
207
|
+
if verification.signer_identity is not None:
|
|
208
|
+
signer_identity = verification.signer_identity.persistent_value()
|
|
209
|
+
return {
|
|
210
|
+
"verify_status": "ok",
|
|
211
|
+
"plugin_id": verification.plugin_id,
|
|
212
|
+
"plugin_version": verification.plugin_version,
|
|
213
|
+
"runtime_profile_id": verification.runtime_profile_id,
|
|
214
|
+
"artifact_digest": verification.artifact_digest,
|
|
215
|
+
"package_sha256": verification.package_sha256,
|
|
216
|
+
"signature_verification_status": verification.signature_verification_status,
|
|
217
|
+
"signer_identity": signer_identity,
|
|
218
|
+
"bundle_path": str(Path(bundle_path).expanduser().resolve()),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def validate_bundle(
|
|
223
|
+
bundle_path: str | Path,
|
|
224
|
+
*,
|
|
225
|
+
mode: str,
|
|
226
|
+
target_runtime_profile_id: str = "",
|
|
227
|
+
allow_unsigned_dev: bool = False,
|
|
228
|
+
) -> dict[str, Any]:
|
|
229
|
+
resolved_bundle_path = Path(bundle_path).expanduser().resolve()
|
|
230
|
+
if mode == "same-profile":
|
|
231
|
+
verification = validate_bundle_same_profile(
|
|
232
|
+
resolved_bundle_path,
|
|
233
|
+
runtime_profile_id=build_runtime_profile_id(),
|
|
234
|
+
allow_unsigned_dev=allow_unsigned_dev,
|
|
235
|
+
)
|
|
236
|
+
elif mode == "target-profile":
|
|
237
|
+
verification = validate_bundle_target_profile(
|
|
238
|
+
resolved_bundle_path,
|
|
239
|
+
target_runtime_profile_id=str(target_runtime_profile_id or "").strip(),
|
|
240
|
+
allow_unsigned_dev=allow_unsigned_dev,
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
raise ValueError(f"unsupported validate mode: {mode}")
|
|
244
|
+
return {
|
|
245
|
+
"validate_status": "ok",
|
|
246
|
+
"mode": mode,
|
|
247
|
+
"plugin_id": verification.plugin_id,
|
|
248
|
+
"plugin_version": verification.plugin_version,
|
|
249
|
+
"runtime_profile_id": verification.runtime_profile_id,
|
|
250
|
+
"artifact_digest": verification.artifact_digest,
|
|
251
|
+
"package_sha256": verification.package_sha256,
|
|
252
|
+
"signature_verification_status": verification.signature_verification_status,
|
|
253
|
+
"bundle_path": str(resolved_bundle_path),
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def install_local_bundle(
|
|
258
|
+
*,
|
|
259
|
+
bundle_path: str | Path,
|
|
260
|
+
plugin_root: str | Path,
|
|
261
|
+
dev_unsigned: bool,
|
|
262
|
+
) -> dict[str, Any]:
|
|
263
|
+
resolved_bundle_path = Path(bundle_path).expanduser().resolve()
|
|
264
|
+
resolved_plugin_root = Path(plugin_root).expanduser().resolve()
|
|
265
|
+
verification = shared_verify_bundle(resolved_bundle_path, allow_unsigned_dev=dev_unsigned)
|
|
266
|
+
target_dir = resolved_plugin_root / verification.plugin_id
|
|
267
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
268
|
+
target_bundle_path = target_dir / f"{verification.plugin_id}{SUPPORTED_BUNDLE_SUFFIX}"
|
|
269
|
+
temp_bundle_path = target_bundle_path.with_suffix(f"{target_bundle_path.suffix}.tmp")
|
|
270
|
+
shutil.copy2(resolved_bundle_path, temp_bundle_path)
|
|
271
|
+
temp_bundle_path.replace(target_bundle_path)
|
|
272
|
+
return {
|
|
273
|
+
"install_status": "ok",
|
|
274
|
+
"plugin_id": verification.plugin_id,
|
|
275
|
+
"plugin_version": verification.plugin_version,
|
|
276
|
+
"package_sha256": verification.package_sha256,
|
|
277
|
+
"signature_verification_status": verification.signature_verification_status,
|
|
278
|
+
"installed_bundle": str(target_bundle_path),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def benchmark_bundle(
|
|
283
|
+
*,
|
|
284
|
+
source_dir: str | Path,
|
|
285
|
+
output_dir: str | Path | None = None,
|
|
286
|
+
runtime_profile_id: str = "",
|
|
287
|
+
) -> dict[str, Any]:
|
|
288
|
+
started_at = time.perf_counter()
|
|
289
|
+
build_result = build_bundle(
|
|
290
|
+
source_dir=source_dir,
|
|
291
|
+
output_dir=output_dir,
|
|
292
|
+
runtime_profile_id=runtime_profile_id,
|
|
293
|
+
)
|
|
294
|
+
build_duration_ms = round((time.perf_counter() - started_at) * 1000, 3)
|
|
295
|
+
bundle_path = Path(build_result["bundle_path"])
|
|
296
|
+
inspect_result = inspect_bundle(bundle_path)
|
|
297
|
+
return {
|
|
298
|
+
"benchmark_status": "ok",
|
|
299
|
+
"build_duration_ms": build_duration_ms,
|
|
300
|
+
"bundle_size_bytes": bundle_path.stat().st_size,
|
|
301
|
+
"plugin_id": build_result["plugin_id"],
|
|
302
|
+
"plugin_version": build_result["plugin_version"],
|
|
303
|
+
"runtime_profile_id": build_result["runtime_profile_id"],
|
|
304
|
+
"artifact_digest": inspect_result["artifact_digest"],
|
|
305
|
+
"package_sha256": inspect_result["package_sha256"],
|
|
306
|
+
"bundle_path": str(bundle_path),
|
|
307
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""tg-bot-plugin-buildkit 的命令行入口。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .buildkit import (
|
|
10
|
+
benchmark_bundle,
|
|
11
|
+
build_bundle,
|
|
12
|
+
inspect_bundle,
|
|
13
|
+
install_local_bundle,
|
|
14
|
+
validate_bundle,
|
|
15
|
+
verify_bundle,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _stable_json(payload: dict[str, object]) -> str:
|
|
20
|
+
return json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_args() -> argparse.Namespace:
|
|
24
|
+
parser = argparse.ArgumentParser(description="构建并校验 TG-BOT 插件 bundle。")
|
|
25
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
26
|
+
|
|
27
|
+
build_parser = subparsers.add_parser("build", help="构建可复现的 .tgpkg bundle。")
|
|
28
|
+
build_parser.add_argument("--source-dir", required=True, help="插件源码目录。")
|
|
29
|
+
build_parser.add_argument("--output-dir", default="", help="生成 bundle 的输出根目录。")
|
|
30
|
+
build_parser.add_argument("--runtime-profile-id", default="", help="覆盖 runtime profile id。")
|
|
31
|
+
|
|
32
|
+
inspect_parser = subparsers.add_parser("inspect", help="检查 bundle 合同完整性。")
|
|
33
|
+
inspect_parser.add_argument("--bundle", required=True, help=".tgpkg bundle 路径。")
|
|
34
|
+
|
|
35
|
+
verify_parser = subparsers.add_parser("verify", help="校验 bundle 签名材料。")
|
|
36
|
+
verify_parser.add_argument("--bundle", required=True, help=".tgpkg bundle 路径。")
|
|
37
|
+
verify_parser.add_argument(
|
|
38
|
+
"--allow-unsigned-dev",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="允许未签名的开发态 bundle。",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
validate_parser = subparsers.add_parser("validate", help="按 runtime profile 校验 bundle。")
|
|
44
|
+
validate_parser.add_argument("--bundle", required=True, help=".tgpkg bundle 路径。")
|
|
45
|
+
validate_parser.add_argument("--mode", required=True, choices=("same-profile", "target-profile"))
|
|
46
|
+
validate_parser.add_argument("--target-runtime-profile-id", default="", help="target-profile 模式必填。")
|
|
47
|
+
validate_parser.add_argument("--allow-unsigned-dev", action="store_true")
|
|
48
|
+
|
|
49
|
+
benchmark_parser = subparsers.add_parser("benchmark", help="执行一次本地构建基准测试。")
|
|
50
|
+
benchmark_parser.add_argument("--source-dir", required=True, help="插件源码目录。")
|
|
51
|
+
benchmark_parser.add_argument("--output-dir", default="", help="生成 bundle 的输出根目录。")
|
|
52
|
+
benchmark_parser.add_argument("--runtime-profile-id", default="", help="覆盖 runtime profile id。")
|
|
53
|
+
|
|
54
|
+
install_parser = subparsers.add_parser("install-local", help="将 bundle 复制到本地插件根目录。")
|
|
55
|
+
install_parser.add_argument("--bundle", required=True, help=".tgpkg bundle 路径。")
|
|
56
|
+
install_parser.add_argument("--plugin-root", required=True, help="目标 TG_BOT_PLUGIN_ROOT 风格目录。")
|
|
57
|
+
install_parser.add_argument("--dev-unsigned", action="store_true", help="允许未签名的开发态 bundle。")
|
|
58
|
+
|
|
59
|
+
return parser.parse_args()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main() -> int:
|
|
63
|
+
args = _parse_args()
|
|
64
|
+
try:
|
|
65
|
+
if args.command == "build":
|
|
66
|
+
payload = build_bundle(
|
|
67
|
+
source_dir=args.source_dir,
|
|
68
|
+
output_dir=args.output_dir,
|
|
69
|
+
runtime_profile_id=args.runtime_profile_id,
|
|
70
|
+
)
|
|
71
|
+
elif args.command == "inspect":
|
|
72
|
+
payload = inspect_bundle(args.bundle)
|
|
73
|
+
elif args.command == "verify":
|
|
74
|
+
payload = verify_bundle(args.bundle, allow_unsigned_dev=bool(args.allow_unsigned_dev))
|
|
75
|
+
elif args.command == "validate":
|
|
76
|
+
payload = validate_bundle(
|
|
77
|
+
args.bundle,
|
|
78
|
+
mode=args.mode,
|
|
79
|
+
target_runtime_profile_id=args.target_runtime_profile_id,
|
|
80
|
+
allow_unsigned_dev=bool(args.allow_unsigned_dev),
|
|
81
|
+
)
|
|
82
|
+
elif args.command == "benchmark":
|
|
83
|
+
payload = benchmark_bundle(
|
|
84
|
+
source_dir=args.source_dir,
|
|
85
|
+
output_dir=args.output_dir,
|
|
86
|
+
runtime_profile_id=args.runtime_profile_id,
|
|
87
|
+
)
|
|
88
|
+
elif args.command == "install-local":
|
|
89
|
+
payload = install_local_bundle(
|
|
90
|
+
bundle_path=args.bundle,
|
|
91
|
+
plugin_root=args.plugin_root,
|
|
92
|
+
dev_unsigned=bool(args.dev_unsigned),
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError(f"unsupported command: {args.command}")
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
print(_stable_json({"status": "error", "message": str(exc)}), end="")
|
|
98
|
+
return 1
|
|
99
|
+
print(_stable_json({"status": "ok", **payload}), end="")
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""面向 tg-bot-plugin-contract-core 的本地适配层。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _candidate_src_paths() -> list[Path]:
|
|
12
|
+
candidates: list[Path] = []
|
|
13
|
+
env_path = str(os.getenv("TG_BOT_PLUGIN_CONTRACT_CORE_SRC") or "").strip()
|
|
14
|
+
if env_path:
|
|
15
|
+
candidates.append(Path(env_path).expanduser())
|
|
16
|
+
sibling = Path(__file__).resolve().parents[3] / "tg-bot-plugin-contract-core" / "src"
|
|
17
|
+
candidates.append(sibling)
|
|
18
|
+
deduped: list[Path] = []
|
|
19
|
+
for candidate in candidates:
|
|
20
|
+
resolved = candidate.resolve()
|
|
21
|
+
if resolved not in deduped:
|
|
22
|
+
deduped.append(resolved)
|
|
23
|
+
return deduped
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_contract_core():
|
|
27
|
+
try:
|
|
28
|
+
public_module = import_module("tg_bot_plugin_contract_core")
|
|
29
|
+
core_module = import_module("tg_bot_plugin_contract_core.core")
|
|
30
|
+
return public_module, core_module
|
|
31
|
+
except ImportError:
|
|
32
|
+
for candidate in _candidate_src_paths():
|
|
33
|
+
if not candidate.is_dir():
|
|
34
|
+
continue
|
|
35
|
+
candidate_str = str(candidate)
|
|
36
|
+
if candidate_str not in sys.path:
|
|
37
|
+
sys.path.insert(0, candidate_str)
|
|
38
|
+
try:
|
|
39
|
+
public_module = import_module("tg_bot_plugin_contract_core")
|
|
40
|
+
core_module = import_module("tg_bot_plugin_contract_core.core")
|
|
41
|
+
return public_module, core_module
|
|
42
|
+
except ImportError:
|
|
43
|
+
continue
|
|
44
|
+
raise ImportError(
|
|
45
|
+
"tg_bot_plugin_contract_core is not importable; install the package or set "
|
|
46
|
+
"TG_BOT_PLUGIN_CONTRACT_CORE_SRC to the package src directory",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_PUBLIC_MODULE, _CORE_MODULE = _load_contract_core()
|
|
51
|
+
|
|
52
|
+
ContractCoreError = _PUBLIC_MODULE.ContractCoreError
|
|
53
|
+
compute_artifact_digest = _PUBLIC_MODULE.compute_artifact_digest
|
|
54
|
+
compute_package_sha256 = _PUBLIC_MODULE.compute_package_sha256
|
|
55
|
+
inspect_bundle = _PUBLIC_MODULE.inspect_bundle
|
|
56
|
+
validate_bundle_same_profile = _PUBLIC_MODULE.validate_bundle_same_profile
|
|
57
|
+
validate_bundle_target_profile = _PUBLIC_MODULE.validate_bundle_target_profile
|
|
58
|
+
verify_bundle = _PUBLIC_MODULE.verify_bundle
|
|
59
|
+
write_reproducible_bundle = _PUBLIC_MODULE.write_reproducible_bundle
|
|
60
|
+
normalize_manifest_payload = _CORE_MODULE._normalize_manifest_payload
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tg-bot-plugin-buildkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 基于共享合同核心构建并校验 TG-BOT 插件 bundle。
|
|
5
|
+
Author: Fire Dragons
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: tg-bot-plugin-contract-core==0.1.1
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# tg-bot-plugin-buildkit
|
|
14
|
+
|
|
15
|
+
`tg-bot-plugin-buildkit` 是面向 TG-BOT 单插件仓库的可复用构建与校验 CLI。
|
|
16
|
+
|
|
17
|
+
它封装共享 `tg-bot-plugin-contract-core` 包,并提供:
|
|
18
|
+
|
|
19
|
+
- `build`
|
|
20
|
+
- `inspect`
|
|
21
|
+
- `verify`
|
|
22
|
+
- `validate --mode same-profile`
|
|
23
|
+
- `validate --mode target-profile`
|
|
24
|
+
- `benchmark`
|
|
25
|
+
- `install-local --dev-unsigned`
|
|
26
|
+
|
|
27
|
+
## 本地开发
|
|
28
|
+
|
|
29
|
+
该包的正式发布形态固定依赖已发布的 `tg-bot-plugin-contract-core==0.1.1`。
|
|
30
|
+
|
|
31
|
+
如果需要在未发布改动上进行本地多仓联调,CLI 仍可通过以下环境变量解析兄弟仓源码:
|
|
32
|
+
|
|
33
|
+
- `TG_BOT_PLUGIN_CONTRACT_CORE_SRC=/path/to/tg-bot-plugin-contract-core/src`
|
|
34
|
+
|
|
35
|
+
这条桥接路径只作为开发态回退,不再是正式 CI / release 的 canonical 依赖方式。
|
|
36
|
+
|
|
37
|
+
## CI / 发布
|
|
38
|
+
|
|
39
|
+
- 仓库 CI 会执行:
|
|
40
|
+
- `pytest`
|
|
41
|
+
- wheel / sdist 构建
|
|
42
|
+
- Docker builder 镜像构建 smoke test
|
|
43
|
+
- `tag push v*` 会额外执行:
|
|
44
|
+
- `tag == project.version` 校验
|
|
45
|
+
- PyPI 发布
|
|
46
|
+
- GHCR builder 镜像发布
|
|
47
|
+
- GitHub Release 附件上传
|
|
48
|
+
- 可复用 workflow `release-plugin.yml` 用于单插件仓库正式发版,固定执行:
|
|
49
|
+
- `tag == manifest.plugin_version` 校验
|
|
50
|
+
- `build`
|
|
51
|
+
- `inspect`
|
|
52
|
+
- `verify`
|
|
53
|
+
- `validate --mode same-profile`
|
|
54
|
+
- `validate --mode target-profile`
|
|
55
|
+
- 可复现性 smoke check
|
|
56
|
+
- bundle 产物上传 / Release asset 发布
|
|
57
|
+
|
|
58
|
+
## 使用方式
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python -m tg_bot_plugin_buildkit.cli build --source-dir ./plugin
|
|
62
|
+
python -m tg_bot_plugin_buildkit.cli inspect --bundle ./dist/plugins/demo/demo-1.2.3-cpython-3.13-linux-x86_64-gnu.tgpkg
|
|
63
|
+
python -m tg_bot_plugin_buildkit.cli verify --bundle ./dist/plugins/demo/demo-1.2.3-cpython-3.13-linux-x86_64-gnu.tgpkg
|
|
64
|
+
python -m tg_bot_plugin_buildkit.cli validate --mode same-profile --bundle ./dist/plugins/demo/demo-1.2.3-cpython-3.13-linux-x86_64-gnu.tgpkg
|
|
65
|
+
python -m tg_bot_plugin_buildkit.cli install-local --bundle ./dist/plugins/demo/demo-1.2.3-cpython-3.13-linux-x86_64-gnu.tgpkg --plugin-root ./plugins --dev-unsigned
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`build` 默认会排除 Git/CI/测试/虚拟环境与 `dist` 等仓库控制或生成目录,避免把仓库元数据和旧 bundle 再次打进新的 `.tgpkg`。
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
tg_bot_plugin_buildkit/__init__.py,sha256=UjF4KMt6OmQQFmCGHhT4hk83GA3pCiw8CN2gqMSITH4,364
|
|
2
|
+
tg_bot_plugin_buildkit/buildkit.py,sha256=8rcY5RjD7IseR213PdkFQooMdh9Qn9R1yp-l0tZcbrc,11093
|
|
3
|
+
tg_bot_plugin_buildkit/cli.py,sha256=D6RmDJSFPE2EWiRxHn_xwSrrVZi3v4tTdvy6SNqDpDM,4562
|
|
4
|
+
tg_bot_plugin_buildkit/core_adapter.py,sha256=xy3kq2ANfUSQGcfgF86htciaKRyD9BUD7bx5Zx7awTo,2309
|
|
5
|
+
tg_bot_plugin_buildkit-0.1.0.dist-info/METADATA,sha256=IGKQy9AJkUnotLyAH2CsnfkT_Qk6ykNhBm3EtOK9j4A,2504
|
|
6
|
+
tg_bot_plugin_buildkit-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
tg_bot_plugin_buildkit-0.1.0.dist-info/entry_points.txt,sha256=c-OiOcCo_MX9wwmEmKjmwnDbt1HY8ovN5WLZq3ypX1Q,75
|
|
8
|
+
tg_bot_plugin_buildkit-0.1.0.dist-info/RECORD,,
|