mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-10-30 06:09:59 +08:00
add # 新增使用开源字体
This commit is contained in:
@@ -12,11 +12,16 @@ 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
|
||||
from help_image import build_help_image
|
||||
|
||||
|
||||
def render_readme(root):
|
||||
template = (root / 'readme/astrbot.md').read_text(encoding='utf-8')
|
||||
marker = '<!-- FARM_COMMANDS -->'
|
||||
if marker not in template:
|
||||
if './images/help-astrbot.png' not in template:
|
||||
raise ValueError('AstrBot README requires its help menu image')
|
||||
return template.encode('utf-8')
|
||||
if template.count(marker) != 1:
|
||||
raise ValueError('AstrBot README requires one command marker')
|
||||
definitions = runpy.run_path(str(root / 'src/farm_core/commands.py'))
|
||||
@@ -32,7 +37,7 @@ def render_readme(root):
|
||||
return template.replace(marker, '\n'.join(lines)).encode('utf-8')
|
||||
|
||||
|
||||
def build(root, output):
|
||||
def build(root, output, *, help_image=None):
|
||||
root, output = Path(root).resolve(), Path(output).resolve()
|
||||
version = read_version(root)
|
||||
if output.exists():
|
||||
@@ -53,6 +58,9 @@ def build(root, output):
|
||||
files['logo.png'] = (root / 'logo-astrbot.png').read_bytes()
|
||||
files['logo-readme.png'] = (root / 'logo.png').read_bytes()
|
||||
files['images/farm-preview.png'] = shared['images/farm-preview.png']
|
||||
files['images/help-astrbot.png'] = build_help_image(
|
||||
root, 'astrbot', help_image,
|
||||
)
|
||||
files['README.md'] = render_readme(root)
|
||||
files['runtime.py'] = (
|
||||
root / 'integrations/shared/runtime.py'
|
||||
@@ -99,6 +107,8 @@ def build(root, output):
|
||||
item.external_attr = 0o100644 << 16
|
||||
archive.writestr(item, content)
|
||||
verify_package(archive_path)
|
||||
if archive_path.stat().st_size > 16_000_000:
|
||||
raise ValueError('AstrBot plugin ZIP exceeds 16 MB')
|
||||
digest = hashlib.sha256(archive_path.read_bytes()).hexdigest()
|
||||
archive_path.with_suffix('.zip.sha256').write_text(
|
||||
f'{digest} {archive_path.name}\n', encoding='ascii',
|
||||
@@ -109,8 +119,10 @@ def build(root, output):
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
parser.add_argument('--help-image', type=Path)
|
||||
args = parser.parse_args()
|
||||
print(build(Path(__file__).resolve().parents[1], args.output))
|
||||
print(build(Path(__file__).resolve().parents[1], args.output,
|
||||
help_image=args.help_image))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -10,9 +10,10 @@ from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
from build_zhenxun import read_version, source_identity
|
||||
from verify_nonebot import MANIFEST, ROOT_NAME, verify_files, verify_package
|
||||
from help_image import build_help_image
|
||||
|
||||
|
||||
def build(root, output):
|
||||
def build(root, output, *, help_image=None):
|
||||
root, output = Path(root).resolve(), Path(output).resolve()
|
||||
version = read_version(root)
|
||||
if output.exists():
|
||||
@@ -20,6 +21,9 @@ def build(root, output):
|
||||
if output.is_relative_to(root) and not output.is_relative_to(root / "dist"):
|
||||
raise ValueError("In-project output must be inside dist")
|
||||
files = {}
|
||||
files['images/help-nonebot.png'] = build_help_image(
|
||||
root, 'nonebot', help_image,
|
||||
)
|
||||
prefix = ROOT_NAME + "/"
|
||||
integration = root / "integrations/nonebot"
|
||||
for name in ("__init__.py", "config.py", "commands.py", "data_paths.py"):
|
||||
@@ -33,7 +37,8 @@ def build(root, output):
|
||||
source = root / "src" / package
|
||||
selected = list(source.glob("*.py")) + [
|
||||
path for path in (source / "data").iterdir()
|
||||
if path.suffix in (".json", ".md")
|
||||
if path.suffix in (".json", ".md", ".zip", ".txt")
|
||||
and path.name != 'assets.zip'
|
||||
]
|
||||
for path in selected:
|
||||
if path.is_symlink():
|
||||
@@ -96,8 +101,10 @@ def build(root, output):
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument('--help-image', type=Path)
|
||||
args = parser.parse_args()
|
||||
print(build(Path(__file__).resolve().parents[1], args.output))
|
||||
print(build(Path(__file__).resolve().parents[1], args.output,
|
||||
help_image=args.help_image))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -41,7 +41,12 @@ foreach ($farmCandidate in $farmCandidates) {
|
||||
try {
|
||||
$farmExe = $farmCandidate.Exe
|
||||
$farmPrefix = $farmCandidate.Prefix
|
||||
& $farmExe @farmPrefix -c 'import sys; sys.exit(sys.version_info < (3, 10))'
|
||||
$farmProbe = 'import sys, importlib.util; sys.exit(' +
|
||||
'sys.version_info < (3, 10) or ' +
|
||||
'importlib.util.find_spec("PIL") is None)'
|
||||
# Windows PowerShell 5.1 rewrites quotes in native arguments.
|
||||
# Use stdin instead of -c; keep this script ASCII for PS 5.1.
|
||||
$farmProbe | & $farmExe @farmPrefix -
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$farmPython = $farmCandidate
|
||||
break
|
||||
@@ -51,7 +56,8 @@ foreach ($farmCandidate in $farmCandidates) {
|
||||
}
|
||||
}
|
||||
if (-not $farmPython) {
|
||||
Write-Error 'Python 3.10+ not found. Install Python or set FARM_PYTHON.'
|
||||
Write-Error ('Python 3.10+ with Pillow not found. ' +
|
||||
'Install Python/Pillow or set FARM_PYTHON.')
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import subprocess
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
from verify_zhenxun import MANIFEST, ROOT_NAME, verify_files, verify_package
|
||||
from help_image import build_help_image
|
||||
|
||||
|
||||
def source_identity(root):
|
||||
@@ -33,7 +34,7 @@ def read_version(root):
|
||||
return version
|
||||
|
||||
|
||||
def build(root, output):
|
||||
def build(root, output, *, help_image=None):
|
||||
root, output = Path(root).resolve(), Path(output).resolve()
|
||||
version = read_version(root)
|
||||
if output.exists():
|
||||
@@ -44,6 +45,9 @@ def build(root, output):
|
||||
)):
|
||||
raise ValueError("Output must not be a source directory")
|
||||
files = {}
|
||||
files['images/help-zhenxun.png'] = build_help_image(
|
||||
root, 'zhenxun', help_image,
|
||||
)
|
||||
integration = root / "integrations/zhenxun"
|
||||
for name in (
|
||||
"__init__.py", "commands.py", "data_paths.py",
|
||||
@@ -58,7 +62,7 @@ def build(root, output):
|
||||
source = root / "src" / package
|
||||
selected = list(source.glob("*.py")) + [
|
||||
p for p in (source / "data").iterdir() if p.suffix in (
|
||||
".json", ".zip", ".md",
|
||||
".json", ".zip", ".md", ".txt",
|
||||
) and p.name != 'assets.zip'
|
||||
]
|
||||
for path in selected:
|
||||
@@ -121,9 +125,10 @@ def build(root, output):
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument('--help-image', type=Path)
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
print(build(root, args.output))
|
||||
print(build(root, args.output, help_image=args.help_image))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -64,6 +64,21 @@ def main():
|
||||
for tokens in install_commands
|
||||
):
|
||||
raise ValueError("Full test suite requires AstrBot dependencies")
|
||||
for platform, workflow in (
|
||||
('zhenxun', source), ('nonebot', nonebot), ('astrbot', astrbot),
|
||||
):
|
||||
steps = workflow['jobs']['build']['steps']
|
||||
generation = next((i for i, step in enumerate(steps)
|
||||
if 'scripts/preview_help.py' in step.get('run', '')),
|
||||
None)
|
||||
build = next((i for i, step in enumerate(steps)
|
||||
if f'scripts/build_{platform}.py'
|
||||
in step.get('run', '')), None)
|
||||
if generation is None or build is None or generation >= build:
|
||||
raise ValueError('Help menu must be generated before packaging')
|
||||
if (f'--platform {platform}' not in steps[generation]['run']
|
||||
or f'help-{platform}.png' not in steps[build]['run']):
|
||||
raise ValueError('Workflow help image platform mismatch')
|
||||
if nonebot["permissions"] != {"contents": "read"}:
|
||||
raise ValueError("NoneBot source permissions differ")
|
||||
trigger = nonebot["on"]
|
||||
|
||||
+15
-11
@@ -1,21 +1,25 @@
|
||||
"""为CI选择现有字体;部署仍须显式配置中文字体。"""
|
||||
"""预览与CI复用 service 字体缓存,不将大字体打入插件。"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def choose_font():
|
||||
configured = os.environ.get("FARM_TEST_FONT")
|
||||
candidates = ([configured] if configured else []) + [
|
||||
"C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/simsun.ttc",
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"C:/Windows/Fonts/arial.ttf",
|
||||
]
|
||||
for name in candidates:
|
||||
if Path(name).is_file():
|
||||
return name
|
||||
raise RuntimeError("Set FARM_TEST_FONT to an existing font")
|
||||
if configured:
|
||||
if not Path(configured).is_file():
|
||||
raise RuntimeError("FARM_TEST_FONT does not exist")
|
||||
return configured
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(root / "src"))
|
||||
from farm_render.font_bundle import FILES, _valid, ensure_font
|
||||
local = root.parent / "farm_service/fonts/Xiaolai-Regular.ttf"
|
||||
if _valid(local, *FILES[local.name]):
|
||||
return str(local)
|
||||
cache = Path(tempfile.gettempdir()) / "farm-preview-resources"
|
||||
return str(ensure_font(cache))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -170,12 +170,27 @@ def export(root):
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def export_soils(root):
|
||||
"""小型升级土地图随插件内置,不改变既有外部素材包摘要。"""
|
||||
output = BytesIO()
|
||||
source = root / 'legacy/zhenxun_plugin_farm/resource'
|
||||
with ZipFile(output, 'w', compression=ZIP_STORED) as bundle:
|
||||
for name in ('红土地', '黑土地', '金土地'):
|
||||
path = f'soil/{name}.png'
|
||||
item = ZipInfo(path, (2020, 1, 1, 0, 0, 0))
|
||||
item.create_system = 3
|
||||
item.external_attr = 0o100644 << 16
|
||||
bundle.writestr(item, (source / path).read_bytes())
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def main():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--check", action="store_true")
|
||||
args = parser.parse_args()
|
||||
files = zip(("assets.zip", "metadata.json"), export(root))
|
||||
files = (*zip(("assets.zip", "metadata.json"), export(root)),
|
||||
('land-soils.zip', export_soils(root)))
|
||||
destination = root / "src/farm_render/data"
|
||||
for name, content in files:
|
||||
path = destination / name
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""构建期间调用独立帮助菜单脚本;不改变开发源或源码身份。"""
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def build_help_image(root, platform, prepared=None):
|
||||
if platform not in ('zhenxun', 'nonebot', 'astrbot'):
|
||||
raise ValueError('Unknown help platform')
|
||||
if prepared is None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
subprocess.run([
|
||||
sys.executable, str(Path(root) / 'scripts/preview_help.py'),
|
||||
'--platform', platform, '--output', temporary,
|
||||
], check=True, stdout=subprocess.PIPE, timeout=60)
|
||||
content = (Path(temporary) / f'help-{platform}.png').read_bytes()
|
||||
else:
|
||||
path = Path(prepared)
|
||||
if path.name != f'help-{platform}.png':
|
||||
raise ValueError('Help image belongs to a different platform')
|
||||
content = path.read_bytes()
|
||||
from PIL import Image
|
||||
with Image.open(BytesIO(content)) as image:
|
||||
if image.format != 'PNG' or image.width != 1200:
|
||||
raise ValueError('Expected generated PNG help menu at width 1200')
|
||||
image.verify()
|
||||
return content
|
||||
@@ -0,0 +1,58 @@
|
||||
"""从当前命令注册表离线生成帮助菜单图片,不访问玩家数据库。"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'src'))
|
||||
|
||||
from ci_font import choose_font
|
||||
from farm_core.commands import command_specs
|
||||
from farm_render import FarmRenderer
|
||||
from farm_render.help import render_help
|
||||
|
||||
|
||||
PLATFORMS = ('zhenxun', 'nonebot', 'astrbot')
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
'--platform', choices=(*PLATFORMS, 'all'), default='all',
|
||||
help='目标平台;默认一次生成三个平台的菜单。',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output', type=Path, default=ROOT / 'images',
|
||||
help='图片输出目录;默认主仓库 images。',
|
||||
)
|
||||
parser.add_argument('--font', type=Path, help='中文字体文件路径。')
|
||||
parser.add_argument(
|
||||
'--format', choices=('PNG', 'JPEG', 'WEBP'), default='PNG',
|
||||
type=str.upper, help='默认PNG无损图片。',
|
||||
)
|
||||
parser.add_argument('--quality', type=int, default=75,
|
||||
help='JPEG/WebP质量,1~95;PNG不受影响。')
|
||||
args = parser.parse_args(argv)
|
||||
if not 1 <= args.quality <= 95:
|
||||
parser.error('--quality 必须为1~95的整数')
|
||||
try:
|
||||
renderer = FarmRenderer(str(args.font) if args.font else choose_font())
|
||||
except (OSError, ValueError, RuntimeError) as error:
|
||||
parser.error(f'无法加载字体或素材,请用 --font 指定中文字体:{error}')
|
||||
platforms = PLATFORMS if args.platform == 'all' else (args.platform,)
|
||||
extension = {'PNG': 'png', 'JPEG': 'jpg', 'WEBP': 'webp'}[args.format]
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
for platform in platforms:
|
||||
picture = render_help(
|
||||
renderer, command_specs(platform), platform,
|
||||
image_format=args.format, quality=args.quality,
|
||||
)
|
||||
target = args.output / f'help-{platform}.{extension}'
|
||||
target.write_bytes(picture)
|
||||
print(f'{target.resolve()} ({len(picture)} bytes)')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -43,7 +43,9 @@ def verify_files(files):
|
||||
'images/farm-preview.png',
|
||||
'requirements.txt', 'LICENSE', 'version.txt', '_version.py',
|
||||
'_vendor/farm_core/__init__.py',
|
||||
'_vendor/farm_render/data/asset-manifest.json'}
|
||||
'_vendor/farm_render/data/asset-manifest.json',
|
||||
'_vendor/farm_render/data/font-manifest.json',
|
||||
'_vendor/farm_render/data/OFL-Xiaolai.txt'}
|
||||
if any(name.endswith('/assets.zip') for name in files):
|
||||
raise ValueError('Resource ZIP must not be in plugin repository')
|
||||
if not required <= set(files):
|
||||
|
||||
@@ -42,6 +42,8 @@ def verify_files(files):
|
||||
"nonebot_plugin_farm/commands.py", "nonebot_plugin_farm/runtime.py",
|
||||
"nonebot_plugin_farm/_vendor/farm_core/__init__.py",
|
||||
"nonebot_plugin_farm/_vendor/farm_render/data/asset-manifest.json",
|
||||
"nonebot_plugin_farm/_vendor/farm_render/data/font-manifest.json",
|
||||
"nonebot_plugin_farm/_vendor/farm_render/data/OFL-Xiaolai.txt",
|
||||
".github/workflows/publish.yaml", ".github/workflows/verify.yml",
|
||||
}
|
||||
if not required <= set(files):
|
||||
|
||||
@@ -38,7 +38,9 @@ def verify_files(files):
|
||||
raise ValueError("Package checksum mismatch")
|
||||
required = {"__init__.py", "runtime.py", "commands.py", "requirements.txt",
|
||||
"LICENSE", "_vendor/farm_core/__init__.py",
|
||||
"_vendor/farm_render/data/asset-manifest.json"}
|
||||
"_vendor/farm_render/data/asset-manifest.json",
|
||||
"_vendor/farm_render/data/font-manifest.json",
|
||||
"_vendor/farm_render/data/OFL-Xiaolai.txt"}
|
||||
if any(name.endswith('/assets.zip') for name in files):
|
||||
raise ValueError('Resource ZIP must not be in plugin repository')
|
||||
if not required <= set(files):
|
||||
|
||||
Reference in New Issue
Block a user