mirror of
https://github.com/zsbai/wechat-versions.git
synced 2026-05-13 07:16:43 +00:00
refactor: rewrite in Python (#7)
* refactor: rewrite destVersionForMac in python; remove redundant code & files; new workflow parameter: Force create release using latest WeChat dmg; * misc: update README file;
This commit is contained in:
11
.github/workflows/destversion.yml
vendored
11
.github/workflows/destversion.yml
vendored
@@ -4,15 +4,20 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: '0 7 * * *'
|
- cron: '0 7 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
force_release:
|
||||||
|
description: 'Force create release (ignore version checks)'
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
save_new_wechat:
|
save_new_wechat:
|
||||||
runs-on: macOS-latest
|
runs-on: macOS-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
# - name: Test Github Action Server Time
|
|
||||||
# run: echo `date`
|
|
||||||
- name: Check new version and push
|
- name: Check new version and push
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: bash -x ./scripts/destVersionForMac.sh ${{ github.event.inputs.download_link }}
|
FORCE_RELEASE: ${{ github.event.inputs.force_release }}
|
||||||
|
run: python3 ./scripts/destVersionForMac.py
|
||||||
|
|||||||
18
.github/workflows/notify.yml
vendored
18
.github/workflows/notify.yml
vendored
@@ -1,18 +0,0 @@
|
|||||||
name: Wechat Release Notify
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
notify_to_tg:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Check new version and push
|
|
||||||
env:
|
|
||||||
GHTOKEN: ${{ secrets.GHTOKEN }}
|
|
||||||
BOTTOKEN: ${{ secrets.BOTTOKEN }}
|
|
||||||
CHATIDS: ${{ secrets.CHATIDS }}
|
|
||||||
run: bash -x ./scripts/notify.sh
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
temp/
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# This below was automatically created for macOS system
|
# This below was automatically created for macOS system
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -1,23 +1,21 @@
|
|||||||
# Wechat-Versions-For-macOS
|
# Wechat For Macos Version Archive
|
||||||
收集 Mac 微信版本并保存
|
收集官网 Mac 微信版本并保存至release。
|
||||||
|
|
||||||
## 目录结构
|
## 更新日志
|
||||||
```shell
|
* 2026.2.12:使用python重写脚本,移除冗余文件和代码。在`4.0.5`版本后,之前获取精确版本号的规则失效,目前已修复;未来如果继续出现无法获取精确版本号则会自动采用大版本+build的形式(如`v4.0.0+build.12345`)。
|
||||||
├── README.md # 自述文件
|
|
||||||
├── WeChatSetup # 微信安装包临时目录
|
|
||||||
│ └── temp # 临时目录
|
|
||||||
└── scripts # 脚本目录
|
|
||||||
├── destVersioForMac.sh # 获取安装包及取得版本号与 hash 值的脚本
|
|
||||||
└── notify.sh # 新release 时调用通知的脚本
|
|
||||||
```
|
|
||||||
|
|
||||||
## 说明
|
* 2024.10.1:通过转换dmg为img后解压,获取精确的微信版本号(例如:3.8.9.xx);在此之前,后两位小版本号无法获取,所以通过添加更新日期后缀来区分大版本中的小版本,需在下载前自行判断。
|
||||||
|
|
||||||
* 2024.10.1 更新:通过转换dmg为img后解压,获取精确的微信版本号(例如:3.8.9.xx);在此之前,后两位小版本号无法获取,所以通过添加更新日期后缀来区分大版本中的小版本,需在下载前自行判断。
|
项目使用 Github Action 每天自动检测微信**官网新版本更新**,计算 Hash/MD5 值并推送至仓库 Release。
|
||||||
|
|
||||||
项目使用 Github Action 自动下载微信**官网最新版本安装包**计算 Hash 值并推送至仓库 Release,目前暂无保留App Store安装包的想法。
|
|
||||||
|
|
||||||
|
项目仅抓取官网的Mac安装包,并不包含App Store中的版本。
|
||||||
|
|
||||||
各版本更新日志可参见官网 [changelog](https://weixin.qq.com/cgi-bin/readtemplate?lang=zh_CN&t=weixin_faq_list&head=true)
|
各版本更新日志可参见官网 [changelog](https://weixin.qq.com/cgi-bin/readtemplate?lang=zh_CN&t=weixin_faq_list&head=true)
|
||||||
|
|
||||||
|
相关项目:
|
||||||
|
- [微信 Windows 64 位 3.0 版本存档](https://github.com/tom-snow/wechat-windows-versions)
|
||||||
|
- [微信 Windows 32 位 3.0 版本存档](https://github.com/tom-snow/wechat-windows-versions-x86)
|
||||||
|
- [微信 Mac 3.0 & 4.0 版本存档](https://github.com/zsbai/wechat-versions)
|
||||||
|
|
||||||
|
|
||||||
*如有问题/侵权,请直接提交 issue 告知。*
|
*如有问题/侵权,请直接提交 issue 告知。*
|
||||||
|
|||||||
491
scripts/destVersionForMac.py
Normal file
491
scripts/destVersionForMac.py
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import html.parser
|
||||||
|
import os
|
||||||
|
import plistlib
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
WEBSITE_URL = "https://mac.weixin.qq.com/?t=mac&lang=zh_CN"
|
||||||
|
BASE_DIR = Path.cwd() / "WeChatMac"
|
||||||
|
TEMP_DIR = BASE_DIR / "temp"
|
||||||
|
|
||||||
|
class DownloadLinkParser(html.parser.HTMLParser):
|
||||||
|
"""
|
||||||
|
从微信 Mac 官方网站的 HTML 中解析下载链接。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.link = ""
|
||||||
|
|
||||||
|
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||||||
|
if tag != "a" or self.link:
|
||||||
|
return
|
||||||
|
attrs_dict = {key: value or "" for key, value in attrs}
|
||||||
|
classes = attrs_dict.get("class", "").split()
|
||||||
|
if "download-button" in classes:
|
||||||
|
self.link = attrs_dict.get("href", "").strip()
|
||||||
|
|
||||||
|
def run(
|
||||||
|
cmd: list[str], check: bool = True, capture: bool = True
|
||||||
|
) -> subprocess.CompletedProcess:
|
||||||
|
"""
|
||||||
|
运行子进程命令的辅助函数,默认捕获输出并检查返回码。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd (list[str]): 要执行的命令列表。
|
||||||
|
check (bool, optional): 是否检查命令返回码。默认为 True。
|
||||||
|
capture (bool, optional): 是否捕获命令输出。默认为 True。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
subprocess.CompletedProcess: 子进程执行结果。
|
||||||
|
"""
|
||||||
|
return subprocess.run(
|
||||||
|
cmd,
|
||||||
|
check=check,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE if capture else None,
|
||||||
|
stderr=subprocess.STDOUT if capture else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def log(message: str) -> None:
|
||||||
|
"""
|
||||||
|
打印日志信息并立即刷新输出缓冲区。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (str): 要打印的日志信息。
|
||||||
|
"""
|
||||||
|
print(message, flush=True)
|
||||||
|
|
||||||
|
def fetch_download_link() -> str:
|
||||||
|
"""
|
||||||
|
从微信 Mac 官方网站获取最新的下载链接
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: 如果在网站上未找到下载链接
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 下载链接 URL
|
||||||
|
"""
|
||||||
|
# Fetch HTML and extract the first download link on the page.
|
||||||
|
with urllib.request.urlopen(WEBSITE_URL, timeout=30) as response:
|
||||||
|
html = response.read().decode("utf-8", errors="replace")
|
||||||
|
parser = DownloadLinkParser()
|
||||||
|
parser.feed(html)
|
||||||
|
if not parser.link:
|
||||||
|
raise RuntimeError("Download link not found on website.")
|
||||||
|
return parser.link
|
||||||
|
|
||||||
|
def fetch_head_metadata(url: str) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
使用 HEAD 请求从直接文件链接读取元数据。
|
||||||
|
Args:
|
||||||
|
url (str): 文件的直接下载链接
|
||||||
|
Returns:
|
||||||
|
dict[str, str]: 从 HEAD 响应中提取的元数据字典,键为小写字符串,值为对应的响应头值
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Use HEAD request to read metadata from the direct file link.
|
||||||
|
request = urllib.request.Request(url, method="HEAD")
|
||||||
|
with urllib.request.urlopen(request, timeout=30) as response:
|
||||||
|
return {key.lower(): value.strip() for key, value in response.headers.items()}
|
||||||
|
|
||||||
|
def download_with_retry(url: str, dest: Path) -> None:
|
||||||
|
"""
|
||||||
|
下载软件包
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): 文件的直接下载链接
|
||||||
|
dest (Path): 目标文件路径
|
||||||
|
"""
|
||||||
|
# Keep temp files under current working directory for debugging.
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
attempts = 2
|
||||||
|
last_error: Exception | None = None
|
||||||
|
for attempt in range(1, attempts + 1):
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"wget",
|
||||||
|
"--quiet",
|
||||||
|
"--tries",
|
||||||
|
"5",
|
||||||
|
"--waitretry",
|
||||||
|
"5",
|
||||||
|
"--retry-connrefused",
|
||||||
|
"--timeout",
|
||||||
|
"30",
|
||||||
|
url,
|
||||||
|
"-O",
|
||||||
|
str(dest),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
last_error = exc
|
||||||
|
if attempt < attempts:
|
||||||
|
log(f"Download failed (attempt {attempt}). Waiting before retry...")
|
||||||
|
time.sleep(10)
|
||||||
|
if last_error:
|
||||||
|
raise last_error
|
||||||
|
|
||||||
|
def mount_dmg(dmg_path: Path) -> str:
|
||||||
|
"""
|
||||||
|
挂载目标 dmg 镜像到本地
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dmg_path (Path): dmg 镜像文件路径
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: 如果挂载失败或未找到挂载点
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 挂载点路径
|
||||||
|
"""
|
||||||
|
# Mount DMG and capture the mount point under /Volumes.
|
||||||
|
result = run(["hdiutil", "attach", str(dmg_path), "-nobrowse"])
|
||||||
|
matches = re.findall(r"(/Volumes/[^\n]+)", result.stdout)
|
||||||
|
if not matches:
|
||||||
|
raise RuntimeError("Failed to mount DMG.")
|
||||||
|
return matches[-1].strip()
|
||||||
|
|
||||||
|
def detach_dmg(mount_dir: str) -> None:
|
||||||
|
"""
|
||||||
|
解除挂载对应目录
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mount_dir (str): 挂载点路径
|
||||||
|
"""
|
||||||
|
run(["hdiutil", "detach", mount_dir], check=False)
|
||||||
|
|
||||||
|
def get_tag_from_plist(mount_dir: str) -> str:
|
||||||
|
"""
|
||||||
|
解析 Info.plist 构建 Tag 标签
|
||||||
|
|
||||||
|
如果 WeChatBundleVersion 存在则直接使用,否则使用 CFBundleShortVersionString 和 CFBundleVersion 组合的形式。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mount_dir (str): 挂载路径
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: 如果 Info.plist 文件未找到
|
||||||
|
RuntimeError: 如果 CFBundleShortVersionString 未找到
|
||||||
|
RuntimeError: 如果 CFBundleVersion 未找到
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 构建标签
|
||||||
|
"""
|
||||||
|
# Info.plist lives inside the mounted WeChat.app bundle.
|
||||||
|
info_plist = Path(mount_dir) / "WeChat.app" / "Contents" / "Info.plist"
|
||||||
|
if not info_plist.exists():
|
||||||
|
raise RuntimeError("Info.plist not found in mounted volume.")
|
||||||
|
with info_plist.open("rb") as handle:
|
||||||
|
data = plistlib.load(handle)
|
||||||
|
short_version = str(data.get("CFBundleShortVersionString", "")).strip()
|
||||||
|
build = str(data.get("CFBundleVersion", "")).strip()
|
||||||
|
version = str(data.get("WeChatBundleVersion", "")).strip()
|
||||||
|
|
||||||
|
if not short_version:
|
||||||
|
raise RuntimeError("CFBundleShortVersionString not found.")
|
||||||
|
if not build:
|
||||||
|
raise RuntimeError("CFBundleVersion not found.")
|
||||||
|
if version:
|
||||||
|
tag = version
|
||||||
|
else:
|
||||||
|
tag = f"{short_version}+build.{build}"
|
||||||
|
return tag
|
||||||
|
|
||||||
|
def compute_sha256(file_path: Path) -> str:
|
||||||
|
"""
|
||||||
|
计算文件的 sha256
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (Path): 文件路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 文件的 sha256 值
|
||||||
|
"""
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with file_path.open("rb") as handle:
|
||||||
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
def parse_release_body(body: str) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
从 GitHub release 的 body 文本中解析出键值对信息,返回一个字典。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
body (str): GitHub release 的 body 文本,预期包含多行 "Key: Value" 格式的内容。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, str]: 从 body 中解析出的键值对字典.
|
||||||
|
"""
|
||||||
|
info: dict[str, str] = {}
|
||||||
|
for line in body.splitlines():
|
||||||
|
if ":" not in line:
|
||||||
|
continue
|
||||||
|
key, value = line.split(":", 1)
|
||||||
|
info[key.lstrip("- ").strip()] = value.strip()
|
||||||
|
return info
|
||||||
|
|
||||||
|
def get_latest_release_info() -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
获取最新 GitHub release 的信息,返回一个包含键值对的字典。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, str]: 包含最新 release 信息的字典
|
||||||
|
"""
|
||||||
|
result = run(
|
||||||
|
["gh", "release", "view", "--json", "body", "--jq", ".body"], check=False
|
||||||
|
)
|
||||||
|
if result.returncode != 0 or not result.stdout:
|
||||||
|
return {}
|
||||||
|
return parse_release_body(result.stdout)
|
||||||
|
|
||||||
|
def tag_exists(tag: str) -> bool:
|
||||||
|
"""
|
||||||
|
检查指定的 Git 标签是否已经存在于远程仓库中。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tag (str): Git 标签名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 指定的 Git 标签是否存在
|
||||||
|
"""
|
||||||
|
result = run(["gh", "release", "view", tag, "--json", "tagName"], check=False)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
def build_release_notes(
|
||||||
|
tag: str,
|
||||||
|
download_link: str,
|
||||||
|
remote_md5: str,
|
||||||
|
sha256_sum: str,
|
||||||
|
remote_size: str,
|
||||||
|
remote_last_modified: str,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
构建 Release 发布信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tag (str): 构建版本号
|
||||||
|
download_link (str): 下载链接
|
||||||
|
remote_md5 (str): 远端md5的值
|
||||||
|
sha256_sum (str): sha256的值
|
||||||
|
remote_size (str): 文件大小
|
||||||
|
remote_last_modified (str): 最后修改时间
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 最终的发布信息
|
||||||
|
"""
|
||||||
|
lines = [
|
||||||
|
"WeChat for Mac automatic release",
|
||||||
|
"",
|
||||||
|
"Download and integrity details are below.",
|
||||||
|
"",
|
||||||
|
"Release details",
|
||||||
|
f"- DestVersion: {tag}",
|
||||||
|
"",
|
||||||
|
"Source and checksums",
|
||||||
|
f"- DownloadFrom: {download_link}",
|
||||||
|
f"- Md5: {remote_md5}",
|
||||||
|
f"- Sha256: {sha256_sum}",
|
||||||
|
]
|
||||||
|
if remote_size:
|
||||||
|
lines.append(f"- ContentLength: {remote_size}")
|
||||||
|
if remote_last_modified:
|
||||||
|
lines.append(f"- LastModified: {remote_last_modified}")
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
def write_sha_file(
|
||||||
|
sha_file: Path,
|
||||||
|
tag: str,
|
||||||
|
download_link: str,
|
||||||
|
sha256_sum: str,
|
||||||
|
remote_md5: str,
|
||||||
|
remote_size: str,
|
||||||
|
remote_last_modified: str,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
写入 SHA 文件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sha_file (Path): SHA 文件路径
|
||||||
|
tag (str): 构建标签
|
||||||
|
download_link (str): 下载链接
|
||||||
|
sha256_sum (str): sha256的值
|
||||||
|
remote_md5 (str): 远端md5的值
|
||||||
|
remote_size (str): 文件大小
|
||||||
|
remote_last_modified (str): 最后修改时间
|
||||||
|
"""
|
||||||
|
timestamp = datetime.datetime.now(datetime.timezone.utc).strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
lines = [
|
||||||
|
f"DestVersion: {tag}",
|
||||||
|
f"Md5: {remote_md5}",
|
||||||
|
f"Sha256: {sha256_sum}",
|
||||||
|
]
|
||||||
|
if remote_size:
|
||||||
|
lines.append(f"ContentLength: {remote_size}")
|
||||||
|
if remote_last_modified:
|
||||||
|
lines.append(f"LastModified: {remote_last_modified}")
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f"UpdateTime: {timestamp} (UTC)",
|
||||||
|
f"DownloadFrom: {download_link}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
sha_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
BASE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
mount_dir = ""
|
||||||
|
try:
|
||||||
|
force_release = os.environ.get("FORCE_RELEASE", "").strip().lower() in {
|
||||||
|
"1",
|
||||||
|
"true",
|
||||||
|
"yes",
|
||||||
|
"on",
|
||||||
|
}
|
||||||
|
log(f"Force release: {'true' if force_release else 'false'}")
|
||||||
|
|
||||||
|
# Step 1: resolve download link from website.
|
||||||
|
log("Resolving download link from website...")
|
||||||
|
download_link = fetch_download_link()
|
||||||
|
log(f"Download link: {download_link}")
|
||||||
|
|
||||||
|
# Step 2: read metadata from HEAD response.
|
||||||
|
log("Fetching HEAD metadata...")
|
||||||
|
headers = fetch_head_metadata(download_link)
|
||||||
|
remote_md5 = headers.get("x-cos-meta-md5", "")
|
||||||
|
remote_size = headers.get("content-length", "")
|
||||||
|
remote_last_modified = headers.get("last-modified", "")
|
||||||
|
log(
|
||||||
|
"HEAD metadata: "
|
||||||
|
f"md5={remote_md5 or 'n/a'}, "
|
||||||
|
f"size={remote_size or 'n/a'}, "
|
||||||
|
f"last_modified={remote_last_modified or 'n/a'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 3: compare with latest release by MD5 to avoid downloads.
|
||||||
|
log("Fetching latest GitHub release info...")
|
||||||
|
latest_info = get_latest_release_info()
|
||||||
|
latest_md5 = latest_info.get("Md5", "")
|
||||||
|
latest_sha256 = latest_info.get("Sha256", "")
|
||||||
|
log(
|
||||||
|
"Latest release: "
|
||||||
|
f"md5={latest_md5 or 'n/a'}, "
|
||||||
|
f"sha256={latest_sha256 or 'n/a'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if remote_md5 and latest_md5 and remote_md5 == latest_md5:
|
||||||
|
if force_release:
|
||||||
|
log("MD5 matches latest release, but force release is enabled.")
|
||||||
|
else:
|
||||||
|
log("No new version detected by MD5. Skipping download.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Step 4: download DMG with retry.
|
||||||
|
log("Downloading DMG...")
|
||||||
|
dmg_path = TEMP_DIR / "WeChatMac.dmg"
|
||||||
|
download_with_retry(download_link, dmg_path)
|
||||||
|
log(f"Downloaded DMG to {dmg_path}")
|
||||||
|
|
||||||
|
# Step 5: mount DMG and read plist values.
|
||||||
|
log("Mounting DMG and reading Info.plist...")
|
||||||
|
mount_dir = mount_dmg(dmg_path)
|
||||||
|
tag = get_tag_from_plist(mount_dir)
|
||||||
|
detach_dmg(mount_dir)
|
||||||
|
mount_dir = ""
|
||||||
|
log(f"Detected tag: {tag}")
|
||||||
|
|
||||||
|
# Step 6: prepare release assets in workspace.
|
||||||
|
log("Preparing release assets...")
|
||||||
|
version_dir = BASE_DIR / tag
|
||||||
|
version_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
final_dmg = version_dir / f"WeChatMac-{tag}.dmg"
|
||||||
|
shutil.copy2(dmg_path, final_dmg)
|
||||||
|
|
||||||
|
sha256_sum = compute_sha256(final_dmg)
|
||||||
|
log(f"Computed SHA256: {sha256_sum}")
|
||||||
|
sha_file = version_dir / f"WeChatMac-{tag}.dmg.sha256"
|
||||||
|
write_sha_file(
|
||||||
|
sha_file,
|
||||||
|
tag,
|
||||||
|
download_link,
|
||||||
|
sha256_sum,
|
||||||
|
remote_md5,
|
||||||
|
remote_size,
|
||||||
|
remote_last_modified,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not latest_md5 and latest_sha256 and sha256_sum == latest_sha256:
|
||||||
|
if force_release:
|
||||||
|
log("SHA256 matches latest release, but force release is enabled.")
|
||||||
|
else:
|
||||||
|
log("No new version detected by SHA256. Skipping release.")
|
||||||
|
return 0
|
||||||
|
if not latest_md5:
|
||||||
|
log("Latest release has no MD5, used SHA256 fallback check.")
|
||||||
|
|
||||||
|
if tag_exists(tag):
|
||||||
|
suffix = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d")
|
||||||
|
tag = f"{tag}_{suffix}"
|
||||||
|
log(f"Release tag: {tag}")
|
||||||
|
|
||||||
|
title = f"Wechat For Mac {tag}"
|
||||||
|
notes_content = build_release_notes(
|
||||||
|
tag,
|
||||||
|
download_link,
|
||||||
|
remote_md5,
|
||||||
|
sha256_sum,
|
||||||
|
remote_size,
|
||||||
|
remote_last_modified,
|
||||||
|
)
|
||||||
|
notes_file = TEMP_DIR / "release_notes.txt"
|
||||||
|
notes_file.write_text(notes_content, encoding="utf-8")
|
||||||
|
log(f"Release notes written to {notes_file}")
|
||||||
|
|
||||||
|
# Step 7: publish release with assets and notes.
|
||||||
|
log("Creating GitHub release...")
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"gh",
|
||||||
|
"release",
|
||||||
|
"create",
|
||||||
|
tag,
|
||||||
|
str(final_dmg),
|
||||||
|
str(sha_file),
|
||||||
|
"-F",
|
||||||
|
str(notes_file),
|
||||||
|
"-t",
|
||||||
|
title,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
log("GitHub release created.")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
# Always detach and cleanup to keep workspace tidy.
|
||||||
|
if mount_dir:
|
||||||
|
detach_dmg(mount_dir)
|
||||||
|
shutil.rmtree(BASE_DIR, ignore_errors=True)
|
||||||
|
log("Cleanup completed.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Error: {exc}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -eo pipefail
|
|
||||||
|
|
||||||
# ====================================================
|
|
||||||
# 配置变量
|
|
||||||
# ====================================================
|
|
||||||
TEMP_PATH="WeChatMac/temp"
|
|
||||||
WEBSITE_URL="https://mac.weixin.qq.com/?t=mac&lang=zh_CN"
|
|
||||||
DOWNLOAD_LINK=""
|
|
||||||
|
|
||||||
# ====================================================
|
|
||||||
# 函数定义
|
|
||||||
# ====================================================
|
|
||||||
|
|
||||||
# 打印分隔线
|
|
||||||
print_separator() {
|
|
||||||
printf '%*s\n' 60 | tr ' ' '#'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 彩色输出函数
|
|
||||||
echo_color() {
|
|
||||||
local color="$1"
|
|
||||||
shift
|
|
||||||
local message="$*"
|
|
||||||
case "$color" in
|
|
||||||
yellow)
|
|
||||||
echo -e "\033[1;33m$message\033[0m"
|
|
||||||
;;
|
|
||||||
red)
|
|
||||||
echo -e "\033[1;31m$message\033[0m" >&2
|
|
||||||
;;
|
|
||||||
green)
|
|
||||||
echo -e "\033[1;32m$message\033[0m"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "$message"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# 安装依赖项
|
|
||||||
install_depends() {
|
|
||||||
print_separator
|
|
||||||
echo_color "yellow" "Installing dependencies: wget, curl, git, gh, shasum, pup"
|
|
||||||
print_separator
|
|
||||||
|
|
||||||
brew install wget curl git gh pup
|
|
||||||
}
|
|
||||||
|
|
||||||
# 下载 WeChat DMG
|
|
||||||
download_wechat() {
|
|
||||||
DOWNLOAD_LINK=$(curl -s "$WEBSITE_URL" | pup 'a.download-button:nth-of-type(1) attr{href}')
|
|
||||||
|
|
||||||
print_separator
|
|
||||||
echo_color "yellow" "Downloading the newest WeChatMac..."
|
|
||||||
print_separator
|
|
||||||
|
|
||||||
mkdir -p "$TEMP_PATH"
|
|
||||||
|
|
||||||
wget -q "$DOWNLOAD_LINK" -O "${TEMP_PATH}/WeChatMac.dmg"
|
|
||||||
if [ "$?" -ne 0 ]; then
|
|
||||||
echo_color "red" "Download Failed, please check your network!"
|
|
||||||
clean_data 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 从 Info.plist 提取版本信息
|
|
||||||
get_version() {
|
|
||||||
print_separator
|
|
||||||
echo_color "yellow" "Extracting version from DMG (macOS)..."
|
|
||||||
print_separator
|
|
||||||
|
|
||||||
# 挂载 dmg
|
|
||||||
MOUNT_DIR=$(hdiutil attach "${TEMP_PATH}/WeChatMac.dmg" -nobrowse | sed -n 's/^.*\(\/Volumes\/.*\)$/\1/p' | tail -n1)
|
|
||||||
|
|
||||||
if [ -z "$MOUNT_DIR" ]; then
|
|
||||||
echo_color "red" "Failed to mount DMG!"
|
|
||||||
clean_data 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 定位 Info.plist
|
|
||||||
# INFO_PLIST=$(find "${MOUNT_DIR}" -type f -name "Info.plist" | head -n 1)
|
|
||||||
INFO_PLIST="${MOUNT_DIR}/WeChat.app/Contents/Info.plist"
|
|
||||||
|
|
||||||
if [ ! -f "$INFO_PLIST" ]; then
|
|
||||||
echo_color "red" "Info.plist not found in mounted volume!"
|
|
||||||
hdiutil detach "$MOUNT_DIR"
|
|
||||||
clean_data 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 使用 grep 和 sed 提取版本号
|
|
||||||
VERSION=$(grep -A1 '<key>CFBundleShortVersionString</key>' "$INFO_PLIST" | grep '<string>' | sed -E 's/.*<string>([^<]+)<\/string>.*/\1/')
|
|
||||||
|
|
||||||
# 使用 grep 和 sed 提取构建版本号
|
|
||||||
BUILD_VERSION=$(grep -A1 '<key>CFBundleVersion</key>' "$INFO_PLIST" | grep '<string>' | sed -E 's/.*<string>([^<]+)<\/string>.*/\1/')
|
|
||||||
|
|
||||||
# 卸载 dmg
|
|
||||||
hdiutil detach "$MOUNT_DIR"
|
|
||||||
|
|
||||||
if [ -z "$VERSION" ]; then
|
|
||||||
echo_color "red" "Version information not found in Info.plist!"
|
|
||||||
clean_data 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Version: $VERSION"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 计算 SHA256
|
|
||||||
compute_sha256() {
|
|
||||||
local file_path="$1"
|
|
||||||
shasum -a 256 "$file_path" | awk '{print $1}'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 准备提交(复制 DMG 并创建 .sha256 文件)
|
|
||||||
prepare_commit() {
|
|
||||||
print_separator
|
|
||||||
echo_color "yellow" "Preparing to commit new version..."
|
|
||||||
print_separator
|
|
||||||
|
|
||||||
VERSION_DIR="WeChatMac/$VERSION"
|
|
||||||
mkdir -p "$VERSION_DIR"
|
|
||||||
|
|
||||||
cp "${TEMP_PATH}/WeChatMac.dmg" "$VERSION_DIR/WeChatMac-$VERSION.dmg"
|
|
||||||
|
|
||||||
NOW_SUM256=$(compute_sha256 "$VERSION_DIR/WeChatMac-$VERSION.dmg")
|
|
||||||
|
|
||||||
cat > "$VERSION_DIR/WeChatMac-$VERSION.dmg.sha256" <<EOF
|
|
||||||
DestVersion: $VERSION
|
|
||||||
DestBuild: $BUILD_VERSION
|
|
||||||
Sha256: $NOW_SUM256
|
|
||||||
UpdateTime: $(date -u '+%Y-%m-%d %H:%M:%S') (UTC)
|
|
||||||
DownloadFrom: $DOWNLOAD_LINK
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "SHA256: $NOW_SUM256"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 获取最新的 GitHub Release 信息
|
|
||||||
get_latest_release_info() {
|
|
||||||
print_separator
|
|
||||||
echo_color "yellow" "Getting latest GitHub release info..."
|
|
||||||
print_separator
|
|
||||||
|
|
||||||
LATEST_BODY=$(gh release view --json body --jq ".body" || true)
|
|
||||||
|
|
||||||
if [ -z "$LATEST_BODY" ]; then
|
|
||||||
LATEST_SUM256=""
|
|
||||||
LATEST_VERSION=""
|
|
||||||
else
|
|
||||||
LATEST_SUM256=$(echo "$LATEST_BODY" | grep 'Sha256:' | awk -F': ' '{print $2}')
|
|
||||||
LATEST_VERSION=$(echo "$LATEST_BODY" | grep 'DestVersion:' | awk -F': ' '{print $2}')
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Latest Version: $LATEST_VERSION"
|
|
||||||
echo "Latest SHA256: $LATEST_SUM256"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建新的 GitHub Release
|
|
||||||
create_release() {
|
|
||||||
print_separator
|
|
||||||
echo_color "yellow" "Creating new GitHub release..."
|
|
||||||
print_separator
|
|
||||||
|
|
||||||
if [ "$VERSION" = "$LATEST_VERSION" ]; then
|
|
||||||
VERSION_TAG="${VERSION}_$(date -u '+%Y%m%d')"
|
|
||||||
else
|
|
||||||
VERSION_TAG="$VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
gh release create "v$VERSION_TAG" "WeChatMac/$VERSION/WeChatMac-$VERSION.dmg" -F "WeChatMac/$VERSION/WeChatMac-$VERSION.dmg.sha256" -t "Wechat For Mac v$VERSION_TAG"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 清理临时数据并退出
|
|
||||||
clean_data() {
|
|
||||||
print_separator
|
|
||||||
echo_color "yellow" "Cleaning runtime and exiting..."
|
|
||||||
print_separator
|
|
||||||
|
|
||||||
rm -rf "WeChatMac"
|
|
||||||
exit "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ====================================================
|
|
||||||
# 主流程
|
|
||||||
# ====================================================
|
|
||||||
main() {
|
|
||||||
# 创建临时目录
|
|
||||||
mkdir -p "$TEMP_PATH"
|
|
||||||
|
|
||||||
# 安装依赖项
|
|
||||||
install_depends
|
|
||||||
|
|
||||||
# 下载 WeChat DMG
|
|
||||||
download_wechat
|
|
||||||
|
|
||||||
# 提取版本信息
|
|
||||||
get_version
|
|
||||||
|
|
||||||
# 准备提交(复制 DMG 并创建 .sha256 文件)
|
|
||||||
prepare_commit
|
|
||||||
|
|
||||||
# 获取最新的 GitHub Release 信息
|
|
||||||
get_latest_release_info
|
|
||||||
|
|
||||||
# 比较 SHA256 值
|
|
||||||
if [ "$NOW_SUM256" = "$LATEST_SUM256" ] && [ -n "$LATEST_SUM256" ]; then
|
|
||||||
echo_color "green" "This is the newest Version!"
|
|
||||||
clean_data 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 创建新的 GitHub Release
|
|
||||||
create_release
|
|
||||||
|
|
||||||
# 清理临时数据并退出
|
|
||||||
clean_data 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 执行主流程
|
|
||||||
main
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -eo pipefail
|
|
||||||
|
|
||||||
|
|
||||||
if [ -z $GHTOKEN ]; then
|
|
||||||
>&2 echo -e "\033[1;31mMissing Github Token(GHTOKEN)! Please get a BotToken from 'Github Settings->Developer settings->Personal access tokens' and set it in Repo Secrect\033[0m"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z $BOTTOKEN ]; then
|
|
||||||
>&2 echo -e "\033[1;31mMissing Bot Token(BOTTOKEN)! Please get a BotToken from @Botfather on Telegram and set it in Repo Secrect\033[0m"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z $CHATIDS ]; then
|
|
||||||
>&2 echo -e "\033[1;31mMissing ChatIds(CHATIDS)! Please get ChatId from @GroupIDbot on Telegram Chats(Muti chatids split with comma ',') and set it in Repo Environment Values\033[0m"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
function login_gh() {
|
|
||||||
printf "#%.0s" {1..60}
|
|
||||||
echo
|
|
||||||
echo -e "## \033[1;33mLogin to github to use github-cli...\033[0m"
|
|
||||||
printf "#%.0s" {1..60}
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo $GHTOKEN > WeChatSetup/temp/GHTOKEN
|
|
||||||
gh auth login --with-token < WeChatSetup/temp/GHTOKEN
|
|
||||||
if [ "$?" -ne 0 ]; then
|
|
||||||
>&2 echo -e "\033[1;31mLogin Failed, please check your network or token!\033[0m"
|
|
||||||
clean_data 1
|
|
||||||
fi
|
|
||||||
rm -rfv WeChatSetup/temp/GHTOKEN
|
|
||||||
}
|
|
||||||
|
|
||||||
### https://kodango.com/sed-and-awk-notes-part-5
|
|
||||||
## start=${1:-""} means as follows in general
|
|
||||||
## if ($1) then
|
|
||||||
## start=$1
|
|
||||||
## else
|
|
||||||
## start=""
|
|
||||||
## end
|
|
||||||
function join_lines() {
|
|
||||||
local delim=${1:-,}
|
|
||||||
sed 'H;$!d;${x;s/^\n//;s/\n/\'$delim'/g}'
|
|
||||||
}
|
|
||||||
|
|
||||||
function clean_data() {
|
|
||||||
printf "#%.0s" {1..60}
|
|
||||||
echo
|
|
||||||
echo -e "## \033[1;33mClean runtime and exit...\033[0m"
|
|
||||||
printf "#%.0s" {1..60}
|
|
||||||
echo
|
|
||||||
|
|
||||||
rm -rfv WeChatSetup/*
|
|
||||||
exit $1
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
temp_path="WeChatSetup/temp"
|
|
||||||
mkdir -p ${temp_path}
|
|
||||||
|
|
||||||
login_gh
|
|
||||||
|
|
||||||
gh release view --json body --jq ".body" > ${temp_path}/release.info
|
|
||||||
|
|
||||||
release_info=`awk '!/^$|Sha256/ { $1="*"$1"*";sub("UpdateTime", "CheckTime"); if ( match($2, /https?:\/\/([\w\.\/:])*/) ) $2="[Url]("$2")"; print $0 }' ${temp_path}/release.info | join_lines '%0A' | sed 's/ /%20/g'`
|
|
||||||
dest_version=`awk '/DestVersion/ { print $2 }' ${temp_path}/release.info`
|
|
||||||
release_info="$release_info%0A%0A*NotifyFrom:*%20[Github](https://github.com/tom-snow/wechat-windows-versions/releases/tag/v$dest_version)"
|
|
||||||
|
|
||||||
echo $CHATIDS | sed 's/,/\n/g' > ${temp_path}/chat_ids
|
|
||||||
# while IFS="" read -r chatid || [ -n "$chatid" ]
|
|
||||||
while IFS="" read -r chatid
|
|
||||||
do
|
|
||||||
api_link="https://api.telegram.org/bot$BOTTOKEN/sendMessage?chat_id=$chatid&text=*New%20WeChat%20Windows%20Version!!*%0A%0A$release_info&parse_mode=Markdown&disable_web_page_preview=true"
|
|
||||||
curl -s -o /dev/null $api_link
|
|
||||||
done < ${temp_path}/chat_ids
|
|
||||||
|
|
||||||
gh auth logout --hostname github.com | echo "y"
|
|
||||||
clean_data 0
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
Reference in New Issue
Block a user