451 lines
13 KiB
Python
451 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
构建脚本,用于生成不同浏览器版本的扩展包
|
||
|
||
支持平台:
|
||
- Chromium(Manifest V3)
|
||
- Firefox(Manifest V2)
|
||
|
||
使用方法:
|
||
python build.py --platform all --package
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import re
|
||
import shutil
|
||
import zipfile
|
||
from pathlib import Path
|
||
|
||
|
||
def create_build_directory(platform, clean=True):
|
||
"""
|
||
创建构建目录
|
||
|
||
Args:
|
||
platform (str): 目标平台名称 (chrome/firefox)
|
||
clean (bool): 是否清理已存在的目录
|
||
|
||
Returns:
|
||
Path: 构建目录路径
|
||
"""
|
||
build_dir = Path(f"build/{platform}")
|
||
if clean and build_dir.exists():
|
||
shutil.rmtree(build_dir)
|
||
build_dir.mkdir(parents=True, exist_ok=True)
|
||
return build_dir
|
||
|
||
|
||
def copy_common_files(build_dir):
|
||
"""
|
||
复制通用文件到构建目录
|
||
|
||
Args:
|
||
build_dir (Path): 目标构建目录
|
||
"""
|
||
# 创建 src 目录结构
|
||
src_dir = build_dir / "src"
|
||
|
||
# 复制源代码目录
|
||
if os.path.exists("src"):
|
||
shutil.copytree("src", src_dir, dirs_exist_ok=True)
|
||
|
||
# 复制根目录的文档文件
|
||
doc_files = [
|
||
"LICENSE",
|
||
"README.md",
|
||
"PRIVACY_POLICY.md",
|
||
"SECURITY.md",
|
||
]
|
||
|
||
for file in doc_files:
|
||
if os.path.exists(file):
|
||
shutil.copy2(file, build_dir)
|
||
|
||
# 复制图标目录
|
||
if os.path.exists("icons"):
|
||
shutil.copytree("icons", build_dir / "icons", dirs_exist_ok=True)
|
||
|
||
|
||
def read_package_metadata():
|
||
with open("package.json", encoding="utf-8") as f:
|
||
package = json.load(f)
|
||
|
||
return {
|
||
"name": package["name"],
|
||
"version": package["version"],
|
||
"description": package["description"],
|
||
"author": package["author"],
|
||
"homepage": package["homepage"],
|
||
}
|
||
|
||
|
||
def build_chrome():
|
||
"""
|
||
构建 Chromium 版本
|
||
|
||
Returns:
|
||
Path: 构建目录路径
|
||
"""
|
||
print("构建 Chrome 版本...")
|
||
build_dir = create_build_directory("chrome")
|
||
|
||
# 复制通用文件
|
||
copy_common_files(build_dir)
|
||
|
||
# 复制 Chrome manifest
|
||
shutil.copy2("manifest.json", build_dir / "manifest.json")
|
||
|
||
# 为 Chrome 创建扁平化的文件结构
|
||
flatten_for_browser(build_dir, "chrome")
|
||
|
||
print(f"Chrome 版本构建完成: {build_dir}")
|
||
return build_dir
|
||
|
||
|
||
def build_firefox():
|
||
"""
|
||
构建 Firefox 版本
|
||
|
||
Returns:
|
||
Path: 构建目录路径
|
||
"""
|
||
print("构建 Firefox 版本...")
|
||
build_dir = create_build_directory("firefox")
|
||
|
||
# 复制通用文件
|
||
copy_common_files(build_dir)
|
||
|
||
# 复制 Firefox manifest
|
||
shutil.copy2("manifest-firefox.json", build_dir / "manifest.json")
|
||
|
||
# 为 Firefox 创建扁平化的文件结构
|
||
flatten_for_browser(build_dir, "firefox")
|
||
|
||
print(f"Firefox 版本构建完成: {build_dir}")
|
||
return build_dir
|
||
|
||
|
||
def flatten_for_browser(build_dir, platform):
|
||
"""
|
||
将 src 目录中的文件扁平化到构建目录根
|
||
|
||
Args:
|
||
build_dir (Path): 构建目录路径
|
||
platform (str): 目标平台 (chrome/firefox)
|
||
"""
|
||
src_dir = build_dir / "src"
|
||
|
||
if not src_dir.exists():
|
||
print(f"警告: {src_dir} 不存在")
|
||
return
|
||
|
||
# 将文件从 src 子目录移动到根目录
|
||
# background/index.js -> background.js
|
||
bg_file = src_dir / "background" / "index.js"
|
||
if bg_file.exists():
|
||
shutil.copy2(bg_file, build_dir / "background.js")
|
||
|
||
# content/index.js -> content.js
|
||
content_file = src_dir / "content" / "index.js"
|
||
if content_file.exists():
|
||
shutil.copy2(content_file, build_dir / "content.js")
|
||
|
||
# popup/index.html -> popup.html
|
||
popup_html = src_dir / "popup" / "index.html"
|
||
if popup_html.exists():
|
||
shutil.copy2(popup_html, build_dir / "popup.html")
|
||
|
||
# popup/popup.js -> popup.js
|
||
popup_js = src_dir / "popup" / "popup.js"
|
||
if popup_js.exists():
|
||
shutil.copy2(popup_js, build_dir / "popup.js")
|
||
|
||
# shared/platforms.js -> platforms.js
|
||
platforms_file = src_dir / "shared" / "platforms.js"
|
||
if platforms_file.exists():
|
||
shutil.copy2(platforms_file, build_dir / "platforms.js")
|
||
|
||
# shared/download-core.js -> download-core.js
|
||
download_core_file = src_dir / "shared" / "download-core.js"
|
||
if download_core_file.exists():
|
||
shutil.copy2(download_core_file, build_dir / "download-core.js")
|
||
|
||
# shared/platform-detector.js -> platform-detector.js
|
||
detector_file = src_dir / "shared" / "platform-detector.js"
|
||
if detector_file.exists():
|
||
shutil.copy2(detector_file, build_dir / "platform-detector.js")
|
||
|
||
# shared/compat/webext-compat.js -> webext-compat.js
|
||
webext_compat = src_dir / "shared" / "compat" / "webext-compat.js"
|
||
if webext_compat.exists():
|
||
shutil.copy2(webext_compat, build_dir / "webext-compat.js")
|
||
|
||
# shared/compat/firefox-compat.js -> firefox-compat.js
|
||
firefox_compat = src_dir / "shared" / "compat" / "firefox-compat.js"
|
||
if firefox_compat.exists():
|
||
shutil.copy2(firefox_compat, build_dir / "firefox-compat.js")
|
||
|
||
# 删除 src 目录
|
||
if src_dir.exists():
|
||
shutil.rmtree(src_dir)
|
||
|
||
# 平台特定优化
|
||
if platform == "chrome":
|
||
optimize_for_chrome(build_dir)
|
||
elif platform == "firefox":
|
||
optimize_for_firefox(build_dir)
|
||
|
||
|
||
def optimize_for_chrome(build_dir):
|
||
"""
|
||
为 Chrome 优化文件
|
||
|
||
Args:
|
||
build_dir (Path): 构建目录路径
|
||
"""
|
||
# 移除 background.js 中的 importScripts 调用,因为在 Manifest V3 中不需要
|
||
bg_file = build_dir / "background.js"
|
||
if bg_file.exists():
|
||
content = bg_file.read_text(encoding="utf-8")
|
||
# 注释掉 importScripts 调用
|
||
content = content.replace(
|
||
"importScripts('webext-compat.js');",
|
||
"// importScripts('webext-compat.js'); // Chrome 不需要显式导入",
|
||
)
|
||
bg_file.write_text(content, encoding="utf-8")
|
||
|
||
|
||
def optimize_for_firefox(build_dir):
|
||
"""
|
||
为 Firefox 优化文件
|
||
|
||
Args:
|
||
build_dir (Path): 构建目录路径
|
||
"""
|
||
# Firefox 特有的优化可以在这里添加
|
||
pass
|
||
|
||
|
||
def create_package(build_dir, platform):
|
||
"""
|
||
创建扩展包
|
||
|
||
Args:
|
||
build_dir (Path): 构建目录路径
|
||
platform (str): 目标平台名称 (chrome/firefox)
|
||
|
||
Returns:
|
||
Path: 扩展包文件路径
|
||
"""
|
||
package_dir = Path("packages")
|
||
package_dir.mkdir(exist_ok=True)
|
||
|
||
# 读取版本信息
|
||
manifest_file = build_dir / "manifest.json"
|
||
with open(manifest_file, encoding="utf-8") as f:
|
||
manifest = json.load(f)
|
||
|
||
version = manifest["version"]
|
||
|
||
# 根据平台设置文件扩展名和命名
|
||
if platform == "chrome":
|
||
file_ext = "zip"
|
||
platform_name = "chromium"
|
||
elif platform == "firefox":
|
||
file_ext = "xpi"
|
||
platform_name = "firefox"
|
||
else:
|
||
file_ext = "zip"
|
||
platform_name = platform
|
||
|
||
package_name = f"Xget-Now_{version}.{platform_name}.{file_ext}"
|
||
package_path = package_dir / package_name
|
||
|
||
# 创建扩展包
|
||
compression = zipfile.ZIP_DEFLATED
|
||
with zipfile.ZipFile(package_path, "w", compression) as zipf:
|
||
for file_path in build_dir.rglob("*"):
|
||
if file_path.is_file():
|
||
arcname = file_path.relative_to(build_dir)
|
||
zipf.write(file_path, arcname)
|
||
|
||
print(f"扩展包已创建: {package_path}")
|
||
return package_path
|
||
|
||
|
||
def create_userscript_package(script_path):
|
||
package_dir = Path("packages")
|
||
package_dir.mkdir(exist_ok=True)
|
||
|
||
metadata = read_package_metadata()
|
||
package_name = f"Xget-Now_{metadata['version']}.userscript.user.js"
|
||
package_path = package_dir / package_name
|
||
shutil.copy2(script_path, package_path)
|
||
print(f"用户脚本包已创建: {package_path}")
|
||
return package_path
|
||
|
||
|
||
def extract_userscript_matches():
|
||
source = Path("src/shared/download-core.js").read_text(encoding="utf-8")
|
||
match = re.search(
|
||
r"const CORE_USERSCRIPT_MATCHES = \[(.*?)\];",
|
||
source,
|
||
re.DOTALL,
|
||
)
|
||
|
||
if not match:
|
||
raise ValueError("无法从 download-core.js 中提取 CORE_USERSCRIPT_MATCHES")
|
||
|
||
raw_items = match.group(1)
|
||
return re.findall(r'"([^"]+)"', raw_items)
|
||
|
||
|
||
def build_userscript():
|
||
print("构建 userscript 版本...")
|
||
build_dir = create_build_directory("userscript")
|
||
metadata = read_package_metadata()
|
||
matches = extract_userscript_matches()
|
||
|
||
metadata_lines = [
|
||
"// ==UserScript==",
|
||
"// @name Xget Now",
|
||
f"// @namespace {metadata['homepage']}",
|
||
f"// @version {metadata['version']}",
|
||
f"// @description {metadata['description']}",
|
||
f"// @author {metadata['author']}",
|
||
"// @license GPL-3.0",
|
||
]
|
||
|
||
metadata_lines.extend(f"// @match {pattern}" for pattern in matches)
|
||
metadata_lines.extend(
|
||
[
|
||
"// @grant GM_getValue",
|
||
"// @grant GM_setValue",
|
||
"// @grant GM_deleteValue",
|
||
"// @grant GM_registerMenuCommand",
|
||
"// @grant GM_xmlhttpRequest",
|
||
"// @connect *",
|
||
"// @run-at document-start",
|
||
"// ==/UserScript==",
|
||
"",
|
||
]
|
||
)
|
||
|
||
script_parts = [
|
||
"\n".join(metadata_lines),
|
||
Path("src/shared/download-core.js").read_text(encoding="utf-8"),
|
||
"",
|
||
Path("src/userscript/index.js").read_text(encoding="utf-8"),
|
||
"",
|
||
]
|
||
|
||
output_path = build_dir / "xget-now.user.js"
|
||
output_path.write_text("\n".join(script_parts), encoding="utf-8")
|
||
|
||
print(f"Userscript 构建完成: {output_path}")
|
||
return output_path
|
||
|
||
|
||
def validate_manifest(manifest_path, platform):
|
||
"""
|
||
验证 manifest 文件格式和内容
|
||
|
||
Args:
|
||
manifest_path (str): manifest 文件路径
|
||
platform (str): 目标平台名称 (chrome/firefox)
|
||
|
||
Returns:
|
||
bool: 验证是否通过
|
||
"""
|
||
try:
|
||
with open(manifest_path, encoding="utf-8") as f:
|
||
manifest = json.load(f)
|
||
|
||
if platform == "chrome":
|
||
# 验证 Manifest V3 要求
|
||
assert manifest.get("manifest_version") == 3, "Chrome 需要 Manifest V3"
|
||
assert "action" in manifest, "Chrome 需要 action 字段"
|
||
assert "service_worker" in manifest.get("background", {}), (
|
||
"Chrome 需要 service_worker"
|
||
)
|
||
|
||
elif platform == "firefox":
|
||
# 验证 Manifest V2 要求
|
||
assert manifest.get("manifest_version") == 2, "Firefox 需要 Manifest V2"
|
||
assert "browser_action" in manifest, "Firefox 需要 browser_action 字段"
|
||
assert (
|
||
"applications" in manifest or "browser_specific_settings" in manifest
|
||
), "Firefox 需要应用ID"
|
||
|
||
print(f"[OK] {platform} manifest 验证通过")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"[FAIL] {platform} manifest 验证失败: {e}")
|
||
return False
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="构建 Xget Now 扩展")
|
||
parser.add_argument(
|
||
"--platform",
|
||
choices=["chrome", "firefox", "userscript", "all"],
|
||
default="all",
|
||
help="要构建的平台",
|
||
)
|
||
parser.add_argument("--package", action="store_true", help="创建扩展包")
|
||
parser.add_argument(
|
||
"--clean", action="store_true", default=True, help="清理构建目录"
|
||
)
|
||
|
||
args = parser.parse_args()
|
||
|
||
platforms = []
|
||
if args.platform == "all":
|
||
platforms = ["chrome", "firefox", "userscript"]
|
||
else:
|
||
platforms = [args.platform]
|
||
|
||
build_results = []
|
||
|
||
for platform in platforms:
|
||
try:
|
||
if platform == "chrome":
|
||
build_dir = build_chrome()
|
||
elif platform == "firefox":
|
||
build_dir = build_firefox()
|
||
elif platform == "userscript":
|
||
userscript_path = build_userscript()
|
||
build_results.append((platform, userscript_path))
|
||
if args.package:
|
||
create_userscript_package(userscript_path)
|
||
continue
|
||
|
||
# 验证 manifest
|
||
manifest_path = build_dir / "manifest.json"
|
||
if not validate_manifest(manifest_path, platform):
|
||
continue
|
||
|
||
build_results.append((platform, build_dir))
|
||
|
||
# 创建扩展包
|
||
if args.package:
|
||
create_package(build_dir, platform)
|
||
|
||
except Exception as e:
|
||
print(f"构建 {platform} 时出错: {e}")
|
||
|
||
# 总结
|
||
print("\n构建总结:")
|
||
for platform, build_dir in build_results:
|
||
print(f"[OK] {platform}: {build_dir}")
|
||
|
||
if args.package:
|
||
print("\n构建包位于 packages/ 目录")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|