#!/usr/bin/env python3 """ Generate OSS metadata JSON files for release artifacts. Usage: python generate_oss_metadata.py \ --file dist/QwenPaw-Setup-1.0.0.exe \ --product desktop \ --platform win \ --version 1.0.0 \ --output metadata.json """ import argparse import hashlib import json import os from datetime import datetime, timezone from pathlib import Path def calculate_sha256(filepath: str) -> str: """Calculate SHA256 hash of a file.""" sha256_hash = hashlib.sha256() with open(filepath, "rb") as f: for byte_block in iter(lambda: f.read(4096), b""): sha256_hash.update(byte_block) return sha256_hash.hexdigest() def format_file_size(size_bytes: int) -> str: """Format file size to human-readable format.""" for unit in ["B", "KB", "MB", "GB"]: if size_bytes < 1024.0: return f"{size_bytes:.1f} {unit}" size_bytes /= 1024.0 return f"{size_bytes:.1f} TB" def get_file_type(filename: str) -> str: """Extract file type from filename extension.""" ext = Path(filename).suffix.lower() return ext[1:] if ext else "unknown" def generate_metadata( filepath: str, product: str, platform: str, version: str, ) -> dict: """Generate metadata for a single artifact file.""" file_path = Path(filepath) if not file_path.exists(): raise FileNotFoundError(f"File not found: {filepath}") filename = file_path.name file_size = file_path.stat().st_size sha256 = calculate_sha256(filepath) file_type = get_file_type(filename) file_id = f"{product}-{platform}-{version}" platform_names = { "win-tauri": { "zh-CN": "Windows Tauri", "en-US": "for Windows (Tauri)", }, "mac-tauri": { "zh-CN": "macOS Tauri", "en-US": "for macOS (Tauri)", }, "win": {"zh-CN": "Windows 版", "en-US": "for Windows"}, "mac": {"zh-CN": "macOS 版", "en-US": "for macOS"}, "linux": {"zh-CN": "Linux 版", "en-US": "for Linux"}, } product_names = { "desktop": {"zh-CN": "桌面客户端", "en-US": "Desktop Client"}, "cli": {"zh-CN": "命令行工具", "en-US": "CLI Tool"}, } platform_suffix = platform_names.get( platform, {"zh-CN": platform, "en-US": platform} ) product_name = product_names.get( product, {"zh-CN": product, "en-US": product} ) oss_path = f"/files/apps/{product}/{platform}/{filename}" metadata = { "id": file_id, "name": { "zh-CN": f"{product_name['zh-CN']} {platform_suffix['zh-CN']}", "en-US": f"{product_name['en-US']} {platform_suffix['en-US']}", }, "description": { "zh-CN": f"适用于 {platform_suffix['zh-CN']}的{product_name['zh-CN']}安装包", "en-US": f"{product_name['en-US']} installer {platform_suffix['en-US']}", }, "product": product, "platform": platform, "version": version, "filename": filename, "url": oss_path, "size": format_file_size(file_size), "size_bytes": file_size, "sha256": sha256, "updated_at": datetime.now(timezone.utc).isoformat(), "type": file_type, } return metadata def merge_desktop_index( existing_index_path: str, new_metadata: dict, platform: str, ) -> dict: """Merge new metadata into desktop/index.json.""" if os.path.exists(existing_index_path): with open(existing_index_path, "r", encoding="utf-8") as f: index = json.load(f) else: index = { "product": "desktop", "updated_at": datetime.now(timezone.utc).isoformat(), "platforms": {}, "files": {}, } if "platforms" not in index: index["platforms"] = {} if "files" not in index: index["files"] = {} if "product" not in index: index["product"] = "desktop" index["updated_at"] = datetime.now(timezone.utc).isoformat() if platform not in index["platforms"]: index["platforms"][platform] = {"latest": "", "versions": []} file_id = new_metadata["id"] index["platforms"][platform]["latest"] = file_id if file_id not in index["platforms"][platform]["versions"]: index["platforms"][platform]["versions"].insert(0, file_id) index["files"][file_id] = new_metadata return index def main(): parser = argparse.ArgumentParser( description="Generate OSS metadata for release artifacts" ) parser.add_argument( "--file", required=True, help="Path to the artifact file" ) parser.add_argument( "--product", required=True, help="Product name (e.g., desktop, cli)" ) parser.add_argument( "--platform", required=True, help="Platform name (e.g., win, mac, linux)", ) parser.add_argument( "--version", required=True, help="Version string (e.g., 1.0.0)" ) parser.add_argument( "--output", default="metadata.json", help="Output metadata JSON file path", ) parser.add_argument( "--merge-index", help="Path to existing desktop/index.json to merge into", ) parser.add_argument( "--output-index", help="Output path for merged desktop/index.json", ) args = parser.parse_args() print(f"Generating metadata for: {args.file}") metadata = generate_metadata( args.file, args.product, args.platform, args.version ) with open(args.output, "w", encoding="utf-8") as f: json.dump(metadata, f, indent=2, ensure_ascii=False) print(f"✓ Metadata written to: {args.output}") print(f" ID: {metadata['id']}") print(f" Size: {metadata['size']}") print(f" SHA256: {metadata['sha256'][:16]}...") # Merge into desktop/index.json if requested if args.merge_index and args.output_index: print(f"\nMerging into desktop index: {args.merge_index}") merged_index = merge_desktop_index( args.merge_index, metadata, args.platform ) with open(args.output_index, "w", encoding="utf-8") as f: json.dump(merged_index, f, indent=2, ensure_ascii=False) print(f"✓ Desktop index written to: {args.output_index}") print( f" Latest {args.platform}: {merged_index['platforms'][args.platform]['latest']}" ) if __name__ == "__main__": main()