ww-manager 2.3.2__tar.gz → 2.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {ww_manager-2.3.2 → ww_manager-2.3.4}/.github/generate-pkgbuild.sh +9 -4
  2. {ww_manager-2.3.2 → ww_manager-2.3.4}/.github/workflows/aur-publish.yml +2 -1
  3. ww_manager-2.3.4/.github/workflows/aur-republish.yml +38 -0
  4. {ww_manager-2.3.2 → ww_manager-2.3.4}/.github/workflows/ci.yml +3 -1
  5. {ww_manager-2.3.2 → ww_manager-2.3.4}/PKG-INFO +3 -3
  6. {ww_manager-2.3.2 → ww_manager-2.3.4}/README.md +2 -2
  7. {ww_manager-2.3.2 → ww_manager-2.3.4}/docs/README.en.md +2 -2
  8. {ww_manager-2.3.2 → ww_manager-2.3.4}/docs/linux.md +31 -3
  9. {ww_manager-2.3.2 → ww_manager-2.3.4}/pyproject.toml +1 -1
  10. {ww_manager-2.3.2 → ww_manager-2.3.4}/scripts/demo_cli_menus.py +1 -1
  11. {ww_manager-2.3.2 → ww_manager-2.3.4}/scripts/demo_resource_menu.py +1 -1
  12. ww_manager-2.3.4/scripts/extract-icon.py +146 -0
  13. ww_manager-2.3.4/scripts/setup-linux.sh +986 -0
  14. {ww_manager-2.3.2 → ww_manager-2.3.4}/src/ww_manager/cli.py +2 -2
  15. {ww_manager-2.3.2 → ww_manager-2.3.4}/src/ww_manager/core.py +12 -5
  16. {ww_manager-2.3.2 → ww_manager-2.3.4}/src/ww_manager/incremental.py +27 -12
  17. {ww_manager-2.3.2 → ww_manager-2.3.4}/src/ww_manager/resource_updates.py +68 -5
  18. {ww_manager-2.3.2 → ww_manager-2.3.4}/tests/test_demo_cli_menus.py +1 -1
  19. {ww_manager-2.3.2 → ww_manager-2.3.4}/tests/test_menu.py +3 -3
  20. {ww_manager-2.3.2 → ww_manager-2.3.4}/tests/test_resource_tier.py +15 -1
  21. {ww_manager-2.3.2 → ww_manager-2.3.4}/tests/test_resource_updates.py +122 -0
  22. {ww_manager-2.3.2 → ww_manager-2.3.4}/uv.lock +2 -2
  23. ww_manager-2.3.4/vendor/riched20-utf8-patch/SOURCE.json +12 -0
  24. ww_manager-2.3.4/vendor/riched20-utf8-patch/riched20-utf8-rtf.patch +90 -0
  25. {ww_manager-2.3.2 → ww_manager-2.3.4}/.github/workflows/publish.yml +0 -0
  26. {ww_manager-2.3.2 → ww_manager-2.3.4}/.github/workflows/release-hpatchz.yml +0 -0
  27. {ww_manager-2.3.2 → ww_manager-2.3.4}/.gitignore +0 -0
  28. {ww_manager-2.3.2 → ww_manager-2.3.4}/.pre-commit-config.yaml +0 -0
  29. {ww_manager-2.3.2 → ww_manager-2.3.4}/LICENSE +0 -0
  30. {ww_manager-2.3.2 → ww_manager-2.3.4}/Makefile +0 -0
  31. {ww_manager-2.3.2 → ww_manager-2.3.4}/docs/images/cli-preview.webp +0 -0
  32. {ww_manager-2.3.2 → ww_manager-2.3.4}/docs/images/download-preview.webp +0 -0
  33. {ww_manager-2.3.2 → ww_manager-2.3.4}/docs/wuwa-launcher.md +0 -0
  34. {ww_manager-2.3.2 → ww_manager-2.3.4}/run_ww.sh +0 -0
  35. {ww_manager-2.3.2 → ww_manager-2.3.4}/scripts/demo_disk_space_errors.py +0 -0
  36. {ww_manager-2.3.2 → ww_manager-2.3.4}/scripts/sync_version.py +0 -0
  37. {ww_manager-2.3.2 → ww_manager-2.3.4}/src/ww_manager/config.py +0 -0
  38. {ww_manager-2.3.2 → ww_manager-2.3.4}/src/ww_manager/download.py +0 -0
  39. {ww_manager-2.3.2 → ww_manager-2.3.4}/src/ww_manager/menu.py +0 -0
  40. {ww_manager-2.3.2 → ww_manager-2.3.4}/tests/test_cli_help.py +0 -0
  41. {ww_manager-2.3.2 → ww_manager-2.3.4}/tests/test_cli_menus.py +0 -0
  42. {ww_manager-2.3.2 → ww_manager-2.3.4}/tests/test_demo_disk_space_errors.py +0 -0
@@ -10,11 +10,15 @@ URL="https://github.com/$REPO/archive/refs/tags/v$VERSION.tar.gz"
10
10
  curl --fail --location "$URL" -o source.tar.gz
11
11
  SHA256=$(sha256sum source.tar.gz | cut -d ' ' -f 1)
12
12
 
13
+ # pkgrel:新上游版本发 tag 时固定为 1;纯打包修正(版本号不变)由
14
+ # aur-republish 工作流传入 2、3…,让已装旧 pkgrel 的用户能被 pacman 升级
15
+ PKGREL="${PKGREL:-1}"
16
+
13
17
  # 生成 PKGBUILD
14
18
  cat <<EOF >PKGBUILD
15
19
  pkgname=ww-manager
16
20
  pkgver=$VERSION
17
- pkgrel=1
21
+ pkgrel=$PKGREL
18
22
  pkgdesc="ww-manager (A Wuthering Waves CLI Manager)"
19
23
  arch=('any')
20
24
  url="https://github.com/$REPO"
@@ -22,10 +26,11 @@ license=('MIT')
22
26
  depends=(
23
27
  'python'
24
28
  'python-typer'
25
- 'python-rich'
26
- 'python-requests'
27
- 'python-certifi'
29
+ 'python-rich'
30
+ 'python-requests'
31
+ 'python-certifi'
28
32
  'python-typing_extensions'
33
+ 'python-prompt_toolkit'
29
34
  )
30
35
  makedepends=('python-build' 'python-installer' 'python-hatchling')
31
36
  source=("\$pkgname-\$pkgver.tar.gz::$URL")
@@ -1,10 +1,11 @@
1
1
  name: Publish to AUR
2
2
 
3
+ # 发新版本 tag 时自动发布,pkgrel 固定为 1。
4
+ # 版本号不变的纯打包修正请用 aur-republish.yml(手动触发,pkgrel 递增)。
3
5
  on:
4
6
  push:
5
7
  tags:
6
8
  - 'v*'
7
- workflow_dispatch:
8
9
 
9
10
  jobs:
10
11
  aur-publish:
@@ -0,0 +1,38 @@
1
+ name: Republish to AUR (pkgrel bump)
2
+
3
+ # 仅手动触发:版本号不变、只改打包内容(依赖、安装路径、启动参数等)时用。
4
+ # pkgrel 递增后 AUR 上的版本从 X.Y.Z-1 变成 X.Y.Z-2,已安装旧包的用户
5
+ # 才能通过 pacman/yay 升级拿到修正。
6
+ on:
7
+ workflow_dispatch:
8
+ inputs:
9
+ pkgrel:
10
+ description: 'pkgrel(纯打包修正请递增,例如 2、3…)'
11
+ required: true
12
+ default: '2'
13
+ reason:
14
+ description: '本次重新打包的原因(写入 commit message)'
15
+ required: false
16
+ default: ''
17
+
18
+ jobs:
19
+ aur-republish:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - name: Generate PKGBUILD
25
+ env:
26
+ PKGREL: ${{ inputs.pkgrel }}
27
+ run: bash .github/generate-pkgbuild.sh
28
+
29
+ - name: Publish AUR package
30
+ uses: KSXGitHub/github-actions-deploy-aur@master
31
+ with:
32
+ pkgname: ww-manager
33
+ pkgbuild: ./PKGBUILD
34
+ commit_username: ${{ github.repository_owner }}
35
+ commit_email: ${{ github.repository_owner }}@users.noreply.github.com
36
+ ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
37
+ commit_message: "Repackage ${{ github.ref_name }}: pkgrel ${{ inputs.pkgrel }}${{ inputs.reason && format(' - {0}', inputs.reason) || '' }}"
38
+ ssh_keyscan_types: rsa,ecdsa,ed25519
@@ -19,7 +19,9 @@ jobs:
19
19
  - name: Set up Python
20
20
  uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
21
21
  with:
22
- python-version: "3.9"
22
+ # ubuntu-latest 已切到 26.04,其预构建版本里没有 3.9;
23
+ # 项目仍声明 >=3.9,这里只需一个能装上的版本跑检查。
24
+ python-version: "3.13"
23
25
 
24
26
  - name: Install dependencies
25
27
  run: pip install ruff -e .
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ww-manager
3
- Version: 2.3.2
3
+ Version: 2.3.4
4
4
  Summary: 鸣潮命令行管理器
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.9
@@ -15,7 +15,7 @@ Description-Content-Type: text/markdown
15
15
  <div align="center">
16
16
  <h1>WutheringWaves CLI Manager</h1>
17
17
  <h3>鸣潮命令行管理器</h3>
18
- <a href="https://python.org"><img src="https://img.shields.io/badge/Python-3.9%2B-blue.svg" alt="Python 3.9+"></a>&nbsp;<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/badge/Tool-uv-purple.svg" alt="uv"></a>&nbsp;<img src="https://img.shields.io/badge/Version-2.3.2-brightgreen.svg" alt="version 2.3.2">
18
+ <a href="https://python.org"><img src="https://img.shields.io/badge/Python-3.9%2B-blue.svg" alt="Python 3.9+"></a>&nbsp;<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/badge/Tool-uv-purple.svg" alt="uv"></a>&nbsp;<img src="https://img.shields.io/badge/Version-2.3.4-brightgreen.svg" alt="version 2.3.4">
19
19
  </div>
20
20
 
21
21
  <p align="center">
@@ -117,7 +117,7 @@ ww status
117
117
 
118
118
  | 档位 | 启动参数 |
119
119
  |---|---|
120
- | SD · 标清 | `-krqlv=sd` |
120
+ | SD · 流畅 | `-krqlv=sd` |
121
121
  | HD · 高清 | `-krqlv=hd` |
122
122
  | UHD · 极致 | `-krqlv=uhd` |
123
123
 
@@ -1,7 +1,7 @@
1
1
  <div align="center">
2
2
  <h1>WutheringWaves CLI Manager</h1>
3
3
  <h3>鸣潮命令行管理器</h3>
4
- <a href="https://python.org"><img src="https://img.shields.io/badge/Python-3.9%2B-blue.svg" alt="Python 3.9+"></a>&nbsp;<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/badge/Tool-uv-purple.svg" alt="uv"></a>&nbsp;<img src="https://img.shields.io/badge/Version-2.3.2-brightgreen.svg" alt="version 2.3.2">
4
+ <a href="https://python.org"><img src="https://img.shields.io/badge/Python-3.9%2B-blue.svg" alt="Python 3.9+"></a>&nbsp;<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/badge/Tool-uv-purple.svg" alt="uv"></a>&nbsp;<img src="https://img.shields.io/badge/Version-2.3.4-brightgreen.svg" alt="version 2.3.4">
5
5
  </div>
6
6
 
7
7
  <p align="center">
@@ -103,7 +103,7 @@ ww status
103
103
 
104
104
  | 档位 | 启动参数 |
105
105
  |---|---|
106
- | SD · 标清 | `-krqlv=sd` |
106
+ | SD · 流畅 | `-krqlv=sd` |
107
107
  | HD · 高清 | `-krqlv=hd` |
108
108
  | UHD · 极致 | `-krqlv=uhd` |
109
109
 
@@ -1,7 +1,7 @@
1
1
  <div align="center">
2
2
  <h1>WutheringWaves CLI Manager</h1>
3
3
  <h3>Command-line manager for Wuthering Waves</h3>
4
- <a href="https://python.org"><img src="https://img.shields.io/badge/Python-3.9%2B-blue.svg" alt="Python 3.9+"></a>&nbsp;<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/badge/Tool-uv-purple.svg" alt="uv"></a>&nbsp;<img src="https://img.shields.io/badge/Version-2.3.2-brightgreen.svg" alt="version 2.3.2">
4
+ <a href="https://python.org"><img src="https://img.shields.io/badge/Python-3.9%2B-blue.svg" alt="Python 3.9+"></a>&nbsp;<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/badge/Tool-uv-purple.svg" alt="uv"></a>&nbsp;<img src="https://img.shields.io/badge/Version-2.3.4-brightgreen.svg" alt="version 2.3.4">
5
5
  </div>
6
6
 
7
7
  <p align="center">
@@ -103,7 +103,7 @@ Pass the launch argument for an installed quality tier:
103
103
 
104
104
  | Tier | Launch argument |
105
105
  |---|---|
106
- | SD · Standard | `-krqlv=sd` |
106
+ | SD · Smooth | `-krqlv=sd` |
107
107
  | HD · High | `-krqlv=hd` |
108
108
  | UHD · Ultra | `-krqlv=uhd` |
109
109
 
@@ -10,11 +10,11 @@
10
10
 
11
11
  | 档位 | 参数 |
12
12
  |---|---|
13
- | SD · 标清 | `-krqlv=sd` |
13
+ | SD · 流畅 | `-krqlv=sd` |
14
14
  | HD · 高清 | `-krqlv=hd` |
15
15
  | UHD · 极致 | `-krqlv=uhd` |
16
16
 
17
- 只指定一个档位。可以用 `ww status` 查看工具当前默认档位,但工具不会自动修改 Steam 或其他启动器中的启动参数。
17
+ 只指定一个档位。可以用 `ww status` 查看工具当前默认档位,但工具不会自动修改 Steam 或其他启动器中的启动参数。下面是一些启动游戏的参考:
18
18
 
19
19
  ## Steam + Proton
20
20
 
@@ -29,7 +29,35 @@
29
29
  -krqlv=hd
30
30
  ```
31
31
 
32
- 也可以使用 Lutris,或参考仓库中的 `run_ww.sh` 自行配置启动方式,并保持档位参数一致。
32
+ 也可以使用 Lutris,或直接调用 DWProton 自带 Wine(见下节)。
33
+
34
+ ## 一键安装(DWProton + 独立 prefix)
35
+
36
+ 不想经过 Steam 或 Lutris 时,用仓库里的一键部署环境脚本(实验性,仅在 Archlinux + niri 图形环境下测试可用):
37
+
38
+ ```bash
39
+ // 不带参数时默认读 ww-cli 的安装目录
40
+ scripts/setup-linux.sh --game-dir ~/Games/WutheringWaves
41
+ ```
42
+
43
+ 脚本依次完成:下载并校验 DWProton、准备 riched20 UTF-8 补丁组件(修复登录协议正文乱码)、建立独立 prefix(DXVK、vkd3d-proton、dxvk-nvapi、NVIDIA 的 NGX 运行时、ICU、字体、DLL 覆盖),初始化后逐项自检并补装缺项,最后把启动器装到 `~/.local/bin/run_ww`,并写入桌面启动器入口 `ww.desktop`(图标从游戏自带启动器里提取)。
44
+
45
+ 常用开关:
46
+
47
+ | 开关 | 作用 |
48
+ |---|---|
49
+ | `-y` | 不询问,缺失的系统包直接装 |
50
+ | `--no-install` | 不装任何系统包,只提示 |
51
+ | `--check` | 只检查路径与档位,不启动游戏 |
52
+ | `--rebuild` | 忽略仓库内的预编译组件,从源码重编 riched20 |
53
+
54
+ 安装时检查 gamemode(启动时默认启用,实测对帧率有提升)与 PE 交叉工具链(仅重编组件时需要,mingw-w64 或 clang+lld 任一即可),缺失则询问是否安装;选择跳过则不打补丁,登录协议等文本可能乱码,但游戏仍可正常启动。MangoHud 属可选项,默认不装。脚本支持断点续传,并会复用其它安装根里已下载的归档;下载长期失败时可设 `https_proxy` 重试,或用 `WW_WINE_SOURCE_URL`、`WW_DWPROTON_URL` 换镜像源。
55
+
56
+ 已存在的 DWProton(`$DWPROTON`、`~/.local/share/dwproton/`、Lutris/Steam 目录等)和已有 prefix 会被直接复用,不重复下载也不移动。
57
+
58
+ 安装后用 `run_ww` 启动(或点桌面图标),默认经 gamemoderun 启动(`WW_GAMEMODE=0` 关闭)。脚本的画质档位自动读取游戏目录的 `wwm_resource_state.json`,`run_ww --check` 只检查路径和档位。日志在 `~/.local/share/ww-linux/logs/game.log`。
59
+
60
+ 已知问题:`bilibili` 服在登录的 webview 框会固定弹出**网络请求失败,请重试**,但是不影响验证码登录,忽略即可。
33
61
 
34
62
  ### 安装额外的 Proton 版本
35
63
  - [dwproton](https://dawn.wine/dawn-winery/dwproton/releases)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ww-manager"
3
- version = "2.3.2"
3
+ version = "2.3.4"
4
4
  description = "鸣潮命令行管理器"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -24,7 +24,7 @@ from ww_manager.download import format_bytes, space_shortage
24
24
 
25
25
  GIB = 1024**3
26
26
  SIZES = {"sd": 12 * GIB, "hd": 24 * GIB, "uhd": 48 * GIB}
27
- NAMES = {"sd": "标清", "hd": "高清", "uhd": "极致"}
27
+ NAMES = {"sd": "流畅", "hd": "高清", "uhd": "极致"}
28
28
  VERSION = "3.7.1"
29
29
 
30
30
 
@@ -14,7 +14,7 @@ from ww_manager.menu import MenuOption, build_selection_menu
14
14
  GIB = 1024**3
15
15
  # 所有体积均为展示交互而虚构,不代表真实客户端下载大小。
16
16
  DEMO_CHOICES = (
17
- ("sd", "标清", 12 * GIB, 12 * GIB),
17
+ ("sd", "流畅", 12 * GIB, 12 * GIB),
18
18
  ("hd", "高清", 24 * GIB, 0),
19
19
  ("uhd", "极致", 48 * GIB, 48 * GIB),
20
20
  )
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env python3
2
+ """从 Windows PE 可执行文件提取最大尺寸的图标。
3
+
4
+ 用法:extract-icon.py <exe> <输出目录> [尺寸...]
5
+
6
+ 把图标写成 <输出目录>/<尺寸>/apps/ww.png。没有 ImageMagick 时只写 256x256。
7
+ """
8
+
9
+ import os
10
+ import pathlib
11
+ import shutil
12
+ import struct
13
+ import subprocess
14
+ import sys
15
+
16
+
17
+ def u16(buf, off):
18
+ return struct.unpack_from("<H", buf, off)[0]
19
+
20
+
21
+ def u32(buf, off):
22
+ return struct.unpack_from("<I", buf, off)[0]
23
+
24
+
25
+ def read_pe_resources(path):
26
+ data = pathlib.Path(path).read_bytes()
27
+ pe = u32(data, 0x3C)
28
+ if data[pe : pe + 4] != b"PE\0\0":
29
+ raise SystemExit("不是 PE 文件")
30
+ coff = pe + 4
31
+ opt = coff + 20
32
+ ndd = opt + (96 if u16(data, opt) == 0x10B else 112)
33
+ rsrc_rva = u32(data, ndd + 16)
34
+
35
+ sections = []
36
+ for i in range(u16(data, coff + 2)):
37
+ base = opt + u16(data, coff + 16) + i * 40
38
+ vaddr = u32(data, base + 12)
39
+ vsize = max(u32(data, base + 8), u32(data, base + 16))
40
+ sections.append((vaddr, vsize, u32(data, base + 20)))
41
+
42
+ def rva2off(rva):
43
+ for vaddr, vsize, raw in sections:
44
+ if vaddr <= rva < vaddr + vsize:
45
+ return raw + rva - vaddr
46
+ raise SystemExit("资源段地址映射失败")
47
+
48
+ def entries(dir_off):
49
+ named, ids = u16(data, dir_off + 12), u16(data, dir_off + 14)
50
+ out = []
51
+ for i in range(named + ids):
52
+ e = dir_off + 16 + i * 8
53
+ out.append((u32(data, e), u32(data, e + 4)))
54
+ return out
55
+
56
+ rsrc = rva2off(rsrc_rva)
57
+
58
+ def leaf(off):
59
+ """目录下第一个语言条目的数据位置与长度。"""
60
+ _name, target = entries(off)[0]
61
+ pos = rsrc + (target & 0x7FFFFFFF)
62
+ return rva2off(u32(data, pos)), u32(data, pos + 4)
63
+
64
+ return data, rsrc, entries, leaf
65
+
66
+
67
+ def largest_icon(path):
68
+ data, rsrc, entries, leaf = read_pe_resources(path)
69
+ types = {name: i for i, (name, _t) in enumerate(entries(rsrc))}
70
+ if 3 not in types or 14 not in types:
71
+ raise SystemExit("文件里没有图标资源")
72
+
73
+ images = {}
74
+ _name, target = entries(rsrc)[types[3]]
75
+ for name, sub in entries(rsrc + (target & 0x7FFFFFFF)):
76
+ off, size = leaf(rsrc + (sub & 0x7FFFFFFF))
77
+ images[name] = data[off : off + size]
78
+
79
+ best, best_width = None, -1
80
+ _name, target = entries(rsrc)[types[14]]
81
+ for _gname, sub in entries(rsrc + (target & 0x7FFFFFFF)):
82
+ off, size = leaf(rsrc + (sub & 0x7FFFFFFF))
83
+ group = data[off : off + size]
84
+ count = u16(group, 4)
85
+ items = [struct.unpack_from("<BBBBHHIH", group, 6 + i * 14) for i in range(count)]
86
+ width = max(int(item[0]) or 256 for item in items)
87
+ if width > best_width:
88
+ best, best_width = items, width
89
+
90
+ # 256x256 条目在 ICO 内以 PNG 存储,可直接作为图片使用。
91
+ if not best:
92
+ raise SystemExit("找不到图标条目")
93
+ for item in best:
94
+ width, _height, _colors, _res, _planes, _bpp, _size, image_id = item
95
+ if (int(width) or 256) >= 256 and image_id in images:
96
+ return images[image_id]
97
+ raise SystemExit("找不到 256x256 图标")
98
+
99
+
100
+ def write_ico(png, path):
101
+ header = struct.pack("<HHH", 0, 1, 1)
102
+ entry = struct.pack("<BBBBHHII", 0, 0, 0, 0, 1, 32, len(png), 6 + 16)
103
+ path.write_bytes(header + entry + png)
104
+
105
+
106
+ def write_png(data, dest):
107
+ tmp = dest.with_name(f".{dest.name}.tmp")
108
+ tmp.write_bytes(data)
109
+ os.replace(tmp, dest)
110
+
111
+
112
+ def main():
113
+ exe, out_dir, *sizes = sys.argv[1:]
114
+ sizes = sizes or ["256x256"]
115
+ png = largest_icon(exe)
116
+ have_magick = shutil.which("magick") is not None
117
+
118
+ icon_file = pathlib.Path("/tmp") / f"ww-icon-{os.getpid()}.ico"
119
+ write_ico(png, icon_file)
120
+ try:
121
+ for size in sizes:
122
+ if size != "256x256" and not have_magick:
123
+ continue
124
+ dest = pathlib.Path(out_dir) / size / "apps" / "ww.png"
125
+ dest.parent.mkdir(parents=True, exist_ok=True)
126
+ if have_magick:
127
+ tmp = dest.with_name(f".{dest.name}.tmp.png")
128
+ try:
129
+ subprocess.run(
130
+ ["magick", str(icon_file), "-resize", size, f"PNG:{tmp}"],
131
+ check=True,
132
+ capture_output=True,
133
+ )
134
+ os.replace(tmp, dest)
135
+ except (subprocess.CalledProcessError, OSError) as exc:
136
+ tmp.unlink(missing_ok=True)
137
+ print(f"警告:ImageMagick 缩放 {size} 失败({exc}),改用原图。", file=sys.stderr)
138
+ write_png(png, dest)
139
+ else:
140
+ write_png(png, dest)
141
+ finally:
142
+ icon_file.unlink(missing_ok=True)
143
+
144
+
145
+ if __name__ == "__main__":
146
+ main()