Files
archery/package.py
gcw_4spBpAfv 28fb62e5d6 v1.2.1
2026-01-23 11:28:40 +08:00

230 lines
7.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
应用打包脚本
根据 app.yaml 中列出的文件,打包成 zip 文件
版本号从 version.py 中读取
"""
import argparse
import os
import yaml
import zipfile
from datetime import datetime
import sys
import secrets
MAGIC = b"AROTAE1" # 7 bytes: Archery OTA Encrypted v1
GCM_NONCE_LEN = 12
GCM_TAG_LEN = 16
# 添加当前目录到路径,以便导入 version 模块
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def load_app_yaml(yaml_path='app.yaml'):
"""加载 app.yaml 文件"""
try:
with open(yaml_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
except Exception as e:
print(f"[ERROR] 读取 {yaml_path} 失败: {e}")
return None
def check_files_exist(files, base_dir='.'):
"""检查文件是否存在"""
missing_files = []
existing_files = []
for file_path in files:
full_path = os.path.join(base_dir, file_path)
if os.path.exists(full_path):
existing_files.append(file_path)
else:
missing_files.append(file_path)
return existing_files, missing_files
def get_version_from_version_py():
"""从 version.py 读取版本号"""
try:
from version import VERSION
return VERSION
except ImportError:
print("[WARNING] 无法导入 version.py使用默认版本号 1.0.0")
return '1.0.0'
except Exception as e:
print(f"[WARNING] 读取 version.py 失败: {e},使用默认版本号 1.0.0")
return '1.0.0'
def create_zip_package(app_info, files, output_dir='.', base_dir='.'):
"""创建 zip 打包文件"""
# 生成输出文件名:{name}_v{version}_{timestamp}.zip
# 版本号从 version.py 读取,而不是从 app.yaml
app_name = app_info.get('name', 'app')
version = get_version_from_version_py() # 从 version.py 读取版本号
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
zip_filename = f"{app_name}_v{version}_{timestamp}.zip"
zip_path = os.path.join(output_dir, zip_filename)
print(f"[INFO] 开始打包: {zip_filename}")
print(f"[INFO] 包含文件数: {len(files)}")
try:
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for file_path in files:
full_path = os.path.join(base_dir, file_path)
# 使用相对路径作为 zip 内的路径
zipf.write(full_path, file_path)
print(f"{file_path}")
# 获取文件大小
file_size = os.path.getsize(zip_path)
file_size_mb = file_size / (1024 * 1024)
print(f"\n[SUCCESS] 打包完成!")
print(f" 文件名: {zip_filename}")
print(f" 文件大小: {file_size_mb:.2f} MB ({file_size:,} 字节)")
print(f" 文件路径: {os.path.abspath(zip_path)}")
return zip_path
except Exception as e:
print(f"[ERROR] 打包失败: {e}")
import traceback
traceback.print_exc()
return None
def _validate_key_hex(key_hex: str) -> bytes:
if not isinstance(key_hex, str):
raise ValueError("aead key must be hex string")
key_hex = key_hex.strip().lower()
if key_hex.startswith("0x"):
key_hex = key_hex[2:]
if len(key_hex) != 64:
raise ValueError("aead key must be 64 hex chars (32 bytes)")
try:
key = bytes.fromhex(key_hex)
except Exception as e:
raise ValueError(f"invalid hex key: {e}")
if len(key) != 32:
raise ValueError("aead key must be 32 bytes")
return key
def encrypt_zip_aead(zip_path: str, key_hex: str, out_ext: str = ".enc") -> str:
"""
Encrypt the whole zip file as one blob:
output format: MAGIC(7) | nonce(12) | ciphertext(N) | tag(16)
using AES-256-GCM (AEAD).
"""
# Lazy import: packaging-only dependency
try:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
except Exception as e:
raise RuntimeError(
"Missing dependency: cryptography. Install with: pip install cryptography. "
f"Import error: {e}"
)
key = _validate_key_hex(key_hex)
with open(zip_path, "rb") as f:
plain = f.read()
nonce = secrets.token_bytes(GCM_NONCE_LEN)
aesgcm = AESGCM(key)
ct_and_tag = aesgcm.encrypt(nonce, plain, None) # ciphertext || tag (16 bytes)
enc_path = zip_path + out_ext if out_ext else (zip_path + ".enc")
with open(enc_path, "wb") as f:
f.write(MAGIC)
f.write(nonce)
f.write(ct_and_tag)
return enc_path
def main():
"""主函数"""
parser = argparse.ArgumentParser(description="打包 app.yaml 文件列表到 zip并可选进行 AES-256-GCM 加密输出 .enc")
parser.add_argument("--aead-key-hex", default=None, help="AES-256-GCM key (64 hex chars = 32 bytes). If set, output encrypted file.")
parser.add_argument("--keep-zip", action="store_true", help="Keep the plaintext zip when encryption is enabled.")
parser.add_argument("--out-ext", default=".enc", help="Encrypted output extension appended to zip path. Default: .enc (produces *.zip.enc)")
args = parser.parse_args()
print("=" * 60)
print("应用打包脚本")
print("=" * 60)
# 1. 加载 app.yaml
app_info = load_app_yaml('app.yaml')
if app_info is None:
return
# 从 version.py 读取版本号
version = get_version_from_version_py()
print(f"\n[INFO] 应用信息:")
print(f" ID: {app_info.get('id', 'N/A')}")
print(f" 名称: {app_info.get('name', 'N/A')}")
print(f" 版本: {version} (来自 version.py)")
print(f" 作者: {app_info.get('author', 'N/A')}")
if app_info.get('version') != version:
print(f" [注意] app.yaml 中的版本 ({app_info.get('version', 'N/A')}) 与 version.py 不一致")
# 2. 获取文件列表
files = app_info.get('files', [])
if not files:
print("[ERROR] app.yaml 中没有找到 files 列表")
return
print(f"\n[INFO] 文件列表 ({len(files)} 个文件):")
# 3. 检查文件是否存在
existing_files, missing_files = check_files_exist(files)
if missing_files:
print(f"\n[WARNING] 以下文件不存在,将被跳过:")
for f in missing_files:
print(f"{f}")
if not existing_files:
print("\n[ERROR] 没有找到任何有效文件,无法打包")
return
print(f"\n[INFO] 找到 {len(existing_files)} 个有效文件")
# 4. 创建 zip 包
zip_path = create_zip_package(app_info, existing_files)
if zip_path:
enc_path = None
if args.aead_key_hex:
try:
enc_path = encrypt_zip_aead(zip_path, args.aead_key_hex, out_ext=args.out_ext)
enc_size = os.path.getsize(enc_path)
print(f"\n[SUCCESS] AEAD加密完成: {os.path.basename(enc_path)} ({enc_size:,} bytes)")
print(f" 文件路径: {os.path.abspath(enc_path)}")
if not args.keep_zip:
try:
os.remove(zip_path)
print(f"[INFO] 已删除明文zip: {os.path.basename(zip_path)}")
except Exception as e:
print(f"[WARNING] 删除明文zip失败可忽略: {e}")
except Exception as e:
print(f"\n[ERROR] AEAD加密失败: {e}")
print("[ERROR] 保留明文zip用于排查。")
print("\n" + "=" * 60)
print("打包成功完成!")
print("=" * 60)
else:
print("\n" + "=" * 60)
print("打包失败!")
print("=" * 60)
if __name__ == "__main__":
main()