mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-10-28 13:10:02 +08:00
fixed # 新增本地部署脚本,一键生成对应平台的压缩包内容
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""白名单生成AstrBot可安装包,复用已校验的共享模块打包路径。"""
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
from build_zhenxun import build as build_shared, read_version, source_identity
|
||||
from verify_astrbot import MANIFEST, ROOT_NAME, verify_files, verify_package
|
||||
from verify_zhenxun import read_package as read_shared
|
||||
|
||||
|
||||
def build(root, output):
|
||||
root, output = Path(root).resolve(), Path(output).resolve()
|
||||
version = read_version(root)
|
||||
if output.exists():
|
||||
raise FileExistsError('Use a new output directory')
|
||||
if output.is_relative_to(root) and not output.is_relative_to(root / 'dist'):
|
||||
raise ValueError('In-project output must be inside dist')
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
shared = read_shared(build_shared(root, Path(temporary) / 'shared'))
|
||||
files = {name: content for name, content in shared.items()
|
||||
if name.startswith('_vendor/') or name in (
|
||||
'_version.py', 'version.txt', 'LICENSE',
|
||||
)}
|
||||
source = root / 'integrations/astrbot'
|
||||
for name in ('main.py', '_conf_schema.json', 'requirements.txt'):
|
||||
files[name] = (source / name).read_bytes()
|
||||
files['README.md'] = (source / 'README.md').read_bytes()
|
||||
files['runtime.py'] = (root / 'integrations/zhenxun/runtime.py').read_bytes()
|
||||
files['verify_package.py'] = (root / 'scripts/verify_astrbot.py').read_bytes()
|
||||
# JSON是YAML的子集;标准库即可生成AstrBot可读的metadata.yaml。
|
||||
files['metadata.yaml'] = (json.dumps({
|
||||
'name': ROOT_NAME, 'display_name': '农场',
|
||||
'desc': '种植、收获、仓库、交易与农场图片,复用Farm共享核心。',
|
||||
'version': version, 'author': 'Shu-Ying',
|
||||
'repo': 'https://github.com/Shu-Ying/astrbot_plugin_farm',
|
||||
}, ensure_ascii=False, indent=2) + '\n').encode()
|
||||
for name, content in files.items():
|
||||
if name.endswith('.py'):
|
||||
ast.parse(content, filename=name, feature_version=(3, 10))
|
||||
manifest = {
|
||||
'schema': 1, 'target': 'astrbot', 'plugin': ROOT_NAME,
|
||||
'version': version, 'source': source_identity(root),
|
||||
'files': {name: hashlib.sha256(content).hexdigest()
|
||||
for name, content in sorted(files.items())},
|
||||
}
|
||||
files[MANIFEST] = (json.dumps(
|
||||
manifest, ensure_ascii=False, sort_keys=True, indent=2,
|
||||
) + '\n').encode()
|
||||
verify_files(files)
|
||||
output.mkdir(parents=True)
|
||||
for name, content in sorted(files.items()):
|
||||
target = output / 'repository' / name
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(content)
|
||||
archive_path = output / f'{ROOT_NAME}-{version}.zip'
|
||||
with ZipFile(archive_path, 'w', compression=ZIP_DEFLATED) as archive:
|
||||
for name, content in sorted(files.items()):
|
||||
item = ZipInfo(f'{ROOT_NAME}/{name}', (2020, 1, 1, 0, 0, 0))
|
||||
item.create_system = 3
|
||||
item.compress_type = ZIP_DEFLATED
|
||||
item.external_attr = 0o100644 << 16
|
||||
archive.writestr(item, content)
|
||||
verify_package(archive_path)
|
||||
digest = hashlib.sha256(archive_path.read_bytes()).hexdigest()
|
||||
archive_path.with_suffix('.zip.sha256').write_text(
|
||||
f'{digest} {archive_path.name}\n', encoding='ascii',
|
||||
)
|
||||
return archive_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
print(build(Path(__file__).resolve().parents[1], args.output))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,71 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('nonebot', 'zhenxun', 'astrbot')]
|
||||
[string]$Platform,
|
||||
[string]$OutputDirectory
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$farmProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$farmCandidates = @()
|
||||
if ($env:FARM_PYTHON) {
|
||||
$farmCandidates += @{ Exe = $env:FARM_PYTHON; Prefix = @() }
|
||||
} else {
|
||||
foreach ($farmLocal in @('.venv\Scripts\python.exe',
|
||||
'venv\Scripts\python.exe')) {
|
||||
$farmCandidate = Join-Path $farmProjectRoot $farmLocal
|
||||
if (Test-Path -LiteralPath $farmCandidate -PathType Leaf) {
|
||||
$farmCandidates += @{ Exe = $farmCandidate; Prefix = @() }
|
||||
}
|
||||
}
|
||||
foreach ($farmName in @('python', 'py')) {
|
||||
$farmCommand = Get-Command $farmName -ErrorAction SilentlyContinue
|
||||
if ($farmCommand) {
|
||||
$farmPrefix = @()
|
||||
if ($farmName -eq 'py') { $farmPrefix = @('-3') }
|
||||
$farmCandidates += @{ Exe = $farmCommand.Source;
|
||||
Prefix = $farmPrefix }
|
||||
}
|
||||
}
|
||||
if ($env:USERPROFILE) {
|
||||
$farmBundled = Join-Path $env:USERPROFILE ('.cache\codex-runtimes\' +
|
||||
'codex-primary-runtime\dependencies\python\python.exe')
|
||||
if (Test-Path -LiteralPath $farmBundled -PathType Leaf) {
|
||||
$farmCandidates += @{ Exe = $farmBundled; Prefix = @() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$farmPython = $null
|
||||
foreach ($farmCandidate in $farmCandidates) {
|
||||
try {
|
||||
$farmExe = $farmCandidate.Exe
|
||||
$farmPrefix = $farmCandidate.Prefix
|
||||
& $farmExe @farmPrefix -c 'import sys; sys.exit(sys.version_info < (3, 10))'
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$farmPython = $farmCandidate
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
Write-Host ('Python candidate unavailable: ' + $farmCandidate.Exe)
|
||||
}
|
||||
}
|
||||
if (-not $farmPython) {
|
||||
Write-Error 'Python 3.10+ not found. Install Python or set FARM_PYTHON.'
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||
$farmGit = Join-Path $env:USERPROFILE ('.cache\codex-runtimes\' +
|
||||
'codex-primary-runtime\dependencies\native\git\cmd')
|
||||
if (Test-Path -LiteralPath (Join-Path $farmGit 'git.exe')) {
|
||||
$env:PATH = $farmGit + ';' + $env:PATH
|
||||
}
|
||||
}
|
||||
$farmExe = $farmPython.Exe
|
||||
$farmArguments = @($farmPython.Prefix) + @(
|
||||
(Join-Path $PSScriptRoot 'build_plugin.py'), $Platform
|
||||
)
|
||||
if ($OutputDirectory) { $farmArguments += @('--output', $OutputDirectory) }
|
||||
& $farmExe @farmArguments
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,36 @@
|
||||
"""三平台本地打包入口;每次使用独立新目录,不执行发布或安装。"""
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
|
||||
|
||||
PLATFORMS = ('nonebot', 'zhenxun', 'astrbot')
|
||||
|
||||
|
||||
def build_plugin(platform, output=None, *, root=None):
|
||||
if platform not in PLATFORMS:
|
||||
raise ValueError('Unsupported platform')
|
||||
source = Path(__file__).resolve().parents[1]
|
||||
if output is None:
|
||||
parent = Path(root) if root else source / 'dist'
|
||||
timestamp = datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')
|
||||
output = parent / f'{platform}-{timestamp}-{uuid.uuid4().hex[:8]}'
|
||||
module = importlib.import_module('build_' + platform)
|
||||
return module.build(source, Path(output).resolve())
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('platform', choices=PLATFORMS)
|
||||
parser.add_argument('--output', type=Path)
|
||||
args = parser.parse_args()
|
||||
archive = build_plugin(args.platform, args.output)
|
||||
print(f'Package: {archive}')
|
||||
print(f'Checksum: {archive.with_suffix(".zip.sha256")}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -17,9 +17,11 @@ def source_identity(root):
|
||||
return subprocess.check_output(
|
||||
["git", *args], cwd=root, text=True, encoding="utf-8",
|
||||
).strip()
|
||||
commit = git("rev-parse", "HEAD")
|
||||
return {
|
||||
"repository": "https://github.com/Shu-Ying/main_plugin_farm.git",
|
||||
"commit": git("rev-parse", "HEAD"),
|
||||
"commit": commit,
|
||||
"message": git("show", "-s", "--format=%B", commit),
|
||||
"dirty": bool(git("status", "--porcelain", "--untracked-files=normal")),
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from urllib.error import HTTPError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from verify_nonebot import read_package, verify_files
|
||||
from release_metadata import commit_message
|
||||
|
||||
REPOSITORY = "Shu-Ying/nonebot_plugin_farm"
|
||||
BRANCH = "main"
|
||||
@@ -39,8 +40,10 @@ def github(method, path, payload, token, allow_missing=False):
|
||||
def publish(package, confirmed=False, request=None):
|
||||
files = read_package(package)
|
||||
manifest = verify_files(files)
|
||||
message = commit_message(manifest)
|
||||
result = {"repository": REPOSITORY, "branch": BRANCH,
|
||||
"version": manifest["version"], "published": False}
|
||||
"version": manifest["version"], "message": message,
|
||||
"published": False}
|
||||
if not confirmed:
|
||||
return result
|
||||
if manifest["source"]["dirty"]:
|
||||
@@ -75,8 +78,7 @@ def publish(package, confirmed=False, request=None):
|
||||
"sha": blob["sha"]})
|
||||
generated = call("POST", "git/trees", {"tree": tree})
|
||||
commit = call("POST", "git/commits", {
|
||||
"message": f"Generate NoneBot farm {manifest['version']} "
|
||||
f"from {manifest['source']['commit']}",
|
||||
"message": message,
|
||||
"tree": generated["sha"], "parents": [parent] if parent else [],
|
||||
})
|
||||
if parent:
|
||||
|
||||
@@ -8,6 +8,7 @@ from urllib.error import HTTPError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from verify_zhenxun import read_package, verify_files
|
||||
from release_metadata import commit_message
|
||||
|
||||
REPOSITORY = "Shu-Ying/zhenxun_plugin_farm"
|
||||
BRANCH = "main"
|
||||
@@ -39,8 +40,10 @@ def github(method, path, payload, token, allow_missing=False):
|
||||
def publish(package, confirmed=False, request=None):
|
||||
files = read_package(package)
|
||||
manifest = verify_files(files)
|
||||
message = commit_message(manifest)
|
||||
result = {"repository": REPOSITORY, "branch": BRANCH,
|
||||
"version": manifest["version"], "published": False}
|
||||
"version": manifest["version"], "message": message,
|
||||
"published": False}
|
||||
if not confirmed:
|
||||
return result
|
||||
if manifest["source"]["dirty"]:
|
||||
@@ -75,8 +78,7 @@ def publish(package, confirmed=False, request=None):
|
||||
"sha":blob["sha"]})
|
||||
generated = call("POST", "git/trees", {"tree":tree})
|
||||
commit = call("POST", "git/commits", {
|
||||
"message": f"Generate farm preview {manifest['version']} "
|
||||
f"from {manifest['source']['commit']}",
|
||||
"message": message,
|
||||
"tree": generated["sha"], "parents": [parent] if parent else [],
|
||||
})
|
||||
if parent:
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""从构建清单生成发布提交说明,不查询发布时的工作目录。"""
|
||||
|
||||
|
||||
def commit_message(manifest):
|
||||
source = manifest["source"]
|
||||
message = source.get("message")
|
||||
if not isinstance(message, str) or not message.strip():
|
||||
raise ValueError("Missing source commit message; rebuild the package")
|
||||
return (
|
||||
f"{message.rstrip()}\n\n"
|
||||
f"Farm-Source-Commit: {source['commit']}\n"
|
||||
f"Farm-Version: {manifest['version']}"
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
"""AstrBot插件ZIP的独立完整性校验;只使用标准库。"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
import stat
|
||||
from zipfile import ZipFile
|
||||
|
||||
|
||||
ROOT_NAME = 'astrbot_plugin_farm'
|
||||
MANIFEST = 'FARM-MANIFEST.json'
|
||||
MAX_BYTES = 100 * 1024 * 1024
|
||||
|
||||
|
||||
def safe_path(name):
|
||||
path = PurePosixPath(name)
|
||||
if (not name or '\\' in name or ':' in name or path.is_absolute()
|
||||
or any(p in ('', '.', '..') for p in name.split('/'))):
|
||||
raise ValueError('Unsafe package path')
|
||||
return path
|
||||
|
||||
|
||||
def verify_files(files):
|
||||
if MANIFEST not in files or len(files[MANIFEST]) > 1024 * 1024:
|
||||
raise ValueError('Missing or oversized manifest')
|
||||
manifest = json.loads(files[MANIFEST])
|
||||
if (manifest.get('schema') != 1 or manifest.get('target') != 'astrbot'
|
||||
or manifest.get('plugin') != ROOT_NAME):
|
||||
raise ValueError('Invalid package identity')
|
||||
expected = manifest.get('files')
|
||||
if not isinstance(expected, dict) or set(files) != set(expected) | {MANIFEST}:
|
||||
raise ValueError('Package files differ from manifest')
|
||||
for name, digest in expected.items():
|
||||
safe_path(name)
|
||||
if hashlib.sha256(files[name]).hexdigest() != digest:
|
||||
raise ValueError('Package checksum mismatch')
|
||||
required = {'main.py', 'metadata.yaml', '_conf_schema.json', 'runtime.py',
|
||||
'requirements.txt', 'LICENSE', 'version.txt', '_version.py',
|
||||
'_vendor/farm_core/__init__.py',
|
||||
'_vendor/farm_render/data/assets.zip'}
|
||||
if not required <= set(files):
|
||||
raise ValueError('Incomplete plugin')
|
||||
metadata = json.loads(files['metadata.yaml'])
|
||||
if (metadata.get('name') != ROOT_NAME
|
||||
or metadata.get('version') != manifest['version']
|
||||
or files['version.txt'].decode().strip() != manifest['version']):
|
||||
raise ValueError('Package version mismatch')
|
||||
return manifest
|
||||
|
||||
|
||||
def read_package(path):
|
||||
with ZipFile(path) as archive:
|
||||
items = archive.infolist()
|
||||
names = [item.filename for item in items]
|
||||
if (len(names) != len(set(names)) or len(names) > 200
|
||||
or sum(item.file_size for item in items) > MAX_BYTES):
|
||||
raise ValueError('Duplicate paths or package too large')
|
||||
files = {}
|
||||
for item in items:
|
||||
name = safe_path(item.filename)
|
||||
if (len(name.parts) < 2 or name.parts[0] != ROOT_NAME
|
||||
or item.is_dir()
|
||||
or stat.S_ISLNK(item.external_attr >> 16)):
|
||||
raise ValueError('Unexpected package root or link')
|
||||
files[name.relative_to(ROOT_NAME).as_posix()] = archive.read(item)
|
||||
verify_files(files)
|
||||
return files
|
||||
|
||||
|
||||
def verify_package(path):
|
||||
return verify_files(read_package(path))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('package', type=Path)
|
||||
args = parser.parse_args()
|
||||
manifest = verify_package(args.package)
|
||||
print(f"Verified astrbot {manifest['version']}: "
|
||||
f"{len(manifest['files'])} files")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user