提交新代码
This commit is contained in:
+46
-7
@@ -1,27 +1,66 @@
|
|||||||
*.pyc
|
# Python 缓存/编译产物
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
**/__pycache__/
|
||||||
|
*.py[cod]
|
||||||
*.pyd
|
*.pyd
|
||||||
*.pyo
|
*.pyo
|
||||||
|
|
||||||
# 打包产物
|
# PyInstaller / Cython / 打包产物
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
dist_cy/
|
dist_cy/
|
||||||
dist_obf/
|
dist_obf/
|
||||||
|
**/build/
|
||||||
|
**/dist/
|
||||||
|
**/dist_cy/
|
||||||
|
**/dist_obf/
|
||||||
|
*.spec
|
||||||
|
_core.*
|
||||||
|
launcher.py
|
||||||
|
setup_cython.py
|
||||||
|
|
||||||
|
# 打包好的安装器,不提交 exe 成品
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
*Installer*.exe
|
*Installer*.exe
|
||||||
*安装*.exe
|
*安装*.exe
|
||||||
*刷入工具*.exe
|
*刷入工具*.exe
|
||||||
|
*推送工具*.exe
|
||||||
|
*Pusher*.exe
|
||||||
|
UNIZ-*.exe
|
||||||
|
|
||||||
# APK文件夹
|
# 资源包/车机文件/临时拉取文件
|
||||||
apks/
|
|
||||||
|
|
||||||
# 资源包
|
|
||||||
package.bin
|
package.bin
|
||||||
|
*package*.bin
|
||||||
|
*.apk
|
||||||
|
*.img
|
||||||
|
*.dat
|
||||||
|
*.zip
|
||||||
|
*.7z
|
||||||
|
*.rar
|
||||||
|
apks/
|
||||||
|
**/apks/
|
||||||
|
diag_apk/
|
||||||
|
pull_list.txt
|
||||||
|
unit.bat
|
||||||
|
|
||||||
|
# 本地工具链副本;只提交需要维护的脚本/说明,不提交二进制
|
||||||
|
tools/*.exe
|
||||||
|
tools/*.dll
|
||||||
|
tools/__pycache__/
|
||||||
|
Q05-Lidar/app.ico
|
||||||
|
Q05-Lidar/tools/*
|
||||||
|
!Q05-Lidar/tools/
|
||||||
|
!Q05-Lidar/tools/encrypt_q05_lidar_resource.py
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# 临时文件
|
# 本地日志/临时文件
|
||||||
*.log
|
*.log
|
||||||
*.tmp
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.old
|
||||||
|
*.swp
|
||||||
|
~$*
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ from PyInstaller.utils.hooks import collect_all
|
|||||||
|
|
||||||
datas = [('adb.exe', '.'), ('AdbWinApi.dll', '.'), ('AdbWinUsbApi.dll', '.'), ('7za.exe', '.')]
|
datas = [('adb.exe', '.'), ('AdbWinApi.dll', '.'), ('AdbWinUsbApi.dll', '.'), ('7za.exe', '.')]
|
||||||
binaries = []
|
binaries = []
|
||||||
hiddenimports = ['queue', 'threading', 'tkinter', 'tkinter.simpledialog', 'zipfile', 'json', 'urllib', 'urllib.parse']
|
hiddenimports = ['queue', 'threading', 'tkinter', 'zipfile', 'json', 'urllib']
|
||||||
tmp_ret = collect_all('tkinter')
|
tmp_ret = collect_all('tkinter')
|
||||||
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
['S05_fixed.py'],
|
['app.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=binaries,
|
binaries=binaries,
|
||||||
datas=datas,
|
datas=datas,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set "ROOT=%~dp0.."
|
||||||
|
set "TOOLS=%ROOT%\tools"
|
||||||
|
set "NAME=Qiyuan_07_Multi-lan-installer"
|
||||||
|
set "SRC=Qiyuan_A07_Multi-lan-installer.py"
|
||||||
|
title %NAME% - Build
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo %NAME% - Cython Build
|
||||||
|
echo ============================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
where python >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Python not found
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
for /f "delims=" %%i in ('where python') do set PY=%%i
|
||||||
|
echo Python: %PY%
|
||||||
|
|
||||||
|
echo [1/6] Installing deps...
|
||||||
|
%PY% -m pip install pyinstaller cython pyzipper -q
|
||||||
|
if errorlevel 1 (
|
||||||
|
%PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [2/6] Clean...
|
||||||
|
if exist "dist_cy" rmdir /s /q dist_cy 2>nul
|
||||||
|
if exist "build" rmdir /s /q build 2>nul
|
||||||
|
if exist "dist" rmdir /s /q dist 2>nul
|
||||||
|
|
||||||
|
echo [3/6] Cython compile...
|
||||||
|
mkdir dist_cy 2>nul
|
||||||
|
copy %SRC% dist_cy\_core.py >nul
|
||||||
|
|
||||||
|
%PY% -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')"
|
||||||
|
|
||||||
|
cd dist_cy
|
||||||
|
%PY% setup_cython.py build_ext --inplace
|
||||||
|
if errorlevel 1 (
|
||||||
|
cd ..
|
||||||
|
echo [WARN] Cython failed, fallback
|
||||||
|
goto :NORMAL
|
||||||
|
)
|
||||||
|
|
||||||
|
for %%f in (_core*.pyd) do set PYD=%%f
|
||||||
|
if "%PYD%"=="" (
|
||||||
|
cd ..
|
||||||
|
echo [WARN] No pyd, fallback
|
||||||
|
goto :NORMAL
|
||||||
|
)
|
||||||
|
echo PYD: %PYD%
|
||||||
|
copy "%PYD%" _core.pyd >nul
|
||||||
|
|
||||||
|
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
||||||
|
|
||||||
|
echo [4/6] Copy resources...
|
||||||
|
copy "%TOOLS%\adb.exe" . >nul
|
||||||
|
copy "%TOOLS%\AdbWinApi.dll" . >nul
|
||||||
|
copy "%TOOLS%\AdbWinUsbApi.dll" . >nul
|
||||||
|
copy "%TOOLS%\7za.exe" . >nul
|
||||||
|
if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul
|
||||||
|
|
||||||
|
echo [5/6] PyInstaller...
|
||||||
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin launcher.py
|
||||||
|
if errorlevel 1 (
|
||||||
|
cd ..
|
||||||
|
echo [ERROR] PyInstaller failed
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [6/6] Cleanup...
|
||||||
|
del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul
|
||||||
|
rmdir /s /q build 2>nul
|
||||||
|
cd ..
|
||||||
|
goto :DONE
|
||||||
|
|
||||||
|
:NORMAL
|
||||||
|
echo [INFO] Normal PyInstaller...
|
||||||
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin %SRC%
|
||||||
|
|
||||||
|
:DONE
|
||||||
|
echo.
|
||||||
|
echo Done.
|
||||||
|
if exist "dist_cy\dist\%NAME%.exe" (
|
||||||
|
echo Output: dist_cy\dist\%NAME%.exe
|
||||||
|
) else if exist "dist\%NAME%.exe" (
|
||||||
|
echo Output: dist\%NAME%.exe
|
||||||
|
) else (
|
||||||
|
echo Check dist folder
|
||||||
|
)
|
||||||
|
pause
|
||||||
@@ -2,167 +2,204 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
A Windows GUI tool suite (Python 3.6+ / tkinter) for flashing multi-language APKs onto Android-based vehicle infotainment systems. Built by 宜宾科宜科技有限公司.
|
Windows GUI tool suite (Python 3.6+ / tkinter) for flashing or pushing multi-language APKs to Android-based vehicle infotainment systems. Built by 宜宾科宜科技有限公司.
|
||||||
|
|
||||||
The suite contains **5 tool variants**, each targeting a different vehicle model. All share the same `ADKAPKGUI` class architecture with model-specific customizations. Authorization is VIN-based via a cloud API.
|
Most tools are single-file tkinter apps with the same rough architecture: GUI, VIN authorization, package extraction, ADB commands, logging, and worker threads live in one class. Preserve that style unless the user explicitly asks for a larger refactor.
|
||||||
|
|
||||||
| Tool file | Vehicle | Window title | Pack script |
|
## Tool Variants
|
||||||
|-----------|---------|-------------|-------------|
|
|
||||||
| `app.py` | 启源Q07 | 长安语言安装工具 | `pack_q07.bat` |
|
|
||||||
| `S05.py` | 深蓝S05 | 深蓝S05多语言安装 | `pack_s05.bat` |
|
|
||||||
| `X5plusTool.py` | X5plus | 适用于X5plus多语言安装 | `pack_x5plus.bat` |
|
|
||||||
| `app-install.py` | 长安逸动 (通用) | 长安语言刷入工具 | `pack_common.bat` |
|
|
||||||
| `app-yidong.py` | 长安逸动 | 长安逸动语言刷入工具 | `pack_yidong.bat` |
|
|
||||||
|
|
||||||
## Project structure
|
| Tool file | Vehicle / purpose | Window title | Pack script |
|
||||||
|
|-----------|-------------------|--------------|-------------|
|
||||||
|
| `Q07/app.py` | 启源Q07 | 长安语言安装工具 | `Q07/pack_q07.bat` |
|
||||||
|
| `S05/S05.py` | 深蓝S05 original | 深蓝S05多语言安装 | `S05/pack_s05.bat` |
|
||||||
|
| `S05/S05_fixed.py` | 深蓝S05 fixed/experimental copy | 长安语言安装工具 | `S05/pack_s05_fixed.bat` |
|
||||||
|
| `X5plus/X5plusTool.py` | X5plus | 适用于X5plus多语言安装 | `X5plus/pack_x5plus.bat` |
|
||||||
|
| `Yidong/app-install.py` | 长安逸动通用 | 长安语言刷入工具 | `Yidong/pack_common.bat` |
|
||||||
|
| `Yidong/app-yidong.py` | 长安逸动 | 长安逸动语言刷入工具 | `Yidong/pack_yidong.bat` |
|
||||||
|
| `UNIZ/UNIZ.py` | UNI-Z file pusher | UNI-Z语言文件推送工具 | `UNIZ/pack_uniz.bat` |
|
||||||
|
| `Mazda-EZ60/Mazda-EZ60.py` | Mazda-EZ60 OS 1.2 | Mazda-EZ60_OS-1.2适用 | `Mazda-EZ60/pack_mazda_ez60.bat` |
|
||||||
|
| `Q05-Lidar/Q05-Lidar_Installer.py` | Q05_Lidar permission/bootstrap + language installer | Q05_Lidar | `Q05-Lidar/pack_q05_lidar.bat` |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
├── app.py # 启源Q07 tool
|
├── Q07/ # Q07 script, pack scripts, ignored build outputs
|
||||||
├── S05.py # 深蓝S05 tool
|
├── S05/ # S05 original and fixed copy
|
||||||
├── X5plusTool.py # X5plus tool
|
├── X5plus/ # X5plus tool
|
||||||
├── app-install.py # 逸动通用 tool
|
├── Yidong/ # common/yidong tools
|
||||||
├── app-yidong.py # 逸动 tool
|
├── UNIZ/ # UNI-Z file pusher
|
||||||
├── pack_q07.bat # Q07 pack script
|
├── Mazda-EZ60/ # Mazda-EZ60 tool
|
||||||
├── pack_s05.bat # S05 pack script
|
├── A07/ # Qiyuan A07 tool
|
||||||
├── pack_x5plus.bat # X5plus pack script
|
├── Q05-Lidar/ # Q05_Lidar tool plus resource.dat/tools
|
||||||
├── pack_common.bat # 逸动通用 pack script
|
├── app.ico
|
||||||
├── pack_yidong.bat # 逸动 pack script
|
├── package.bin # encrypted package, not committed; shared from root
|
||||||
├── test_extract.py # Standalone test for package.bin extraction
|
└── tools/ # shared adb/fastboot/7za dependencies for pack scripts
|
||||||
├── app.ico # Application icon
|
├── adb.exe
|
||||||
├── package.bin # Encrypted ZIP (not in repo)
|
├── AdbWinApi.dll
|
||||||
├── adb.exe # Bundled ADB
|
├── AdbWinUsbApi.dll
|
||||||
├── AdbWinApi.dll # ADB dependency
|
├── fastboot.exe
|
||||||
├── AdbWinUsbApi.dll # ADB dependency
|
└── 7za.exe # 7-Zip Extra 26.01, bundled by pack scripts
|
||||||
└── .vscode/
|
|
||||||
└── settings.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture Notes
|
||||||
|
|
||||||
### Single-file design
|
- Most tools use class `ADKAPKGUI`; `UNIZ/UNIZ.py` uses `UNIZLanguageGUI`.
|
||||||
|
- Worker actions run in `threading.Thread(..., daemon=True)`.
|
||||||
|
- Tkinter calls from workers must go through `run_on_ui_thread(...)`.
|
||||||
|
- Prefer `self.root.after(0, lambda: func(*args, **kwargs))` in `run_on_ui_thread`; direct `after(0, func, *args, **kwargs)` breaks when kwargs such as `text=` or `fg=` are passed.
|
||||||
|
- Background `messagebox.*` calls should be scheduled with `run_on_ui_thread`.
|
||||||
|
- Keep files UTF-8 with `# -*- coding: utf-8 -*-`.
|
||||||
|
|
||||||
Each tool is a single Python file under the `ADKAPKGUI` class. No MVC separation — the class handles GUI, business logic, ADB operations, network calls, and file extraction.
|
## ADB And Auth
|
||||||
|
|
||||||
### Threading model
|
- `run_adb_command(command)` handles normal ADB commands such as `adb devices`, `adb push`, and non-shell install calls.
|
||||||
|
- `run_adb_shell(shell_command)` exists in the 逸动-family tools and Mazda copy; it shells into the device and automatically sends password `adb36987`.
|
||||||
|
- All `adb shell` operations in `Yidong/app-install.py`, `Yidong/app-yidong.py`, and `Mazda-EZ60/Mazda-EZ60.py` should go through `run_adb_shell()`.
|
||||||
|
- `UNIZ/UNIZ.py` must not use `adb shell`; it only checks devices and pushes APKs to `/storage/emulated/0/Download/`.
|
||||||
|
- Standard auth flow uses:
|
||||||
|
- `auth-check?vin=...` for authorization.
|
||||||
|
- `package-key?vin=...` for `package.bin` extraction password.
|
||||||
|
- VIN keys:
|
||||||
|
- Q07/S05/X5plus: `ca_vin_info` or `VIN`.
|
||||||
|
- 逸动/Mazda: `settings get system ca.car.vin` via auto-password shell.
|
||||||
|
- UNI-Z: user manually enters VIN.
|
||||||
|
|
||||||
- **Main thread**: tkinter event loop (`root.mainloop()`)
|
## Package Extraction
|
||||||
- **Background threads**: All user-triggered operations spawn `threading.Thread(target=..., daemon=True)`
|
|
||||||
- **Thread-safety**: UI mutations via `self.run_on_ui_thread(func, ...)` → `self.root.after(0, func, ...)`. Internal `_impl` methods are the actual tkinter-touching implementations.
|
|
||||||
|
|
||||||
### ADB operations
|
- `package.bin` is extracted under `%LOCALAPPDATA%\.cache\system\.android\...`.
|
||||||
|
- Current cache directories:
|
||||||
|
- `Q07/app.py` -> `apps_cache_Q07`
|
||||||
|
- `S05/S05.py` / `S05/S05_fixed.py` -> `apps_cache_S05`
|
||||||
|
- `X5plus/X5plusTool.py` -> `apps_cache_X5plus`
|
||||||
|
- `Yidong/app-install.py` -> `apps_cache_common`
|
||||||
|
- `Yidong/app-yidong.py` -> `apps_cache_yidong`
|
||||||
|
- `UNIZ/UNIZ.py` -> `apps_cache_UNIZ`
|
||||||
|
- `Mazda-EZ60/Mazda-EZ60.py` -> `apps_cache_Mazda_EZ60`
|
||||||
|
- Shared binaries are managed under root `tools/`: `adb.exe`, `AdbWinApi.dll`, `AdbWinUsbApi.dll`, `fastboot.exe`, and `7za.exe`. Vehicle pack scripts in subfolders should copy from `%ROOT%\tools`, not from each vehicle folder.
|
||||||
|
- For progress display, detect support for `-bsp1` by checking for `-bs{o|e|p}` in 7za help output.
|
||||||
|
- If `Incorrect command line` appears, retry with the basic compatible command: `x package.bin -pPASSWORD -oDIR -y`.
|
||||||
|
- Decode 7za output with GBK first, then UTF-8 fallback.
|
||||||
|
- 7za progress must parse streamed output cumulatively. Do not read one byte and regex that single byte; percentages such as `42%` span multiple bytes and will otherwise jump from 0 to 100.
|
||||||
|
- User-facing resource extraction text should say `资源准备中` / `Preparing resources`, not `资源解压` / `Extracting package`, unless the UI is an explicit debug test.
|
||||||
|
- Cache cleanup should be best effort in three places when feasible: before a new extraction, during normal window close, and via `atexit` for ordinary process exit. A forced process kill cannot be guaranteed, so also clear stale caches at next extraction/startup.
|
||||||
|
|
||||||
- `run_adb_command(command)` — standard ADB commands; auto-substitutes `adb` with bundled `adb.exe` path when frozen
|
## Shared UX And Safety Rules
|
||||||
- `run_adb_shell(shell_command)` — only in app-install.py / app-yidong.py; shells into device with auto-password `adb36987`
|
|
||||||
|
|
||||||
ADB path is portable: `self.adb` resolves to `sys._MEIPASS/adb.exe` when frozen (PyInstaller bundle), or `'adb'` in development.
|
- Hosts update logic should replace conflicting entries for the managed domain. If the hosts file already contains the target domain with a different IP, delete that line and write the expected `IP domain` entry instead of appending duplicates.
|
||||||
|
- VIN authorization logs should be explicit for operator-facing flows: print the current VIN, print `data.vehicleName` when `auth-check` returns it, print authorization success, and print a clear unauthorized/failure log when denied.
|
||||||
|
- `package-key` requests should use the vehicle name returned by `auth-check` (`data.vehicleName`) whenever available. Do not hardcode a model name if the authorization API already returned the exact vehicle name for the VIN.
|
||||||
|
- Normal users should not see low-level sensitive process details such as `fastboot`, `init_boot`, boot keys, or image names during permission/bootstrap flows. Use black-box text such as `正在获取权限中`, `获取成功`, and `获取失败`; leave command details for debug mode only.
|
||||||
|
- Process logs should stay minimal in normal mode. Detailed ADB/7za/API command logs belong behind debug mode.
|
||||||
|
- All Tkinter UI updates and `messagebox.*` calls from workers must go through `run_on_ui_thread(...)`.
|
||||||
|
|
||||||
### Authentication flow
|
## Model-Specific Behavior
|
||||||
|
|
||||||
1. ADB detects device → reads VIN (`ca_vin_info` or `VIN`; 逸动 uses `ca.car.vin`)
|
### S05
|
||||||
2. VIN sent to `https://api.changan.softwindy.cn/api/authorizations/auth-check?vin=...`
|
|
||||||
3. Package password from `https://api.changan.softwindy.cn/api/authorizations/package-key?vin=...`
|
|
||||||
4. Password used to decrypt `package.bin`
|
|
||||||
|
|
||||||
### Package extraction
|
- Keep `S05/S05.py` as original unless explicitly asked.
|
||||||
|
- Use `S05/S05_fixed.py` for experimental/fixed S05 changes.
|
||||||
|
- Do not add `chmod`, `chown`, or `restorecon` to the S05 system-app push path unless explicitly requested; the target system inherits permissions.
|
||||||
|
- `S05/S05_fixed.py` includes debug extract test `Ctrl+Shift+E` and 7za progress support.
|
||||||
|
|
||||||
- `package.bin` is a password-protected ZIP (AES via pyzipper, fallback to zipfile)
|
### UNI-Z
|
||||||
- Extracted to `%LOCALAPPDATA%\.cache\system\.android\apps_cache_{variant}` (hidden via `attrib +h`)
|
|
||||||
- Each tool has its own cache folder to avoid conflicts:
|
|
||||||
- app.py → `apps_cache_Q07`
|
|
||||||
- S05.py → `apps_cache_S05`
|
|
||||||
- X5plusTool.py → `apps_cache_X5plus`
|
|
||||||
- app-install.py → `apps_cache_common`
|
|
||||||
- app-yidong.py → `apps_cache_common` (default)
|
|
||||||
- Extracted APKs in `app/` pushed to `/system/app/`, `priv-app/` to `/system/priv-app/`
|
|
||||||
|
|
||||||
### Device status monitoring
|
- Endpoint for visible passwords: `/api/authorizations/get-uni-z-pwd`.
|
||||||
|
- Display `factoryPwd` as factory mode password and `password` as debug password.
|
||||||
|
- If `authorized == false` or `password` is empty, show unauthorized state and do not proceed.
|
||||||
|
- `password` from `get-uni-z-pwd` is not the package extraction password.
|
||||||
|
- Before push, call `/api/authorizations/package-key?vin=...` to get the real `package.bin` password.
|
||||||
|
- Push only to `/storage/emulated/0/Download/`; no `adb shell`.
|
||||||
|
- Language selection: RU/FR/ES/EN. Only the selected language Settings APK is pushed; other language Settings APKs are skipped silently.
|
||||||
|
- Hidden debug mode: `Ctrl+Shift+D`, password `zxch5200`, logs full ADB/7za/API details.
|
||||||
|
|
||||||
Background daemon thread runs `adb devices` every 5 seconds to detect connect/disconnect events.
|
### Mazda-EZ60
|
||||||
|
|
||||||
## GUI layout (650x640 dark theme for most; 650x550 for 逸动)
|
- Based on `Yidong/app-install.py` / 逸动 flow.
|
||||||
|
- Uses auto-password shell (`adb36987`) for VIN reads, `pm install`, overlay enable, disable commands, settings, and reboot.
|
||||||
|
- Installs APKs from extracted `apps` via push to `/data/local/tmp` then `pm install -r -d`.
|
||||||
|
- `Mazda-EZ60/Mazda-EZ60.py` should show 7za extraction progress with stream parsing.
|
||||||
|
- Regardless of APK install failures, run post-install configuration after the install loop.
|
||||||
|
- Post-install overlays to enable:
|
||||||
|
- `com.tinnove.launcher.overlay`
|
||||||
|
- `com.tinnove.scenemode.overlay`
|
||||||
|
- `com.incall.dvr.overlay`
|
||||||
|
- Post-install packages to disable:
|
||||||
|
- `com.carinno.p1`
|
||||||
|
- `com.wtcl.electronicdirections`
|
||||||
|
- `com.ximalaya.ting.android.car`
|
||||||
|
- `com.tinnove.netease.music`
|
||||||
|
- `com.migu.miguplay.car`
|
||||||
|
- `cn.cmvideo.car.play`
|
||||||
|
- `com.tinnove.carshow`
|
||||||
|
- `com.tinnove.changba`
|
||||||
|
- `com.qiyi.video.iv`
|
||||||
|
- User cancelled the `Ctrl+Shift+E` direct extract test request for Mazda; do not add it unless asked again.
|
||||||
|
|
||||||
| Section | Contents |
|
### Q05_Lidar
|
||||||
|---------|----------|
|
|
||||||
| Title bar | Vehicle model + company subtitle |
|
|
||||||
| Password query | Hidden VIN→password lookup (S05, app-install only) |
|
|
||||||
| Button row 1 | 获取权限 (Q07/S05), 刷入语言包, 安装App, 语言设置 |
|
|
||||||
| Button row 2 | 时区设置, 安卓设置, 重启设备, 禁用升级 |
|
|
||||||
| Status bar | Connection dot, VIN, Auth status, Refresh button |
|
|
||||||
| Tips | Usage warnings |
|
|
||||||
| Progress | Extraction + push progress (hidden by default) |
|
|
||||||
| Log area | ScrolledText with tags (INFO/SUCCESS/ERROR/WARNING/CMD), 清空日志 button in title bar |
|
|
||||||
|
|
||||||
## Key features
|
- This is the Q05_Lidar-specific tool and must not be confused with any ordinary Q05 variant or package.
|
||||||
|
- Based on the shared installer visual style, but its resource structure and flashing flow are Q05_Lidar-specific.
|
||||||
|
- The first-row `获取权限` button installs `runtime.dat` -> `base.apk`, reboots to fastboot, waits for a real `fastboot devices` row like `<serial> fastboot` with a non-aggressive interval, then fetches a boot key through `POST /api/authorizations/boot-challenge` then `POST /api/authorizations/boot-key`, decrypts embedded `resource.dat`, flashes `init_boot`, immediately reboots, and deletes the temporary img. Keep the decrypted img lifetime as short as possible.
|
||||||
|
- `resource.dat` is AES-GCM encrypted and must match the server `BOOT_KEY`; the tool only accepts `data.sessionKey` from `boot-key`.
|
||||||
|
- Device fingerprint data sent to the server includes ADB serial, `ro.serialno`, `ro.boot.serialno`, manufacturer, model, device, build fingerprint, and VIN.
|
||||||
|
- `刷入语言包` installs Magisk modules, not APKs. It opens `com.topjohnwu.magisk`, warns the user to grant Shell/root permission, verifies `/debug_ramdisk/su -c "id"` returns `uid=0`, extracts `Q05_Lidar-package.bin`, then pushes module files to `/data/local/tmp/q05_lidar_modules/<MODID>/` and root-copies them into `/data/adb/modules/<MODID>`.
|
||||||
|
- `Q05_Lidar-package.bin` should unpack with module files at archive root: `module.prop`, scripts, `system/`, and `disable-wireless-adb-vecentek-magisk.zip`; do not wrap them in an outer `Q05_LIDAR_DATA/` directory.
|
||||||
|
- Q05_Lidar package cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_Q05_Lidar`; the tool cleans it on startup/extraction and on normal/atexit shutdown.
|
||||||
|
- Q05_Lidar `runtime.dat` cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_q05_lidar_runtime`; treat it as temporary and clean stale contents before extraction.
|
||||||
|
- `Q05_Lidar-package.bin` is an external release file next to the exe because it is large. `resource.dat` is embedded in the exe; `runtime.dat` should be copied next to the exe by the pack script.
|
||||||
|
- `package-key` must include the `vehicleName` returned by `auth-check` for the VIN. The tool caches `data.vehicleName` from password query / authorization check and uses it for package-key; if missing, query `auth-check` first rather than falling back to a hardcoded Q05_Lidar value.
|
||||||
|
- All `adb shell` commands in Q05_Lidar, including Magisk launch and `/debug_ramdisk/su -c ...`, must go through `run_adb_shell()` so the tool silently sends `adb36987`.
|
||||||
|
- The `安装App` button remains the APK install path: file picker -> `adb push` -> `setprop vecentek.model 1` -> `pm install -r -d -f` -> cleanup. Do not replace it with the Magisk module flow.
|
||||||
|
- Temporary debug mode exists only for development and should be removed before release when requested. Press `Ctrl+Shift+D`; the password is verified through `POST /api/authorizations/verify-debug-mode-password`.
|
||||||
|
- In Q05_Lidar debug mode, hidden buttons appear for:
|
||||||
|
- `指纹测试`: collect and log device fingerprint fields plus local SHA256 summary.
|
||||||
|
- `解密测试`: if VIN/device is available, fetch boot key; otherwise prompt for a pasted `BOOT_KEY`/`sessionKey`, decrypt `resource.dat` locally to a temporary img, log size/SHA256, then delete it.
|
||||||
|
- `解压测试`: fetch `package-key`, extract `Q05_Lidar-package.bin`, and verify the main Magisk module plus `disable_wireless_adb_vecentek` module can be identified.
|
||||||
|
- Do not log the actual boot key/session key in debug mode.
|
||||||
|
|
||||||
### Install App (unified single/batch)
|
### Yidong/app-yidong.py
|
||||||
"安装App" button uses `filedialog.askopenfilenames` for multi-file selection, replacing old separate "单个安装" and "批量安装" buttons. Select 1 or more APKs and installs all in one pass (`adb install -r`).
|
|
||||||
|
|
||||||
### Debug mode (hidden)
|
- Uses `apps_cache_yidong`.
|
||||||
Press `Ctrl+Shift+D` → password `zxch5200` to enter debug mode:
|
- Has 7za compatibility handling for progress switches and `Incorrect command line` fallback.
|
||||||
- Bypasses device connection and authorization checks (UI shows "已连接" / "已授权")
|
- `Yidong/pack_yidong.bat` output name is ASCII: `Changan-Yidong-Language-Installer.exe`, to avoid CMD codepage issues with Chinese `NAME`.
|
||||||
- Logs all raw ADB commands and output to the log panel
|
|
||||||
- Exiting debug mode auto-refreshes real device state
|
|
||||||
|
|
||||||
### Language quick-set
|
## Build Notes
|
||||||
Popup with 8 one-click locale switches (zh-CN, en-US, ru-RU, fr-FR, es-ES, pt-BR, it-IT, ar-SA). Also links to native Android language settings. Changes take effect after reboot.
|
|
||||||
|
|
||||||
### Model-specific behaviors
|
- Pack scripts install/use `pyinstaller`, `cython`, and usually `pyzipper`.
|
||||||
|
- Cython success requires Microsoft C++ Build Tools.
|
||||||
|
- Cython success signs in logs:
|
||||||
|
- `building '_core' extension`
|
||||||
|
- `_core.cpXXX-win_amd64.pyd`
|
||||||
|
- `PYD: _core...pyd`
|
||||||
|
- output under `dist_cy\dist\...exe`
|
||||||
|
- If logs show `[WARN] Cython failed, fallback` and `[INFO] Normal PyInstaller`, the exe still builds but is normal PyInstaller and easier to reverse.
|
||||||
|
- For security-sensitive tools like Q05_Lidar, do not keep a normal PyInstaller fallback. If Cython fails or no `_core*.pyd` is generated, stop the build and show an error.
|
||||||
|
- A Cython onefile PyInstaller build should use a tiny `launcher.py` that imports `main` from compiled `_core.pyd`, and the exe archive should contain `_core*.pyd`. Confirm with PyInstaller archive viewer when in doubt.
|
||||||
|
- `UNIZ/pack_uniz.bat` and `Mazda-EZ60/pack_mazda_ez60.bat` use ASCII output names to avoid CMD encoding problems.
|
||||||
|
- Generated `.exe`, `.spec`, `build/`, `dist/`, and `dist_cy/` are build artifacts and should not be committed unless explicitly requested.
|
||||||
|
|
||||||
| Feature | Q07 (app.py) | S05 (S05.py) | X5plus (X5plusTool.py) | 逸动 (app-install/yidong) |
|
## Key Behaviors To Preserve
|
||||||
|---------|-------------|-------------|------------------------|---------------------------|
|
|
||||||
| Root perm | Y | Y | N (no btn_root) | N |
|
|
||||||
| priv-app | Y | Y | Y | N (app only) |
|
|
||||||
| ADB shell | Standard | Standard | Standard | Auto-password `adb36987` |
|
|
||||||
| VIN key | ca_vin_info/VIN | ca_vin_info/VIN | ca_vin_info/VIN | ca.car.vin |
|
|
||||||
| Old app cleanup | N | N | Y (5 packages) | N |
|
|
||||||
| Font push | N | N | Y (FZLTHPro) | N |
|
|
||||||
| Overlay enable | N | N | N | N (removed) |
|
|
||||||
| Password query | N | Y | N | Y |
|
|
||||||
| Factory hints | N | Y | Y (dynamic pwd) | Y |
|
|
||||||
|
|
||||||
## Build process
|
1. Keep original tools untouched when a fixed or model-specific copy exists.
|
||||||
|
2. Preserve VIN-based authorization for normal flashing tools.
|
||||||
|
3. Fetch `package-key` from the server instead of hardcoding package passwords.
|
||||||
|
4. Keep all shell commands in 逸动/Mazda tools behind `run_adb_shell()`.
|
||||||
|
5. Keep UNI-Z shell-free.
|
||||||
|
6. Use `run_on_ui_thread()` for all tkinter UI updates from worker threads.
|
||||||
|
7. Keep shared Android/7za binaries under root `tools/` and have pack scripts copy from there.
|
||||||
|
|
||||||
Each `pack_*.bat` follows the same pipeline:
|
## Current Local State (2026-05-28)
|
||||||
1. Install deps: `pyinstaller`, `cython`, `pyzipper`
|
|
||||||
2. Clean old build dirs
|
|
||||||
3. Cython compile: `{source}.py` → `_core.pyd`
|
|
||||||
4. Copy resources: `adb.exe`, DLLs, `app.ico`
|
|
||||||
5. PyInstaller: single `.exe` with `--uac-admin`, bundling `_core.pyd` + ADB + DLLs
|
|
||||||
6. Fallback to normal PyInstaller if Cython fails
|
|
||||||
|
|
||||||
Output exe is self-contained — bundles `adb.exe` and DLLs via `sys._MEIPASS`, no external ADB needed.
|
- `UNIZ/UNIZ.py` and `UNIZ/pack_uniz.bat` exist locally. Cython build has succeeded after installing Microsoft C++ Build Tools, producing `dist_cy\dist\UNIZ-Language-Pusher.exe`.
|
||||||
|
- `Mazda-EZ60/Mazda-EZ60.py` and `Mazda-EZ60/pack_mazda_ez60.bat` exist locally. Mazda has 7za progress extraction, unconditional post-install configuration, three overlay enables, and nine package disables.
|
||||||
|
- `Yidong/app-yidong.py` has been updated for 7za progress compatibility and `Incorrect command line` fallback.
|
||||||
|
- `Yidong/pack_yidong.bat` has been updated with quoted paths, `cd /d "%~dp0"`, and ASCII output name.
|
||||||
|
- `.gitignore` has been expanded to ignore generated exe/spec artifacts.
|
||||||
|
- There may be untracked local build outputs and generated specs; inspect `git status --ignored` before committing.
|
||||||
|
|
||||||
## Key behaviors to preserve
|
## Known Issues
|
||||||
|
|
||||||
1. **Thread safety**: Never call tkinter from background threads — always use `run_on_ui_thread`
|
- Some older tools still have minimal exception handling and bare `except: pass`.
|
||||||
2. **VIN-based auth**: Authorization required before push; flow must remain intact
|
- `test_extract.py` hardcodes a password and should not be treated as production flow.
|
||||||
3. **Silent extraction**: Auto-extract `package.bin` without user step
|
- `on_disable_upgrade` behavior is Windows/vehicle specific.
|
||||||
4. **ADB portability**: `self.adb` resolves to bundled exe path; all commands go through `run_adb_command`
|
- Pure PyInstaller fallback is easy to reverse; prefer successful Cython builds for release.
|
||||||
5. **vecentek.model**: Set to 1 before install, 0 after
|
|
||||||
6. **Chinese encoding**: `# -*- coding: utf-8 -*-` throughout
|
|
||||||
7. **Debug mode**: `self.debug_mode` flag gates auth/connection bypass and verbose logging
|
|
||||||
8. **Separate cache dirs**: Each tool extracts to its own cache folder
|
|
||||||
|
|
||||||
## Known issues
|
|
||||||
|
|
||||||
- `test_extract.py` hardcodes password — should fetch from API
|
|
||||||
- `on_disable_upgrade` uses `findstr` (Windows-specific)
|
|
||||||
- Exception handling is minimal in many places (bare `except: pass`)
|
|
||||||
- 逸动 tools (app-install/yidong) use `subprocess.Popen` with stdin password injection — fragile
|
|
||||||
|
|
||||||
## Current local state (2026-05-20)
|
|
||||||
|
|
||||||
- `S05.py` is kept as the original S05 tool entry. Do not fold the fixed-version changes back into it unless explicitly requested.
|
|
||||||
- `S05_fixed.py` is the active experimental/fixed S05 copy. It includes:
|
|
||||||
- `simpledialog` import fix for debug mode.
|
|
||||||
- Thread-safe message boxes from background threads via `run_on_ui_thread`.
|
|
||||||
- Quoted/portable ADB command path helper.
|
|
||||||
- `try/finally` recovery for `setprop vecentek.model 0` in install flows.
|
|
||||||
- `_refreshing` reset in `refresh_device_status()` via `finally`.
|
|
||||||
- URL-encoded VIN query parameters.
|
|
||||||
- `Ctrl+Shift+E` debug-only package extraction test that skips device/auth checks and asks for the package password.
|
|
||||||
- 7za progress support: detects `-bsp1` support and enables `-bsp1 -bso0 -bse1` only when supported, otherwise falls back to normal extraction.
|
|
||||||
- Root `7za.exe` has been replaced with 7-Zip Extra `26.01 (x64)` and is the file pack scripts will bundle.
|
|
||||||
- The unpacked 7-Zip Extra files/folders (`x64/`, `arm64/`, `Far/`, `7za.dll`, `7zxa.dll`, docs) are present locally, but existing pack scripts only bundle root `7za.exe`.
|
|
||||||
- `pack_s05_fixed.bat` was added to build `S05_fixed.py` separately as `深蓝S05刷入工具_fixed.exe`; it leaves `pack_s05.bat` unchanged.
|
|
||||||
- Do not add `chmod`, `chown`, or `restorecon` commands to the S05 system-app push path unless explicitly requested; the user confirmed the target system inherits permissions automatically.
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -2,150 +2,204 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
A Windows GUI tool suite (Python 3.6+ / tkinter) for flashing multi-language APKs onto Android-based vehicle infotainment systems. Built by 宜宾科宜科技有限公司.
|
Windows GUI tool suite (Python 3.6+ / tkinter) for flashing or pushing multi-language APKs to Android-based vehicle infotainment systems. Built by 宜宾科宜科技有限公司.
|
||||||
|
|
||||||
The suite contains **5 tool variants**, each targeting a different vehicle model. All share the same `ADKAPKGUI` class architecture with model-specific customizations. Authorization is VIN-based via a cloud API.
|
Most tools are single-file tkinter apps with the same rough architecture: GUI, VIN authorization, package extraction, ADB commands, logging, and worker threads live in one class. Preserve that style unless the user explicitly asks for a larger refactor.
|
||||||
|
|
||||||
| Tool file | Vehicle | Window title | Pack script |
|
## Tool Variants
|
||||||
|-----------|---------|-------------|-------------|
|
|
||||||
| `app.py` | 启源Q07 | 长安语言安装工具 | `pack_q07.bat` |
|
|
||||||
| `S05.py` | 深蓝S05 | 深蓝S05多语言安装 | `pack_s05.bat` |
|
|
||||||
| `X5plusTool.py` | X5plus | 适用于X5plus多语言安装 | `pack_x5plus.bat` |
|
|
||||||
| `app-install.py` | 长安逸动 (通用) | 长安语言刷入工具 | `pack_common.bat` |
|
|
||||||
| `app-yidong.py` | 长安逸动 | 长安逸动语言刷入工具 | `pack_yidong.bat` |
|
|
||||||
|
|
||||||
## Project structure
|
| Tool file | Vehicle / purpose | Window title | Pack script |
|
||||||
|
|-----------|-------------------|--------------|-------------|
|
||||||
|
| `Q07/app.py` | 启源Q07 | 长安语言安装工具 | `Q07/pack_q07.bat` |
|
||||||
|
| `S05/S05.py` | 深蓝S05 original | 深蓝S05多语言安装 | `S05/pack_s05.bat` |
|
||||||
|
| `S05/S05_fixed.py` | 深蓝S05 fixed/experimental copy | 长安语言安装工具 | `S05/pack_s05_fixed.bat` |
|
||||||
|
| `X5plus/X5plusTool.py` | X5plus | 适用于X5plus多语言安装 | `X5plus/pack_x5plus.bat` |
|
||||||
|
| `Yidong/app-install.py` | 长安逸动通用 | 长安语言刷入工具 | `Yidong/pack_common.bat` |
|
||||||
|
| `Yidong/app-yidong.py` | 长安逸动 | 长安逸动语言刷入工具 | `Yidong/pack_yidong.bat` |
|
||||||
|
| `UNIZ/UNIZ.py` | UNI-Z file pusher | UNI-Z语言文件推送工具 | `UNIZ/pack_uniz.bat` |
|
||||||
|
| `Mazda-EZ60/Mazda-EZ60.py` | Mazda-EZ60 OS 1.2 | Mazda-EZ60_OS-1.2适用 | `Mazda-EZ60/pack_mazda_ez60.bat` |
|
||||||
|
| `Q05-Lidar/Q05-Lidar_Installer.py` | Q05_Lidar permission/bootstrap + language installer | Q05_Lidar | `Q05-Lidar/pack_q05_lidar.bat` |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
├── app.py # 启源Q07 tool
|
├── Q07/ # Q07 script, pack scripts, ignored build outputs
|
||||||
├── S05.py # 深蓝S05 tool
|
├── S05/ # S05 original and fixed copy
|
||||||
├── X5plusTool.py # X5plus tool
|
├── X5plus/ # X5plus tool
|
||||||
├── app-install.py # 逸动通用 tool
|
├── Yidong/ # common/yidong tools
|
||||||
├── app-yidong.py # 逸动 tool
|
├── UNIZ/ # UNI-Z file pusher
|
||||||
├── pack_q07.bat # Q07 pack script
|
├── Mazda-EZ60/ # Mazda-EZ60 tool
|
||||||
├── pack_s05.bat # S05 pack script
|
├── A07/ # Qiyuan A07 tool
|
||||||
├── pack_x5plus.bat # X5plus pack script
|
├── Q05-Lidar/ # Q05_Lidar tool plus resource.dat/tools
|
||||||
├── pack_common.bat # 逸动通用 pack script
|
├── app.ico
|
||||||
├── pack_yidong.bat # 逸动 pack script
|
├── package.bin # encrypted package, not committed; shared from root
|
||||||
├── test_extract.py # Standalone test for package.bin extraction
|
└── tools/ # shared adb/fastboot/7za dependencies for pack scripts
|
||||||
├── app.ico # Application icon
|
├── adb.exe
|
||||||
├── package.bin # Encrypted ZIP (not in repo)
|
├── AdbWinApi.dll
|
||||||
├── adb.exe # Bundled ADB
|
├── AdbWinUsbApi.dll
|
||||||
├── AdbWinApi.dll # ADB dependency
|
├── fastboot.exe
|
||||||
├── AdbWinUsbApi.dll # ADB dependency
|
└── 7za.exe # 7-Zip Extra 26.01, bundled by pack scripts
|
||||||
└── .vscode/
|
|
||||||
└── settings.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture Notes
|
||||||
|
|
||||||
### Single-file design
|
- Most tools use class `ADKAPKGUI`; `UNIZ/UNIZ.py` uses `UNIZLanguageGUI`.
|
||||||
|
- Worker actions run in `threading.Thread(..., daemon=True)`.
|
||||||
|
- Tkinter calls from workers must go through `run_on_ui_thread(...)`.
|
||||||
|
- Prefer `self.root.after(0, lambda: func(*args, **kwargs))` in `run_on_ui_thread`; direct `after(0, func, *args, **kwargs)` breaks when kwargs such as `text=` or `fg=` are passed.
|
||||||
|
- Background `messagebox.*` calls should be scheduled with `run_on_ui_thread`.
|
||||||
|
- Keep files UTF-8 with `# -*- coding: utf-8 -*-`.
|
||||||
|
|
||||||
Each tool is a single Python file under the `ADKAPKGUI` class. No MVC separation — the class handles GUI, business logic, ADB operations, network calls, and file extraction.
|
## ADB And Auth
|
||||||
|
|
||||||
### Threading model
|
- `run_adb_command(command)` handles normal ADB commands such as `adb devices`, `adb push`, and non-shell install calls.
|
||||||
|
- `run_adb_shell(shell_command)` exists in the 逸动-family tools and Mazda copy; it shells into the device and automatically sends password `adb36987`.
|
||||||
|
- All `adb shell` operations in `Yidong/app-install.py`, `Yidong/app-yidong.py`, and `Mazda-EZ60/Mazda-EZ60.py` should go through `run_adb_shell()`.
|
||||||
|
- `UNIZ/UNIZ.py` must not use `adb shell`; it only checks devices and pushes APKs to `/storage/emulated/0/Download/`.
|
||||||
|
- Standard auth flow uses:
|
||||||
|
- `auth-check?vin=...` for authorization.
|
||||||
|
- `package-key?vin=...` for `package.bin` extraction password.
|
||||||
|
- VIN keys:
|
||||||
|
- Q07/S05/X5plus: `ca_vin_info` or `VIN`.
|
||||||
|
- 逸动/Mazda: `settings get system ca.car.vin` via auto-password shell.
|
||||||
|
- UNI-Z: user manually enters VIN.
|
||||||
|
|
||||||
- **Main thread**: tkinter event loop (`root.mainloop()`)
|
## Package Extraction
|
||||||
- **Background threads**: All user-triggered operations spawn `threading.Thread(target=..., daemon=True)`
|
|
||||||
- **Thread-safety**: UI mutations via `self.run_on_ui_thread(func, ...)` → `self.root.after(0, func, ...)`. Internal `_impl` methods are the actual tkinter-touching implementations.
|
|
||||||
|
|
||||||
### ADB operations
|
- `package.bin` is extracted under `%LOCALAPPDATA%\.cache\system\.android\...`.
|
||||||
|
- Current cache directories:
|
||||||
|
- `Q07/app.py` -> `apps_cache_Q07`
|
||||||
|
- `S05/S05.py` / `S05/S05_fixed.py` -> `apps_cache_S05`
|
||||||
|
- `X5plus/X5plusTool.py` -> `apps_cache_X5plus`
|
||||||
|
- `Yidong/app-install.py` -> `apps_cache_common`
|
||||||
|
- `Yidong/app-yidong.py` -> `apps_cache_yidong`
|
||||||
|
- `UNIZ/UNIZ.py` -> `apps_cache_UNIZ`
|
||||||
|
- `Mazda-EZ60/Mazda-EZ60.py` -> `apps_cache_Mazda_EZ60`
|
||||||
|
- Shared binaries are managed under root `tools/`: `adb.exe`, `AdbWinApi.dll`, `AdbWinUsbApi.dll`, `fastboot.exe`, and `7za.exe`. Vehicle pack scripts in subfolders should copy from `%ROOT%\tools`, not from each vehicle folder.
|
||||||
|
- For progress display, detect support for `-bsp1` by checking for `-bs{o|e|p}` in 7za help output.
|
||||||
|
- If `Incorrect command line` appears, retry with the basic compatible command: `x package.bin -pPASSWORD -oDIR -y`.
|
||||||
|
- Decode 7za output with GBK first, then UTF-8 fallback.
|
||||||
|
- 7za progress must parse streamed output cumulatively. Do not read one byte and regex that single byte; percentages such as `42%` span multiple bytes and will otherwise jump from 0 to 100.
|
||||||
|
- User-facing resource extraction text should say `资源准备中` / `Preparing resources`, not `资源解压` / `Extracting package`, unless the UI is an explicit debug test.
|
||||||
|
- Cache cleanup should be best effort in three places when feasible: before a new extraction, during normal window close, and via `atexit` for ordinary process exit. A forced process kill cannot be guaranteed, so also clear stale caches at next extraction/startup.
|
||||||
|
|
||||||
- `run_adb_command(command)` — standard ADB commands; auto-substitutes `adb` with bundled `adb.exe` path when frozen
|
## Shared UX And Safety Rules
|
||||||
- `run_adb_shell(shell_command)` — only in app-install.py / app-yidong.py; shells into device with auto-password `adb36987`
|
|
||||||
|
|
||||||
ADB path is portable: `self.adb` resolves to `sys._MEIPASS/adb.exe` when frozen (PyInstaller bundle), or `'adb'` in development.
|
- Hosts update logic should replace conflicting entries for the managed domain. If the hosts file already contains the target domain with a different IP, delete that line and write the expected `IP domain` entry instead of appending duplicates.
|
||||||
|
- VIN authorization logs should be explicit for operator-facing flows: print the current VIN, print `data.vehicleName` when `auth-check` returns it, print authorization success, and print a clear unauthorized/failure log when denied.
|
||||||
|
- `package-key` requests should use the vehicle name returned by `auth-check` (`data.vehicleName`) whenever available. Do not hardcode a model name if the authorization API already returned the exact vehicle name for the VIN.
|
||||||
|
- Normal users should not see low-level sensitive process details such as `fastboot`, `init_boot`, boot keys, or image names during permission/bootstrap flows. Use black-box text such as `正在获取权限中`, `获取成功`, and `获取失败`; leave command details for debug mode only.
|
||||||
|
- Process logs should stay minimal in normal mode. Detailed ADB/7za/API command logs belong behind debug mode.
|
||||||
|
- All Tkinter UI updates and `messagebox.*` calls from workers must go through `run_on_ui_thread(...)`.
|
||||||
|
|
||||||
### Authentication flow
|
## Model-Specific Behavior
|
||||||
|
|
||||||
1. ADB detects device → reads VIN (`ca_vin_info` or `VIN`; 逸动 uses `ca.car.vin`)
|
### S05
|
||||||
2. VIN sent to `https://api.changan.softwindy.cn/api/authorizations/auth-check?vin=...`
|
|
||||||
3. Package password from `https://api.changan.softwindy.cn/api/authorizations/package-key?vin=...`
|
|
||||||
4. Password used to decrypt `package.bin`
|
|
||||||
|
|
||||||
### Package extraction
|
- Keep `S05/S05.py` as original unless explicitly asked.
|
||||||
|
- Use `S05/S05_fixed.py` for experimental/fixed S05 changes.
|
||||||
|
- Do not add `chmod`, `chown`, or `restorecon` to the S05 system-app push path unless explicitly requested; the target system inherits permissions.
|
||||||
|
- `S05/S05_fixed.py` includes debug extract test `Ctrl+Shift+E` and 7za progress support.
|
||||||
|
|
||||||
- `package.bin` is a password-protected ZIP (AES via pyzipper, fallback to zipfile)
|
### UNI-Z
|
||||||
- Extracted to `%LOCALAPPDATA%\.cache\system\.android\apps_cache_{variant}` (hidden via `attrib +h`)
|
|
||||||
- Each tool has its own cache folder to avoid conflicts:
|
|
||||||
- app.py → `apps_cache_Q07`
|
|
||||||
- S05.py → `apps_cache_S05`
|
|
||||||
- X5plusTool.py → `apps_cache_X5plus`
|
|
||||||
- app-install.py → `apps_cache_common`
|
|
||||||
- app-yidong.py → `apps_cache_common` (default)
|
|
||||||
- Extracted APKs in `app/` pushed to `/system/app/`, `priv-app/` to `/system/priv-app/`
|
|
||||||
|
|
||||||
### Device status monitoring
|
- Endpoint for visible passwords: `/api/authorizations/get-uni-z-pwd`.
|
||||||
|
- Display `factoryPwd` as factory mode password and `password` as debug password.
|
||||||
|
- If `authorized == false` or `password` is empty, show unauthorized state and do not proceed.
|
||||||
|
- `password` from `get-uni-z-pwd` is not the package extraction password.
|
||||||
|
- Before push, call `/api/authorizations/package-key?vin=...` to get the real `package.bin` password.
|
||||||
|
- Push only to `/storage/emulated/0/Download/`; no `adb shell`.
|
||||||
|
- Language selection: RU/FR/ES/EN. Only the selected language Settings APK is pushed; other language Settings APKs are skipped silently.
|
||||||
|
- Hidden debug mode: `Ctrl+Shift+D`, password `zxch5200`, logs full ADB/7za/API details.
|
||||||
|
|
||||||
Background daemon thread runs `adb devices` every 5 seconds to detect connect/disconnect events.
|
### Mazda-EZ60
|
||||||
|
|
||||||
## GUI layout (650x640 dark theme for most; 650x550 for 逸动)
|
- Based on `Yidong/app-install.py` / 逸动 flow.
|
||||||
|
- Uses auto-password shell (`adb36987`) for VIN reads, `pm install`, overlay enable, disable commands, settings, and reboot.
|
||||||
|
- Installs APKs from extracted `apps` via push to `/data/local/tmp` then `pm install -r -d`.
|
||||||
|
- `Mazda-EZ60/Mazda-EZ60.py` should show 7za extraction progress with stream parsing.
|
||||||
|
- Regardless of APK install failures, run post-install configuration after the install loop.
|
||||||
|
- Post-install overlays to enable:
|
||||||
|
- `com.tinnove.launcher.overlay`
|
||||||
|
- `com.tinnove.scenemode.overlay`
|
||||||
|
- `com.incall.dvr.overlay`
|
||||||
|
- Post-install packages to disable:
|
||||||
|
- `com.carinno.p1`
|
||||||
|
- `com.wtcl.electronicdirections`
|
||||||
|
- `com.ximalaya.ting.android.car`
|
||||||
|
- `com.tinnove.netease.music`
|
||||||
|
- `com.migu.miguplay.car`
|
||||||
|
- `cn.cmvideo.car.play`
|
||||||
|
- `com.tinnove.carshow`
|
||||||
|
- `com.tinnove.changba`
|
||||||
|
- `com.qiyi.video.iv`
|
||||||
|
- User cancelled the `Ctrl+Shift+E` direct extract test request for Mazda; do not add it unless asked again.
|
||||||
|
|
||||||
| Section | Contents |
|
### Q05_Lidar
|
||||||
|---------|----------|
|
|
||||||
| Title bar | Vehicle model + company subtitle |
|
|
||||||
| Password query | Hidden VIN→password lookup (S05, app-install only) |
|
|
||||||
| Button row 1 | 获取权限 (Q07/S05), 刷入语言包, 安装App, 语言设置 |
|
|
||||||
| Button row 2 | 时区设置, 安卓设置, 重启设备, 禁用升级 |
|
|
||||||
| Status bar | Connection dot, VIN, Auth status, Refresh button |
|
|
||||||
| Tips | Usage warnings |
|
|
||||||
| Progress | Extraction + push progress (hidden by default) |
|
|
||||||
| Log area | ScrolledText with tags (INFO/SUCCESS/ERROR/WARNING/CMD), 清空日志 button in title bar |
|
|
||||||
|
|
||||||
## Key features
|
- This is the Q05_Lidar-specific tool and must not be confused with any ordinary Q05 variant or package.
|
||||||
|
- Based on the shared installer visual style, but its resource structure and flashing flow are Q05_Lidar-specific.
|
||||||
|
- The first-row `获取权限` button installs `runtime.dat` -> `base.apk`, reboots to fastboot, waits for a real `fastboot devices` row like `<serial> fastboot` with a non-aggressive interval, then fetches a boot key through `POST /api/authorizations/boot-challenge` then `POST /api/authorizations/boot-key`, decrypts embedded `resource.dat`, flashes `init_boot`, immediately reboots, and deletes the temporary img. Keep the decrypted img lifetime as short as possible.
|
||||||
|
- `resource.dat` is AES-GCM encrypted and must match the server `BOOT_KEY`; the tool only accepts `data.sessionKey` from `boot-key`.
|
||||||
|
- Device fingerprint data sent to the server includes ADB serial, `ro.serialno`, `ro.boot.serialno`, manufacturer, model, device, build fingerprint, and VIN.
|
||||||
|
- `刷入语言包` installs Magisk modules, not APKs. It opens `com.topjohnwu.magisk`, warns the user to grant Shell/root permission, verifies `/debug_ramdisk/su -c "id"` returns `uid=0`, extracts `Q05_Lidar-package.bin`, then pushes module files to `/data/local/tmp/q05_lidar_modules/<MODID>/` and root-copies them into `/data/adb/modules/<MODID>`.
|
||||||
|
- `Q05_Lidar-package.bin` should unpack with module files at archive root: `module.prop`, scripts, `system/`, and `disable-wireless-adb-vecentek-magisk.zip`; do not wrap them in an outer `Q05_LIDAR_DATA/` directory.
|
||||||
|
- Q05_Lidar package cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_Q05_Lidar`; the tool cleans it on startup/extraction and on normal/atexit shutdown.
|
||||||
|
- Q05_Lidar `runtime.dat` cache is `%LOCALAPPDATA%\.cache\system\.android\apps_cache_q05_lidar_runtime`; treat it as temporary and clean stale contents before extraction.
|
||||||
|
- `Q05_Lidar-package.bin` is an external release file next to the exe because it is large. `resource.dat` is embedded in the exe; `runtime.dat` should be copied next to the exe by the pack script.
|
||||||
|
- `package-key` must include the `vehicleName` returned by `auth-check` for the VIN. The tool caches `data.vehicleName` from password query / authorization check and uses it for package-key; if missing, query `auth-check` first rather than falling back to a hardcoded Q05_Lidar value.
|
||||||
|
- All `adb shell` commands in Q05_Lidar, including Magisk launch and `/debug_ramdisk/su -c ...`, must go through `run_adb_shell()` so the tool silently sends `adb36987`.
|
||||||
|
- The `安装App` button remains the APK install path: file picker -> `adb push` -> `setprop vecentek.model 1` -> `pm install -r -d -f` -> cleanup. Do not replace it with the Magisk module flow.
|
||||||
|
- Temporary debug mode exists only for development and should be removed before release when requested. Press `Ctrl+Shift+D`; the password is verified through `POST /api/authorizations/verify-debug-mode-password`.
|
||||||
|
- In Q05_Lidar debug mode, hidden buttons appear for:
|
||||||
|
- `指纹测试`: collect and log device fingerprint fields plus local SHA256 summary.
|
||||||
|
- `解密测试`: if VIN/device is available, fetch boot key; otherwise prompt for a pasted `BOOT_KEY`/`sessionKey`, decrypt `resource.dat` locally to a temporary img, log size/SHA256, then delete it.
|
||||||
|
- `解压测试`: fetch `package-key`, extract `Q05_Lidar-package.bin`, and verify the main Magisk module plus `disable_wireless_adb_vecentek` module can be identified.
|
||||||
|
- Do not log the actual boot key/session key in debug mode.
|
||||||
|
|
||||||
### Install App (unified single/batch)
|
### Yidong/app-yidong.py
|
||||||
"安装App" button uses `filedialog.askopenfilenames` for multi-file selection, replacing old separate "单个安装" and "批量安装" buttons. Select 1 or more APKs and installs all in one pass (`adb install -r`).
|
|
||||||
|
|
||||||
### Debug mode (hidden)
|
- Uses `apps_cache_yidong`.
|
||||||
Press `Ctrl+Shift+D` → password `zxch5200` to enter debug mode:
|
- Has 7za compatibility handling for progress switches and `Incorrect command line` fallback.
|
||||||
- Bypasses device connection and authorization checks (UI shows "已连接" / "已授权")
|
- `Yidong/pack_yidong.bat` output name is ASCII: `Changan-Yidong-Language-Installer.exe`, to avoid CMD codepage issues with Chinese `NAME`.
|
||||||
- Logs all raw ADB commands and output to the log panel
|
|
||||||
- Exiting debug mode auto-refreshes real device state
|
|
||||||
|
|
||||||
### Language quick-set
|
## Build Notes
|
||||||
Popup with 8 one-click locale switches (zh-CN, en-US, ru-RU, fr-FR, es-ES, pt-BR, it-IT, ar-SA). Also links to native Android language settings. Changes take effect after reboot.
|
|
||||||
|
|
||||||
### Model-specific behaviors
|
- Pack scripts install/use `pyinstaller`, `cython`, and usually `pyzipper`.
|
||||||
|
- Cython success requires Microsoft C++ Build Tools.
|
||||||
|
- Cython success signs in logs:
|
||||||
|
- `building '_core' extension`
|
||||||
|
- `_core.cpXXX-win_amd64.pyd`
|
||||||
|
- `PYD: _core...pyd`
|
||||||
|
- output under `dist_cy\dist\...exe`
|
||||||
|
- If logs show `[WARN] Cython failed, fallback` and `[INFO] Normal PyInstaller`, the exe still builds but is normal PyInstaller and easier to reverse.
|
||||||
|
- For security-sensitive tools like Q05_Lidar, do not keep a normal PyInstaller fallback. If Cython fails or no `_core*.pyd` is generated, stop the build and show an error.
|
||||||
|
- A Cython onefile PyInstaller build should use a tiny `launcher.py` that imports `main` from compiled `_core.pyd`, and the exe archive should contain `_core*.pyd`. Confirm with PyInstaller archive viewer when in doubt.
|
||||||
|
- `UNIZ/pack_uniz.bat` and `Mazda-EZ60/pack_mazda_ez60.bat` use ASCII output names to avoid CMD encoding problems.
|
||||||
|
- Generated `.exe`, `.spec`, `build/`, `dist/`, and `dist_cy/` are build artifacts and should not be committed unless explicitly requested.
|
||||||
|
|
||||||
| Feature | Q07 (app.py) | S05 (S05.py) | X5plus (X5plusTool.py) | 逸动 (app-install/yidong) |
|
## Key Behaviors To Preserve
|
||||||
|---------|-------------|-------------|------------------------|---------------------------|
|
|
||||||
| Root perm | Y | Y | N (no btn_root) | N |
|
|
||||||
| priv-app | Y | Y | Y | N (app only) |
|
|
||||||
| ADB shell | Standard | Standard | Standard | Auto-password `adb36987` |
|
|
||||||
| VIN key | ca_vin_info/VIN | ca_vin_info/VIN | ca_vin_info/VIN | ca.car.vin |
|
|
||||||
| Old app cleanup | N | N | Y (5 packages) | N |
|
|
||||||
| Font push | N | N | Y (FZLTHPro) | N |
|
|
||||||
| Overlay enable | N | N | N | N (removed) |
|
|
||||||
| Password query | N | Y | N | Y |
|
|
||||||
| Factory hints | N | Y | Y (dynamic pwd) | Y |
|
|
||||||
|
|
||||||
## Build process
|
1. Keep original tools untouched when a fixed or model-specific copy exists.
|
||||||
|
2. Preserve VIN-based authorization for normal flashing tools.
|
||||||
|
3. Fetch `package-key` from the server instead of hardcoding package passwords.
|
||||||
|
4. Keep all shell commands in 逸动/Mazda tools behind `run_adb_shell()`.
|
||||||
|
5. Keep UNI-Z shell-free.
|
||||||
|
6. Use `run_on_ui_thread()` for all tkinter UI updates from worker threads.
|
||||||
|
7. Keep shared Android/7za binaries under root `tools/` and have pack scripts copy from there.
|
||||||
|
|
||||||
Each `pack_*.bat` follows the same pipeline:
|
## Current Local State (2026-05-28)
|
||||||
1. Install deps: `pyinstaller`, `cython`, `pyzipper`
|
|
||||||
2. Clean old build dirs
|
|
||||||
3. Cython compile: `{source}.py` → `_core.pyd`
|
|
||||||
4. Copy resources: `adb.exe`, DLLs, `app.ico`
|
|
||||||
5. PyInstaller: single `.exe` with `--uac-admin`, bundling `_core.pyd` + ADB + DLLs
|
|
||||||
6. Fallback to normal PyInstaller if Cython fails
|
|
||||||
|
|
||||||
Output exe is self-contained — bundles `adb.exe` and DLLs via `sys._MEIPASS`, no external ADB needed.
|
- `UNIZ/UNIZ.py` and `UNIZ/pack_uniz.bat` exist locally. Cython build has succeeded after installing Microsoft C++ Build Tools, producing `dist_cy\dist\UNIZ-Language-Pusher.exe`.
|
||||||
|
- `Mazda-EZ60/Mazda-EZ60.py` and `Mazda-EZ60/pack_mazda_ez60.bat` exist locally. Mazda has 7za progress extraction, unconditional post-install configuration, three overlay enables, and nine package disables.
|
||||||
|
- `Yidong/app-yidong.py` has been updated for 7za progress compatibility and `Incorrect command line` fallback.
|
||||||
|
- `Yidong/pack_yidong.bat` has been updated with quoted paths, `cd /d "%~dp0"`, and ASCII output name.
|
||||||
|
- `.gitignore` has been expanded to ignore generated exe/spec artifacts.
|
||||||
|
- There may be untracked local build outputs and generated specs; inspect `git status --ignored` before committing.
|
||||||
|
|
||||||
## Key behaviors to preserve
|
## Known Issues
|
||||||
|
|
||||||
1. **Thread safety**: Never call tkinter from background threads — always use `run_on_ui_thread`
|
- Some older tools still have minimal exception handling and bare `except: pass`.
|
||||||
2. **VIN-based auth**: Authorization required before push; flow must remain intact
|
- `test_extract.py` hardcodes a password and should not be treated as production flow.
|
||||||
3. **Silent extraction**: Auto-extract `package.bin` without user step
|
- `on_disable_upgrade` behavior is Windows/vehicle specific.
|
||||||
4. **ADB portability**: `self.adb` resolves to bundled exe path; all commands go through `run_adb_command`
|
- Pure PyInstaller fallback is easy to reverse; prefer successful Cython builds for release.
|
||||||
5. **vecentek.model**: Set to 1 before install, 0 after
|
|
||||||
6. **Chinese encoding**: `# -*- coding: utf-8 -*-` throughout
|
|
||||||
7. **Debug mode**: `self.debug_mode` flag gates auth/connection bypass and verbose logging
|
|
||||||
8. **Separate cache dirs**: Each tool extracts to its own cache folder
|
|
||||||
|
|
||||||
## Known issues
|
|
||||||
|
|
||||||
- `test_extract.py` hardcodes password — should fetch from API
|
|
||||||
- `on_disable_upgrade` uses `findstr` (Windows-specific)
|
|
||||||
- Exception handling is minimal in many places (bare `except: pass`)
|
|
||||||
- 逸动 tools (app-install/yidong) use `subprocess.Popen` with stdin password injection — fragile
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,100 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set "ROOT=%~dp0.."
|
||||||
|
set "TOOLS=%ROOT%\tools"
|
||||||
|
set "NAME=Mazda-EZ60-Language-Installer"
|
||||||
|
set "SRC=Mazda-EZ60.py"
|
||||||
|
title %NAME% - Build
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo %NAME% - Cython Build
|
||||||
|
echo ============================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
where python >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Python not found
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
for /f "delims=" %%i in ('where python') do set "PY=%%i"
|
||||||
|
echo Python: %PY%
|
||||||
|
|
||||||
|
echo [1/6] Installing deps...
|
||||||
|
"%PY%" -m pip install pyinstaller cython pyzipper -q
|
||||||
|
if errorlevel 1 (
|
||||||
|
"%PY%" -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [2/6] Clean...
|
||||||
|
if exist "dist_cy" rmdir /s /q dist_cy 2>nul
|
||||||
|
if exist "build" rmdir /s /q build 2>nul
|
||||||
|
if exist "dist" rmdir /s /q dist 2>nul
|
||||||
|
|
||||||
|
echo [3/6] Cython compile...
|
||||||
|
mkdir dist_cy 2>nul
|
||||||
|
copy "%SRC%" dist_cy\_core.py >nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [WARN] Copy source failed, fallback
|
||||||
|
goto :NORMAL
|
||||||
|
)
|
||||||
|
|
||||||
|
"%PY%" -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')"
|
||||||
|
|
||||||
|
cd dist_cy
|
||||||
|
"%PY%" setup_cython.py build_ext --inplace
|
||||||
|
if errorlevel 1 (
|
||||||
|
cd ..
|
||||||
|
echo [WARN] Cython failed, fallback
|
||||||
|
goto :NORMAL
|
||||||
|
)
|
||||||
|
|
||||||
|
for %%f in (_core*.pyd) do set PYD=%%f
|
||||||
|
if "%PYD%"=="" (
|
||||||
|
cd ..
|
||||||
|
echo [WARN] No pyd, fallback
|
||||||
|
goto :NORMAL
|
||||||
|
)
|
||||||
|
echo PYD: %PYD%
|
||||||
|
copy "%PYD%" _core.pyd >nul
|
||||||
|
|
||||||
|
"%PY%" -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
||||||
|
|
||||||
|
echo [4/6] Copy resources...
|
||||||
|
copy "%TOOLS%\adb.exe" . >nul
|
||||||
|
copy "%TOOLS%\AdbWinApi.dll" . >nul
|
||||||
|
copy "%TOOLS%\AdbWinUsbApi.dll" . >nul
|
||||||
|
copy "%TOOLS%\7za.exe" . >nul
|
||||||
|
if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul
|
||||||
|
|
||||||
|
echo [5/6] PyInstaller...
|
||||||
|
"%PY%" -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin launcher.py
|
||||||
|
if errorlevel 1 (
|
||||||
|
cd ..
|
||||||
|
echo [ERROR] PyInstaller failed
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [6/6] Cleanup...
|
||||||
|
del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul
|
||||||
|
rmdir /s /q build 2>nul
|
||||||
|
cd ..
|
||||||
|
goto :DONE
|
||||||
|
|
||||||
|
:NORMAL
|
||||||
|
echo [INFO] Normal PyInstaller...
|
||||||
|
"%PY%" -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin "%SRC%"
|
||||||
|
|
||||||
|
:DONE
|
||||||
|
echo.
|
||||||
|
echo Done.
|
||||||
|
if exist "dist_cy\dist\%NAME%.exe" (
|
||||||
|
echo Output: dist_cy\dist\%NAME%.exe
|
||||||
|
) else if exist "dist\%NAME%.exe" (
|
||||||
|
echo Output: dist\%NAME%.exe
|
||||||
|
) else (
|
||||||
|
echo Check dist folder
|
||||||
|
)
|
||||||
|
pause
|
||||||
+110
-17
@@ -22,6 +22,44 @@ except ImportError:
|
|||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_dir():
|
||||||
|
return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def resource_candidates(file_name):
|
||||||
|
base_dir = get_app_dir()
|
||||||
|
candidates = []
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / file_name)
|
||||||
|
candidates.extend([
|
||||||
|
base_dir / file_name,
|
||||||
|
base_dir / 'tools' / file_name,
|
||||||
|
base_dir / 'shared' / file_name,
|
||||||
|
base_dir.parent / 'tools' / file_name,
|
||||||
|
base_dir.parent / 'shared' / file_name,
|
||||||
|
base_dir.parent / file_name,
|
||||||
|
])
|
||||||
|
unique = []
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate not in unique:
|
||||||
|
unique.append(candidate)
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
|
def find_resource(file_name):
|
||||||
|
candidates = resource_candidates(file_name)
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate.exists():
|
||||||
|
return candidate
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
|
def find_tool(file_name, fallback=None):
|
||||||
|
path = find_resource(file_name)
|
||||||
|
if path.exists():
|
||||||
|
return str(path)
|
||||||
|
return fallback or str(path)
|
||||||
class ADKAPKGUI:
|
class ADKAPKGUI:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
@@ -199,14 +237,10 @@ class ADKAPKGUI:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 从 exe/py 所在目录查找资源文件
|
# 从 exe/py 所在目录查找资源文件
|
||||||
self.base_dir = Path(sys.executable).parent if getattr(sys, 'frozen', False) else Path(__file__).parent
|
self.base_dir = get_app_dir()
|
||||||
if getattr(sys, 'frozen', False):
|
self.adb = find_tool('adb.exe', 'adb')
|
||||||
self.adb = str(Path(sys._MEIPASS) / 'adb.exe')
|
self.sz = find_tool('7za.exe')
|
||||||
self.sz = str(Path(sys._MEIPASS) / '7za.exe')
|
self.package_file = find_resource("package.bin")
|
||||||
else:
|
|
||||||
self.adb = 'adb'
|
|
||||||
self.sz = str(self.base_dir / '7za.exe')
|
|
||||||
self.package_file = self.base_dir / "package.bin"
|
|
||||||
self.extract_password = None
|
self.extract_password = None
|
||||||
self.apps_dir = None
|
self.apps_dir = None
|
||||||
self.priv_apps_dir = None
|
self.priv_apps_dir = None
|
||||||
@@ -576,7 +610,7 @@ class ADKAPKGUI:
|
|||||||
|
|
||||||
def run_on_ui_thread(self, func, *args, **kwargs):
|
def run_on_ui_thread(self, func, *args, **kwargs):
|
||||||
"""将函数调度到主线程执行,确保线程安全"""
|
"""将函数调度到主线程执行,确保线程安全"""
|
||||||
self.root.after(0, func, *args, **kwargs)
|
self.root.after(0, lambda: func(*args, **kwargs))
|
||||||
|
|
||||||
def _adb_cmd(self):
|
def _adb_cmd(self):
|
||||||
return subprocess.list2cmdline([self.adb])
|
return subprocess.list2cmdline([self.adb])
|
||||||
@@ -821,7 +855,54 @@ class ADKAPKGUI:
|
|||||||
"""检查语言包是否已解压"""
|
"""检查语言包是否已解压"""
|
||||||
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
||||||
has_priv = self.priv_apps_dir and self.priv_apps_dir.exists() and len(list(self.priv_apps_dir.glob("*.apk"))) > 0
|
has_priv = self.priv_apps_dir and self.priv_apps_dir.exists() and len(list(self.priv_apps_dir.glob("*.apk"))) > 0
|
||||||
|
if has_app or has_priv:
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"已解压缓存无效: {reason}", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return False
|
||||||
return has_app or has_priv
|
return has_app or has_priv
|
||||||
|
|
||||||
|
def _validate_extracted_apks(self):
|
||||||
|
apks = []
|
||||||
|
if self.apps_dir and self.apps_dir.exists():
|
||||||
|
apks.extend(self.apps_dir.glob("*.apk"))
|
||||||
|
if self.priv_apps_dir and self.priv_apps_dir.exists():
|
||||||
|
apks.extend(self.priv_apps_dir.glob("*.apk"))
|
||||||
|
if not apks:
|
||||||
|
return False, "未找到可用 APK"
|
||||||
|
|
||||||
|
zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0]
|
||||||
|
if zero_apks:
|
||||||
|
preview = ", ".join(zero_apks[:5])
|
||||||
|
suffix = "..." if len(zero_apks) > 5 else ""
|
||||||
|
return False, f"发现 0KB APK: {preview}{suffix}"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
def _clear_extracted_cache(self):
|
||||||
|
if self.temp_dir and self.temp_dir.exists():
|
||||||
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.apps_dir = None
|
||||||
|
self.priv_apps_dir = None
|
||||||
|
|
||||||
|
def _format_extract_error(self, err_msg, return_code):
|
||||||
|
text = (err_msg or "").lower()
|
||||||
|
if any(marker in text for marker in (
|
||||||
|
"wrong password",
|
||||||
|
"incorrect password",
|
||||||
|
"password is incorrect",
|
||||||
|
"data error in encrypted file",
|
||||||
|
"can not open encrypted archive",
|
||||||
|
)):
|
||||||
|
return "解压密码错误,请重新确认 package.bin 密码"
|
||||||
|
if "data error" in text:
|
||||||
|
return "资源包数据错误,可能是密码错误或 package.bin 损坏"
|
||||||
|
if "headers error" in text or "unexpected end" in text:
|
||||||
|
return "资源包损坏或不完整,请检查 package.bin"
|
||||||
|
if err_msg.strip():
|
||||||
|
return f"解压失败: {err_msg.strip()[:300]}"
|
||||||
|
return f"解压失败 (返回码 {return_code}),请检查密码是否正确"
|
||||||
|
|
||||||
def _decode_7z_output(self, output):
|
def _decode_7z_output(self, output):
|
||||||
for enc in ('gbk', 'utf-8'):
|
for enc in ('gbk', 'utf-8'):
|
||||||
@@ -922,14 +1003,12 @@ class ADKAPKGUI:
|
|||||||
subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True)
|
subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True)
|
||||||
subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True)
|
subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True)
|
||||||
|
|
||||||
self.log("Preparing package...", "INFO")
|
self.log("正在准备资源包", "INFO")
|
||||||
|
|
||||||
ok, err_msg = self._extract_with_7za_progress()
|
ok, err_msg = self._extract_with_7za_progress()
|
||||||
if not ok:
|
if not ok:
|
||||||
if err_msg.strip():
|
self.log(self._format_extract_error(err_msg, 1), "ERROR")
|
||||||
self.log(f"Package preparation failed: {err_msg.strip()[:300]}", "ERROR")
|
self._clear_extracted_cache()
|
||||||
else:
|
|
||||||
self.log("Package preparation failed", "ERROR")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.apps_dir = None
|
self.apps_dir = None
|
||||||
@@ -944,10 +1023,18 @@ class ADKAPKGUI:
|
|||||||
self.priv_apps_dir = priv_app_candidates[0]
|
self.priv_apps_dir = priv_app_candidates[0]
|
||||||
|
|
||||||
if not self.apps_dir and not self.priv_apps_dir:
|
if not self.apps_dir and not self.priv_apps_dir:
|
||||||
self.log("Warning: app/priv-app directory not found", "WARNING")
|
self.log("警告:未找到 app/priv-app 目录", "WARNING")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.log("Package prepared", "SUCCESS")
|
apk_count = len(list(self.apps_dir.glob("*.apk"))) if self.apps_dir else 0
|
||||||
|
priv_count = len(list(self.priv_apps_dir.glob("*.apk"))) if self.priv_apps_dir else 0
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return False
|
||||||
|
self.log(f"资源准备完成 (app: {apk_count}, priv-app: {priv_count})", "SUCCESS")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -956,7 +1043,8 @@ class ADKAPKGUI:
|
|||||||
import traceback
|
import traceback
|
||||||
self.log(traceback.format_exc(), "ERROR")
|
self.log(traceback.format_exc(), "ERROR")
|
||||||
else:
|
else:
|
||||||
self.log("Package preparation failed, please check network and retry", "ERROR")
|
self.log("资源准备失败,请检查网络连接后重试", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_environment(self):
|
def check_environment(self):
|
||||||
@@ -999,6 +1087,11 @@ class ADKAPKGUI:
|
|||||||
if has_priv:
|
if has_priv:
|
||||||
self.priv_apps_dir = priv_candidates[0]
|
self.priv_apps_dir = priv_candidates[0]
|
||||||
self.temp_dir = cache_dir
|
self.temp_dir = cache_dir
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"缓存资源无效,已清理: {reason}", "WARNING")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return
|
||||||
# self.log("已复用缓存的资源文件", "INFO")
|
# self.log("已复用缓存的资源文件", "INFO")
|
||||||
|
|
||||||
def refresh_device_status(self):
|
def refresh_device_status(self):
|
||||||
+92
-89
@@ -1,92 +1,95 @@
|
|||||||
@echo off
|
@echo off
|
||||||
chcp 65001 >nul
|
chcp 65001 >nul
|
||||||
set NAME=启源Q07刷入工具
|
cd /d "%~dp0"
|
||||||
title %NAME% - Build
|
set "ROOT=%~dp0.."
|
||||||
|
set "TOOLS=%ROOT%\tools"
|
||||||
echo ============================================================
|
set NAME=启源Q07刷入工具
|
||||||
echo %NAME% - Cython Build
|
title %NAME% - Build
|
||||||
echo ============================================================
|
|
||||||
echo.
|
echo ============================================================
|
||||||
|
echo %NAME% - Cython Build
|
||||||
where python >nul 2>&1
|
echo ============================================================
|
||||||
if errorlevel 1 (
|
echo.
|
||||||
echo [ERROR] Python not found
|
|
||||||
pause
|
where python >nul 2>&1
|
||||||
exit /b
|
if errorlevel 1 (
|
||||||
)
|
echo [ERROR] Python not found
|
||||||
for /f "delims=" %%i in ('where python') do set PY=%%i
|
pause
|
||||||
echo Python: %PY%
|
exit /b
|
||||||
|
)
|
||||||
echo [1/6] Installing deps...
|
for /f "delims=" %%i in ('where python') do set PY=%%i
|
||||||
%PY% -m pip install pyinstaller cython pyzipper -q
|
echo Python: %PY%
|
||||||
if errorlevel 1 (
|
|
||||||
%PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple
|
echo [1/6] Installing deps...
|
||||||
)
|
%PY% -m pip install pyinstaller cython pyzipper -q
|
||||||
|
if errorlevel 1 (
|
||||||
echo [2/6] Clean...
|
%PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
if exist "dist_cy" rmdir /s /q dist_cy 2>nul
|
)
|
||||||
if exist "build" rmdir /s /q build 2>nul
|
|
||||||
if exist "dist" rmdir /s /q dist 2>nul
|
echo [2/6] Clean...
|
||||||
|
if exist "dist_cy" rmdir /s /q dist_cy 2>nul
|
||||||
echo [3/6] Cython compile...
|
if exist "build" rmdir /s /q build 2>nul
|
||||||
mkdir dist_cy 2>nul
|
if exist "dist" rmdir /s /q dist 2>nul
|
||||||
copy app.py dist_cy\_core.py >nul
|
|
||||||
|
echo [3/6] Cython compile...
|
||||||
%PY% -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')"
|
mkdir dist_cy 2>nul
|
||||||
|
copy app.py dist_cy\_core.py >nul
|
||||||
cd dist_cy
|
|
||||||
%PY% setup_cython.py build_ext --inplace
|
%PY% -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')"
|
||||||
if errorlevel 1 (
|
|
||||||
cd ..
|
cd dist_cy
|
||||||
echo [WARN] Cython failed, fallback
|
%PY% setup_cython.py build_ext --inplace
|
||||||
goto :NORMAL
|
if errorlevel 1 (
|
||||||
)
|
cd ..
|
||||||
|
echo [WARN] Cython failed, fallback
|
||||||
for %%f in (_core*.pyd) do set PYD=%%f
|
goto :NORMAL
|
||||||
if "%PYD%"=="" (
|
)
|
||||||
cd ..
|
|
||||||
echo [WARN] No pyd, fallback
|
for %%f in (_core*.pyd) do set PYD=%%f
|
||||||
goto :NORMAL
|
if "%PYD%"=="" (
|
||||||
)
|
cd ..
|
||||||
echo PYD: %PYD%
|
echo [WARN] No pyd, fallback
|
||||||
copy "%PYD%" _core.pyd >nul
|
goto :NORMAL
|
||||||
|
)
|
||||||
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
echo PYD: %PYD%
|
||||||
|
copy "%PYD%" _core.pyd >nul
|
||||||
echo [4/6] Copy resources...
|
|
||||||
copy ..\adb.exe . >nul
|
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
||||||
copy ..\AdbWinApi.dll . >nul
|
|
||||||
copy ..\AdbWinUsbApi.dll . >nul
|
echo [4/6] Copy resources...
|
||||||
if exist "..\app.ico" copy "..\app.ico" . >nul
|
copy "%TOOLS%\adb.exe" . >nul
|
||||||
|
copy "%TOOLS%\AdbWinApi.dll" . >nul
|
||||||
|
copy "%TOOLS%\AdbWinUsbApi.dll" . >nul
|
||||||
|
copy "%TOOLS%\7za.exe" . >nul
|
||||||
|
if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul
|
||||||
|
|
||||||
echo [5/6] PyInstaller...
|
echo [5/6] PyInstaller...
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin launcher.py
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin launcher.py
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
cd ..
|
cd ..
|
||||||
echo [ERROR] PyInstaller failed
|
echo [ERROR] PyInstaller failed
|
||||||
pause
|
pause
|
||||||
exit /b
|
exit /b
|
||||||
)
|
)
|
||||||
|
|
||||||
echo [6/6] Cleanup...
|
echo [6/6] Cleanup...
|
||||||
del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul
|
del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul
|
||||||
rmdir /s /q build 2>nul
|
rmdir /s /q build 2>nul
|
||||||
cd ..
|
cd ..
|
||||||
goto :DONE
|
goto :DONE
|
||||||
|
|
||||||
:NORMAL
|
:NORMAL
|
||||||
cd /d "%~dp0"
|
|
||||||
echo [INFO] Normal PyInstaller...
|
echo [INFO] Normal PyInstaller...
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin app.py
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin app.py
|
||||||
|
|
||||||
:DONE
|
:DONE
|
||||||
echo.
|
echo.
|
||||||
echo Done.
|
echo Done.
|
||||||
if exist "dist_cy\dist\%NAME%.exe" (
|
if exist "dist_cy\dist\%NAME%.exe" (
|
||||||
echo Output: dist_cy\dist\%NAME%.exe
|
echo Output: dist_cy\dist\%NAME%.exe
|
||||||
) else if exist "dist\%NAME%.exe" (
|
) else if exist "dist\%NAME%.exe" (
|
||||||
echo Output: dist\%NAME%.exe
|
echo Output: dist\%NAME%.exe
|
||||||
) else (
|
) else (
|
||||||
echo Check dist folder
|
echo Check dist folder
|
||||||
)
|
)
|
||||||
pause
|
pause
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
@echo off
|
@echo off
|
||||||
chcp 65001 >nul
|
chcp 65001 >nul
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set "ROOT=%~dp0.."
|
||||||
|
set "TOOLS=%ROOT%\tools"
|
||||||
set NAME=启源Q07刷入工具
|
set NAME=启源Q07刷入工具
|
||||||
set SRC=app.py
|
set SRC=app.py
|
||||||
title %NAME% - Build
|
title %NAME% - Build
|
||||||
@@ -55,14 +58,14 @@ copy "%PYD%" _core.pyd >nul
|
|||||||
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
||||||
|
|
||||||
echo [4/6] Copy resources...
|
echo [4/6] Copy resources...
|
||||||
copy ..\adb.exe . >nul
|
copy "%TOOLS%\adb.exe" . >nul
|
||||||
copy ..\AdbWinApi.dll . >nul
|
copy "%TOOLS%\AdbWinApi.dll" . >nul
|
||||||
copy ..\AdbWinUsbApi.dll . >nul
|
copy "%TOOLS%\AdbWinUsbApi.dll" . >nul
|
||||||
copy ..\7za.exe . >nul
|
copy "%TOOLS%\7za.exe" . >nul
|
||||||
if exist "..\app.ico" copy "..\app.ico" . >nul
|
if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul
|
||||||
|
|
||||||
echo [5/6] PyInstaller...
|
echo [5/6] PyInstaller...
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin launcher.py
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin launcher.py
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
cd ..
|
cd ..
|
||||||
echo [ERROR] PyInstaller failed
|
echo [ERROR] PyInstaller failed
|
||||||
@@ -77,9 +80,8 @@ cd ..
|
|||||||
goto :DONE
|
goto :DONE
|
||||||
|
|
||||||
:NORMAL
|
:NORMAL
|
||||||
cd /d "%~dp0"
|
|
||||||
echo [INFO] Normal PyInstaller...
|
echo [INFO] Normal PyInstaller...
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin %SRC%
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin %SRC%
|
||||||
|
|
||||||
:DONE
|
:DONE
|
||||||
echo.
|
echo.
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
# language_installer
|
# language_installer
|
||||||
|
|
||||||
长安语言刷入工具 — 适用于启源Q07、深蓝S05、X5plus、长安逸动等多车型的Android车机多语言APK刷入工具套件。
|
长安语言刷入工具套件,适用于启源Q07、深蓝S05、X5plus、长安逸动、UNI-Z、Mazda-EZ60 等 Android 车机多语言 APK 刷入/推送场景。
|
||||||
|
|
||||||
|
## 常用入口
|
||||||
|
|
||||||
|
| 文件 | 用途 | 打包脚本 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `Q07/app.py` | 启源Q07 | `Q07/pack_q07.bat` |
|
||||||
|
| `S05/S05.py` | 深蓝S05原版 | `S05/pack_s05.bat` |
|
||||||
|
| `S05/S05_fixed.py` | 深蓝S05修复版 | `S05/pack_s05_fixed.bat` |
|
||||||
|
| `X5plus/X5plusTool.py` | X5plus | `X5plus/pack_x5plus.bat` |
|
||||||
|
| `Yidong/app-install.py` | 长安逸动通用 | `Yidong/pack_common.bat` |
|
||||||
|
| `Yidong/app-yidong.py` | 长安逸动 | `Yidong/pack_yidong.bat` |
|
||||||
|
| `UNIZ/UNIZ.py` | UNI-Z 文件推送 | `UNIZ/pack_uniz.bat` |
|
||||||
|
| `Mazda-EZ60/Mazda-EZ60.py` | Mazda-EZ60 OS 1.2 | `Mazda-EZ60/pack_mazda_ez60.bat` |
|
||||||
|
| `A07/Qiyuan_A07_Multi-lan-installer.py` | 启源A07 | `A07/pack_a07.bat` |
|
||||||
|
| `Q05-Lidar/Q05-Lidar_Installer.py` | Q05_Lidar | `Q05-Lidar/pack_q05_lidar.bat` |
|
||||||
|
|
||||||
|
公共工具如 `adb.exe`、`fastboot.exe`、`7za.exe` 及相关 DLL 统一放在根目录 `tools/` 管理;`app.ico`、`package.bin` 保留在仓库根目录。各车型脚本会自动回退查找这些资源。
|
||||||
|
|
||||||
|
详细维护说明见 `AGENTS.md` / `CLAUDE.md`。
|
||||||
|
|||||||
+60
-4
@@ -785,8 +785,54 @@ class ADKAPKGUI:
|
|||||||
"""检查语言包是否已解压"""
|
"""检查语言包是否已解压"""
|
||||||
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
||||||
has_priv = self.priv_apps_dir and self.priv_apps_dir.exists() and len(list(self.priv_apps_dir.glob("*.apk"))) > 0
|
has_priv = self.priv_apps_dir and self.priv_apps_dir.exists() and len(list(self.priv_apps_dir.glob("*.apk"))) > 0
|
||||||
|
if has_app or has_priv:
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"已解压缓存无效: {reason}", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return False
|
||||||
return has_app or has_priv
|
return has_app or has_priv
|
||||||
|
|
||||||
|
def _validate_extracted_apks(self):
|
||||||
|
apks = []
|
||||||
|
if self.apps_dir and self.apps_dir.exists():
|
||||||
|
apks.extend(self.apps_dir.glob("*.apk"))
|
||||||
|
if self.priv_apps_dir and self.priv_apps_dir.exists():
|
||||||
|
apks.extend(self.priv_apps_dir.glob("*.apk"))
|
||||||
|
if not apks:
|
||||||
|
return False, "未找到可用 APK"
|
||||||
|
zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0]
|
||||||
|
if zero_apks:
|
||||||
|
preview = ", ".join(zero_apks[:5])
|
||||||
|
suffix = "..." if len(zero_apks) > 5 else ""
|
||||||
|
return False, f"发现 0KB APK: {preview}{suffix}"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
def _clear_extracted_cache(self):
|
||||||
|
if self.temp_dir and self.temp_dir.exists():
|
||||||
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.apps_dir = None
|
||||||
|
self.priv_apps_dir = None
|
||||||
|
|
||||||
|
def _format_extract_error(self, err_msg, return_code):
|
||||||
|
text = (err_msg or "").lower()
|
||||||
|
if any(marker in text for marker in (
|
||||||
|
"wrong password",
|
||||||
|
"incorrect password",
|
||||||
|
"password is incorrect",
|
||||||
|
"data error in encrypted file",
|
||||||
|
"can not open encrypted archive",
|
||||||
|
)):
|
||||||
|
return "解压密码错误,请重新确认 package.bin 密码"
|
||||||
|
if "data error" in text:
|
||||||
|
return "资源包数据错误,可能是密码错误或 package.bin 损坏"
|
||||||
|
if "headers error" in text or "unexpected end" in text:
|
||||||
|
return "资源包损坏或不完整,请检查 package.bin"
|
||||||
|
if err_msg.strip():
|
||||||
|
return f"解压失败: {err_msg.strip()[:300]}"
|
||||||
|
return f"解压失败 (返回码 {return_code}),请检查密码是否正确"
|
||||||
|
|
||||||
def extract_package_silent(self):
|
def extract_package_silent(self):
|
||||||
"""静默解压语言包(带进度)"""
|
"""静默解压语言包(带进度)"""
|
||||||
if not self.package_file.exists():
|
if not self.package_file.exists():
|
||||||
@@ -843,10 +889,8 @@ class ADKAPKGUI:
|
|||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
if err_msg.strip():
|
self.log(self._format_extract_error(err_msg, result.returncode), "ERROR")
|
||||||
self.log(f"解压失败: {err_msg.strip()[:300]}", "ERROR")
|
self._clear_extracted_cache()
|
||||||
else:
|
|
||||||
self.log(f"解压失败 (返回码: {result.returncode}),请检查密码是否正确", "ERROR")
|
|
||||||
return False
|
return False
|
||||||
self.update_progress(1, 1, "资源加载完成")
|
self.update_progress(1, 1, "资源加载完成")
|
||||||
|
|
||||||
@@ -864,10 +908,16 @@ class ADKAPKGUI:
|
|||||||
|
|
||||||
if not self.apps_dir and not self.priv_apps_dir:
|
if not self.apps_dir and not self.priv_apps_dir:
|
||||||
self.log("警告:未找到 app/priv-app 目录", "WARNING")
|
self.log("警告:未找到 app/priv-app 目录", "WARNING")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
apk_count = len(list(self.apps_dir.glob("*.apk"))) if self.apps_dir else 0
|
apk_count = len(list(self.apps_dir.glob("*.apk"))) if self.apps_dir else 0
|
||||||
priv_count = len(list(self.priv_apps_dir.glob("*.apk"))) if self.priv_apps_dir else 0
|
priv_count = len(list(self.priv_apps_dir.glob("*.apk"))) if self.priv_apps_dir else 0
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return False
|
||||||
self.log(f"资源准备完成 (app: {apk_count}, priv-app: {priv_count})", "SUCCESS")
|
self.log(f"资源准备完成 (app: {apk_count}, priv-app: {priv_count})", "SUCCESS")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -878,6 +928,7 @@ class ADKAPKGUI:
|
|||||||
self.log(traceback.format_exc(), "ERROR")
|
self.log(traceback.format_exc(), "ERROR")
|
||||||
else:
|
else:
|
||||||
self.log("资源准备失败,请检查网络连接后重试", "ERROR")
|
self.log("资源准备失败,请检查网络连接后重试", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_environment(self):
|
def check_environment(self):
|
||||||
@@ -920,6 +971,11 @@ class ADKAPKGUI:
|
|||||||
if has_priv:
|
if has_priv:
|
||||||
self.priv_apps_dir = priv_candidates[0]
|
self.priv_apps_dir = priv_candidates[0]
|
||||||
self.temp_dir = cache_dir
|
self.temp_dir = cache_dir
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"缓存资源无效,已清理: {reason}", "WARNING")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return
|
||||||
# self.log("已复用缓存的资源文件", "INFO")
|
# self.log("已复用缓存的资源文件", "INFO")
|
||||||
|
|
||||||
def refresh_device_status(self):
|
def refresh_device_status(self):
|
||||||
+102
-12
@@ -22,6 +22,44 @@ except ImportError:
|
|||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_dir():
|
||||||
|
return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def resource_candidates(file_name):
|
||||||
|
base_dir = get_app_dir()
|
||||||
|
candidates = []
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / file_name)
|
||||||
|
candidates.extend([
|
||||||
|
base_dir / file_name,
|
||||||
|
base_dir / 'tools' / file_name,
|
||||||
|
base_dir / 'shared' / file_name,
|
||||||
|
base_dir.parent / 'tools' / file_name,
|
||||||
|
base_dir.parent / 'shared' / file_name,
|
||||||
|
base_dir.parent / file_name,
|
||||||
|
])
|
||||||
|
unique = []
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate not in unique:
|
||||||
|
unique.append(candidate)
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
|
def find_resource(file_name):
|
||||||
|
candidates = resource_candidates(file_name)
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate.exists():
|
||||||
|
return candidate
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
|
def find_tool(file_name, fallback=None):
|
||||||
|
path = find_resource(file_name)
|
||||||
|
if path.exists():
|
||||||
|
return str(path)
|
||||||
|
return fallback or str(path)
|
||||||
class ADKAPKGUI:
|
class ADKAPKGUI:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
@@ -131,14 +169,10 @@ class ADKAPKGUI:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 从 exe/py 所在目录查找资源文件
|
# 从 exe/py 所在目录查找资源文件
|
||||||
self.base_dir = Path(sys.executable).parent if getattr(sys, 'frozen', False) else Path(__file__).parent
|
self.base_dir = get_app_dir()
|
||||||
if getattr(sys, 'frozen', False):
|
self.adb = find_tool('adb.exe', 'adb')
|
||||||
self.adb = str(Path(sys._MEIPASS) / 'adb.exe')
|
self.sz = find_tool('7za.exe')
|
||||||
self.sz = str(Path(sys._MEIPASS) / '7za.exe')
|
self.package_file = find_resource("package.bin")
|
||||||
else:
|
|
||||||
self.adb = 'adb'
|
|
||||||
self.sz = str(self.base_dir / '7za.exe')
|
|
||||||
self.package_file = self.base_dir / "package.bin"
|
|
||||||
self.extract_password = None
|
self.extract_password = None
|
||||||
self.apps_dir = None
|
self.apps_dir = None
|
||||||
self.priv_apps_dir = None
|
self.priv_apps_dir = None
|
||||||
@@ -792,8 +826,54 @@ class ADKAPKGUI:
|
|||||||
"""检查语言包是否已解压"""
|
"""检查语言包是否已解压"""
|
||||||
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
||||||
has_priv = self.priv_apps_dir and self.priv_apps_dir.exists() and len(list(self.priv_apps_dir.glob("*.apk"))) > 0
|
has_priv = self.priv_apps_dir and self.priv_apps_dir.exists() and len(list(self.priv_apps_dir.glob("*.apk"))) > 0
|
||||||
|
if has_app or has_priv:
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"已解压缓存无效: {reason}", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return False
|
||||||
return has_app or has_priv
|
return has_app or has_priv
|
||||||
|
|
||||||
|
def _validate_extracted_apks(self):
|
||||||
|
apks = []
|
||||||
|
if self.apps_dir and self.apps_dir.exists():
|
||||||
|
apks.extend(self.apps_dir.glob("*.apk"))
|
||||||
|
if self.priv_apps_dir and self.priv_apps_dir.exists():
|
||||||
|
apks.extend(self.priv_apps_dir.glob("*.apk"))
|
||||||
|
if not apks:
|
||||||
|
return False, "未找到可用 APK"
|
||||||
|
zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0]
|
||||||
|
if zero_apks:
|
||||||
|
preview = ", ".join(zero_apks[:5])
|
||||||
|
suffix = "..." if len(zero_apks) > 5 else ""
|
||||||
|
return False, f"发现 0KB APK: {preview}{suffix}"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
def _clear_extracted_cache(self):
|
||||||
|
if self.temp_dir and self.temp_dir.exists():
|
||||||
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.apps_dir = None
|
||||||
|
self.priv_apps_dir = None
|
||||||
|
|
||||||
|
def _format_extract_error(self, err_msg):
|
||||||
|
text = (err_msg or "").lower()
|
||||||
|
if any(marker in text for marker in (
|
||||||
|
"wrong password",
|
||||||
|
"incorrect password",
|
||||||
|
"password is incorrect",
|
||||||
|
"data error in encrypted file",
|
||||||
|
"can not open encrypted archive",
|
||||||
|
)):
|
||||||
|
return "解压密码错误,请重新确认 package.bin 密码"
|
||||||
|
if "data error" in text:
|
||||||
|
return "资源包数据错误,可能是密码错误或 package.bin 损坏"
|
||||||
|
if "headers error" in text or "unexpected end" in text:
|
||||||
|
return "资源包损坏或不完整,请检查 package.bin"
|
||||||
|
if err_msg.strip():
|
||||||
|
return f"资源准备失败: {err_msg.strip()[:300]}"
|
||||||
|
return "资源准备失败,请检查解压密码是否正确"
|
||||||
|
|
||||||
def _decode_7z_output(self, output):
|
def _decode_7z_output(self, output):
|
||||||
"""解码 7za 输出,兼容中文 Windows 控制台编码"""
|
"""解码 7za 输出,兼容中文 Windows 控制台编码"""
|
||||||
for enc in ('gbk', 'utf-8'):
|
for enc in ('gbk', 'utf-8'):
|
||||||
@@ -903,10 +983,8 @@ class ADKAPKGUI:
|
|||||||
|
|
||||||
ok, err_msg = self._extract_with_7za_progress()
|
ok, err_msg = self._extract_with_7za_progress()
|
||||||
if not ok:
|
if not ok:
|
||||||
if err_msg.strip():
|
self.log(self._format_extract_error(err_msg), "ERROR")
|
||||||
self.log(f"资源准备失败: {err_msg.strip()[:300]}", "ERROR")
|
self._clear_extracted_cache()
|
||||||
else:
|
|
||||||
self.log("资源准备失败", "ERROR")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 查找app和priv-app目录
|
# 查找app和priv-app目录
|
||||||
@@ -923,10 +1001,16 @@ class ADKAPKGUI:
|
|||||||
|
|
||||||
if not self.apps_dir and not self.priv_apps_dir:
|
if not self.apps_dir and not self.priv_apps_dir:
|
||||||
self.log("警告:未找到对应目录", "WARNING")
|
self.log("警告:未找到对应目录", "WARNING")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
apk_count = len(list(self.apps_dir.glob("*.apk"))) if self.apps_dir else 0
|
apk_count = len(list(self.apps_dir.glob("*.apk"))) if self.apps_dir else 0
|
||||||
priv_count = len(list(self.priv_apps_dir.glob("*.apk"))) if self.priv_apps_dir else 0
|
priv_count = len(list(self.priv_apps_dir.glob("*.apk"))) if self.priv_apps_dir else 0
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return False
|
||||||
self.log(f"资源准备完成", "SUCCESS")
|
self.log(f"资源准备完成", "SUCCESS")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -937,6 +1021,7 @@ class ADKAPKGUI:
|
|||||||
self.log(traceback.format_exc(), "ERROR")
|
self.log(traceback.format_exc(), "ERROR")
|
||||||
else:
|
else:
|
||||||
self.log("资源准备失败,请检查网络连接后重试", "ERROR")
|
self.log("资源准备失败,请检查网络连接后重试", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_environment(self):
|
def check_environment(self):
|
||||||
@@ -979,6 +1064,11 @@ class ADKAPKGUI:
|
|||||||
if has_priv:
|
if has_priv:
|
||||||
self.priv_apps_dir = priv_candidates[0]
|
self.priv_apps_dir = priv_candidates[0]
|
||||||
self.temp_dir = cache_dir
|
self.temp_dir = cache_dir
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"缓存资源无效,已清理: {reason}", "WARNING")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return
|
||||||
# self.log("已复用缓存的资源文件", "INFO")
|
# self.log("已复用缓存的资源文件", "INFO")
|
||||||
|
|
||||||
def refresh_device_status(self):
|
def refresh_device_status(self):
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
@echo off
|
@echo off
|
||||||
chcp 65001 >nul
|
chcp 65001 >nul
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set "ROOT=%~dp0.."
|
||||||
|
set "TOOLS=%ROOT%\tools"
|
||||||
set NAME=深蓝S05刷入工具
|
set NAME=深蓝S05刷入工具
|
||||||
set SRC=S05.py
|
set SRC=S05.py
|
||||||
title %NAME% - Build
|
title %NAME% - Build
|
||||||
@@ -55,14 +58,14 @@ copy "%PYD%" _core.pyd >nul
|
|||||||
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
||||||
|
|
||||||
echo [4/6] Copy resources...
|
echo [4/6] Copy resources...
|
||||||
copy ..\adb.exe . >nul
|
copy "%TOOLS%\adb.exe" . >nul
|
||||||
copy ..\AdbWinApi.dll . >nul
|
copy "%TOOLS%\AdbWinApi.dll" . >nul
|
||||||
copy ..\AdbWinUsbApi.dll . >nul
|
copy "%TOOLS%\AdbWinUsbApi.dll" . >nul
|
||||||
copy ..\7za.exe . >nul
|
copy "%TOOLS%\7za.exe" . >nul
|
||||||
if exist "..\app.ico" copy "..\app.ico" . >nul
|
if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul
|
||||||
|
|
||||||
echo [5/6] PyInstaller...
|
echo [5/6] PyInstaller...
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin launcher.py
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin launcher.py
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
cd ..
|
cd ..
|
||||||
echo [ERROR] PyInstaller failed
|
echo [ERROR] PyInstaller failed
|
||||||
@@ -77,9 +80,8 @@ cd ..
|
|||||||
goto :DONE
|
goto :DONE
|
||||||
|
|
||||||
:NORMAL
|
:NORMAL
|
||||||
cd /d "%~dp0"
|
|
||||||
echo [INFO] Normal PyInstaller...
|
echo [INFO] Normal PyInstaller...
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin %SRC%
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin %SRC%
|
||||||
|
|
||||||
:DONE
|
:DONE
|
||||||
echo.
|
echo.
|
||||||
@@ -1,94 +1,96 @@
|
|||||||
@echo off
|
@echo off
|
||||||
chcp 65001 >nul
|
chcp 65001 >nul
|
||||||
set NAME=深蓝S05刷入工具_fixed
|
cd /d "%~dp0"
|
||||||
set SRC=S05_fixed.py
|
set "ROOT=%~dp0.."
|
||||||
title %NAME% - Build
|
set "TOOLS=%ROOT%\tools"
|
||||||
|
set NAME=深蓝S05刷入工具_fixed
|
||||||
echo ============================================================
|
set SRC=S05_fixed.py
|
||||||
echo %NAME% - Cython Build
|
title %NAME% - Build
|
||||||
echo ============================================================
|
|
||||||
echo.
|
echo ============================================================
|
||||||
|
echo %NAME% - Cython Build
|
||||||
where python >nul 2>&1
|
echo ============================================================
|
||||||
if errorlevel 1 (
|
echo.
|
||||||
echo [ERROR] Python not found
|
|
||||||
pause
|
where python >nul 2>&1
|
||||||
exit /b
|
if errorlevel 1 (
|
||||||
)
|
echo [ERROR] Python not found
|
||||||
for /f "delims=" %%i in ('where python') do set PY=%%i
|
pause
|
||||||
echo Python: %PY%
|
exit /b
|
||||||
|
)
|
||||||
echo [1/6] Installing deps...
|
for /f "delims=" %%i in ('where python') do set PY=%%i
|
||||||
%PY% -m pip install pyinstaller cython pyzipper -q
|
echo Python: %PY%
|
||||||
if errorlevel 1 (
|
|
||||||
%PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple
|
echo [1/6] Installing deps...
|
||||||
)
|
%PY% -m pip install pyinstaller cython pyzipper -q
|
||||||
|
if errorlevel 1 (
|
||||||
echo [2/6] Clean...
|
%PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
if exist "dist_cy" rmdir /s /q dist_cy 2>nul
|
)
|
||||||
if exist "build" rmdir /s /q build 2>nul
|
|
||||||
if exist "dist" rmdir /s /q dist 2>nul
|
echo [2/6] Clean...
|
||||||
|
if exist "dist_cy" rmdir /s /q dist_cy 2>nul
|
||||||
echo [3/6] Cython compile...
|
if exist "build" rmdir /s /q build 2>nul
|
||||||
mkdir dist_cy 2>nul
|
if exist "dist" rmdir /s /q dist 2>nul
|
||||||
copy %SRC% dist_cy\_core.py >nul
|
|
||||||
|
echo [3/6] Cython compile...
|
||||||
%PY% -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')"
|
mkdir dist_cy 2>nul
|
||||||
|
copy %SRC% dist_cy\_core.py >nul
|
||||||
cd dist_cy
|
|
||||||
%PY% setup_cython.py build_ext --inplace
|
%PY% -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')"
|
||||||
if errorlevel 1 (
|
|
||||||
cd ..
|
cd dist_cy
|
||||||
echo [WARN] Cython failed, fallback
|
%PY% setup_cython.py build_ext --inplace
|
||||||
goto :NORMAL
|
if errorlevel 1 (
|
||||||
)
|
cd ..
|
||||||
|
echo [WARN] Cython failed, fallback
|
||||||
for %%f in (_core*.pyd) do set PYD=%%f
|
goto :NORMAL
|
||||||
if "%PYD%"=="" (
|
)
|
||||||
cd ..
|
|
||||||
echo [WARN] No pyd, fallback
|
for %%f in (_core*.pyd) do set PYD=%%f
|
||||||
goto :NORMAL
|
if "%PYD%"=="" (
|
||||||
)
|
cd ..
|
||||||
echo PYD: %PYD%
|
echo [WARN] No pyd, fallback
|
||||||
copy "%PYD%" _core.pyd >nul
|
goto :NORMAL
|
||||||
|
)
|
||||||
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
echo PYD: %PYD%
|
||||||
|
copy "%PYD%" _core.pyd >nul
|
||||||
echo [4/6] Copy resources...
|
|
||||||
copy ..\adb.exe . >nul
|
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
||||||
copy ..\AdbWinApi.dll . >nul
|
|
||||||
copy ..\AdbWinUsbApi.dll . >nul
|
echo [4/6] Copy resources...
|
||||||
copy ..\7za.exe . >nul
|
copy "%TOOLS%\adb.exe" . >nul
|
||||||
if exist "..\app.ico" copy "..\app.ico" . >nul
|
copy "%TOOLS%\AdbWinApi.dll" . >nul
|
||||||
|
copy "%TOOLS%\AdbWinUsbApi.dll" . >nul
|
||||||
echo [5/6] PyInstaller...
|
copy "%TOOLS%\7za.exe" . >nul
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin launcher.py
|
if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul
|
||||||
if errorlevel 1 (
|
|
||||||
cd ..
|
echo [5/6] PyInstaller...
|
||||||
echo [ERROR] PyInstaller failed
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin launcher.py
|
||||||
pause
|
if errorlevel 1 (
|
||||||
exit /b
|
cd ..
|
||||||
)
|
echo [ERROR] PyInstaller failed
|
||||||
|
pause
|
||||||
echo [6/6] Cleanup...
|
exit /b
|
||||||
del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul
|
)
|
||||||
rmdir /s /q build 2>nul
|
|
||||||
cd ..
|
echo [6/6] Cleanup...
|
||||||
goto :DONE
|
del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul
|
||||||
|
rmdir /s /q build 2>nul
|
||||||
:NORMAL
|
cd ..
|
||||||
cd /d "%~dp0"
|
goto :DONE
|
||||||
echo [INFO] Normal PyInstaller...
|
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin %SRC%
|
:NORMAL
|
||||||
|
echo [INFO] Normal PyInstaller...
|
||||||
:DONE
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin %SRC%
|
||||||
echo.
|
|
||||||
echo Done.
|
:DONE
|
||||||
if exist "dist_cy\dist\%NAME%.exe" (
|
echo.
|
||||||
echo Output: dist_cy\dist\%NAME%.exe
|
echo Done.
|
||||||
) else if exist "dist\%NAME%.exe" (
|
if exist "dist_cy\dist\%NAME%.exe" (
|
||||||
echo Output: dist\%NAME%.exe
|
echo Output: dist_cy\dist\%NAME%.exe
|
||||||
) else (
|
) else if exist "dist\%NAME%.exe" (
|
||||||
echo Check dist folder
|
echo Output: dist\%NAME%.exe
|
||||||
)
|
) else (
|
||||||
pause
|
echo Check dist folder
|
||||||
|
)
|
||||||
|
pause
|
||||||
+1212
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,100 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set "ROOT=%~dp0.."
|
||||||
|
set "TOOLS=%ROOT%\tools"
|
||||||
|
set "NAME=UNIZ-Language-Pusher"
|
||||||
|
set "SRC=UNIZ.py"
|
||||||
|
title %NAME% - Build
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo %NAME% - Cython Build
|
||||||
|
echo ============================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
where python >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Python not found
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
for /f "delims=" %%i in ('where python') do set PY=%%i
|
||||||
|
echo Python: %PY%
|
||||||
|
|
||||||
|
echo [1/6] Installing deps...
|
||||||
|
"%PY%" -m pip install pyinstaller cython -q
|
||||||
|
if errorlevel 1 (
|
||||||
|
"%PY%" -m pip install pyinstaller cython -q -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [2/6] Clean...
|
||||||
|
if exist "dist_cy" rmdir /s /q dist_cy 2>nul
|
||||||
|
if exist "build" rmdir /s /q build 2>nul
|
||||||
|
if exist "dist" rmdir /s /q dist 2>nul
|
||||||
|
|
||||||
|
echo [3/6] Cython compile...
|
||||||
|
mkdir dist_cy 2>nul
|
||||||
|
copy "%SRC%" "dist_cy\_core.py" >nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [WARN] Copy source failed, fallback
|
||||||
|
goto :NORMAL
|
||||||
|
)
|
||||||
|
|
||||||
|
"%PY%" -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')"
|
||||||
|
|
||||||
|
cd dist_cy
|
||||||
|
"%PY%" setup_cython.py build_ext --inplace
|
||||||
|
if errorlevel 1 (
|
||||||
|
cd ..
|
||||||
|
echo [WARN] Cython failed, fallback
|
||||||
|
goto :NORMAL
|
||||||
|
)
|
||||||
|
|
||||||
|
for %%f in (_core*.pyd) do set PYD=%%f
|
||||||
|
if "%PYD%"=="" (
|
||||||
|
cd ..
|
||||||
|
echo [WARN] No pyd, fallback
|
||||||
|
goto :NORMAL
|
||||||
|
)
|
||||||
|
echo PYD: %PYD%
|
||||||
|
copy "%PYD%" _core.pyd >nul
|
||||||
|
|
||||||
|
"%PY%" -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
||||||
|
|
||||||
|
echo [4/6] Copy resources...
|
||||||
|
copy "%TOOLS%\adb.exe" . >nul
|
||||||
|
copy "%TOOLS%\AdbWinApi.dll" . >nul
|
||||||
|
copy "%TOOLS%\AdbWinUsbApi.dll" . >nul
|
||||||
|
copy "%TOOLS%\7za.exe" . >nul
|
||||||
|
if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul
|
||||||
|
|
||||||
|
echo [5/6] PyInstaller...
|
||||||
|
"%PY%" -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter launcher.py
|
||||||
|
if errorlevel 1 (
|
||||||
|
cd ..
|
||||||
|
echo [ERROR] PyInstaller failed
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [6/6] Cleanup...
|
||||||
|
del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul
|
||||||
|
rmdir /s /q build 2>nul
|
||||||
|
cd ..
|
||||||
|
goto :DONE
|
||||||
|
|
||||||
|
:NORMAL
|
||||||
|
echo [INFO] Normal PyInstaller...
|
||||||
|
"%PY%" -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter "%SRC%"
|
||||||
|
|
||||||
|
:DONE
|
||||||
|
echo.
|
||||||
|
echo Done.
|
||||||
|
if exist "dist_cy\dist\%NAME%.exe" (
|
||||||
|
echo Output: dist_cy\dist\%NAME%.exe
|
||||||
|
) else if exist "dist\%NAME%.exe" (
|
||||||
|
echo Output: dist\%NAME%.exe
|
||||||
|
) else (
|
||||||
|
echo Check dist folder
|
||||||
|
)
|
||||||
|
pause
|
||||||
@@ -22,6 +22,44 @@ except ImportError:
|
|||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_dir():
|
||||||
|
return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def resource_candidates(file_name):
|
||||||
|
base_dir = get_app_dir()
|
||||||
|
candidates = []
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / file_name)
|
||||||
|
candidates.extend([
|
||||||
|
base_dir / file_name,
|
||||||
|
base_dir / 'tools' / file_name,
|
||||||
|
base_dir / 'shared' / file_name,
|
||||||
|
base_dir.parent / 'tools' / file_name,
|
||||||
|
base_dir.parent / 'shared' / file_name,
|
||||||
|
base_dir.parent / file_name,
|
||||||
|
])
|
||||||
|
unique = []
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate not in unique:
|
||||||
|
unique.append(candidate)
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
|
def find_resource(file_name):
|
||||||
|
candidates = resource_candidates(file_name)
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate.exists():
|
||||||
|
return candidate
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
|
def find_tool(file_name, fallback=None):
|
||||||
|
path = find_resource(file_name)
|
||||||
|
if path.exists():
|
||||||
|
return str(path)
|
||||||
|
return fallback or str(path)
|
||||||
class ADKAPKGUI:
|
class ADKAPKGUI:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
@@ -123,14 +161,10 @@ class ADKAPKGUI:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 从 exe/py 所在目录查找资源文件
|
# 从 exe/py 所在目录查找资源文件
|
||||||
self.base_dir = Path(sys.executable).parent if getattr(sys, 'frozen', False) else Path(__file__).parent
|
self.base_dir = get_app_dir()
|
||||||
if getattr(sys, 'frozen', False):
|
self.adb = find_tool('adb.exe', 'adb')
|
||||||
self.adb = str(Path(sys._MEIPASS) / 'adb.exe')
|
self.sz = find_tool('7za.exe')
|
||||||
self.sz = str(Path(sys._MEIPASS) / '7za.exe')
|
self.package_file = find_resource("package.bin")
|
||||||
else:
|
|
||||||
self.adb = 'adb'
|
|
||||||
self.sz = str(self.base_dir / '7za.exe')
|
|
||||||
self.package_file = self.base_dir / "package.bin"
|
|
||||||
self.extract_password = None
|
self.extract_password = None
|
||||||
self.apps_dir = None
|
self.apps_dir = None
|
||||||
self.priv_apps_dir = None
|
self.priv_apps_dir = None
|
||||||
@@ -519,7 +553,7 @@ class ADKAPKGUI:
|
|||||||
|
|
||||||
def run_on_ui_thread(self, func, *args, **kwargs):
|
def run_on_ui_thread(self, func, *args, **kwargs):
|
||||||
"""将函数调度到主线程执行,确保线程安全"""
|
"""将函数调度到主线程执行,确保线程安全"""
|
||||||
self.root.after(0, func, *args, **kwargs)
|
self.root.after(0, lambda: func(*args, **kwargs))
|
||||||
|
|
||||||
def _adb_cmd(self):
|
def _adb_cmd(self):
|
||||||
return subprocess.list2cmdline([self.adb])
|
return subprocess.list2cmdline([self.adb])
|
||||||
@@ -711,8 +745,54 @@ class ADKAPKGUI:
|
|||||||
"""检查语言包是否已解压"""
|
"""检查语言包是否已解压"""
|
||||||
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
||||||
has_priv = self.priv_apps_dir and self.priv_apps_dir.exists() and len(list(self.priv_apps_dir.glob("*.apk"))) > 0
|
has_priv = self.priv_apps_dir and self.priv_apps_dir.exists() and len(list(self.priv_apps_dir.glob("*.apk"))) > 0
|
||||||
|
if has_app or has_priv:
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"已解压缓存无效: {reason}", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return False
|
||||||
return has_app or has_priv
|
return has_app or has_priv
|
||||||
|
|
||||||
|
def _validate_extracted_apks(self):
|
||||||
|
apks = []
|
||||||
|
if self.apps_dir and self.apps_dir.exists():
|
||||||
|
apks.extend(self.apps_dir.glob("*.apk"))
|
||||||
|
if self.priv_apps_dir and self.priv_apps_dir.exists():
|
||||||
|
apks.extend(self.priv_apps_dir.glob("*.apk"))
|
||||||
|
if not apks:
|
||||||
|
return False, "未找到可用 APK"
|
||||||
|
zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0]
|
||||||
|
if zero_apks:
|
||||||
|
preview = ", ".join(zero_apks[:5])
|
||||||
|
suffix = "..." if len(zero_apks) > 5 else ""
|
||||||
|
return False, f"发现 0KB APK: {preview}{suffix}"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
def _clear_extracted_cache(self):
|
||||||
|
if self.temp_dir and self.temp_dir.exists():
|
||||||
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.apps_dir = None
|
||||||
|
self.priv_apps_dir = None
|
||||||
|
|
||||||
|
def _format_extract_error(self, err_msg, return_code):
|
||||||
|
text = (err_msg or "").lower()
|
||||||
|
if any(marker in text for marker in (
|
||||||
|
"wrong password",
|
||||||
|
"incorrect password",
|
||||||
|
"password is incorrect",
|
||||||
|
"data error in encrypted file",
|
||||||
|
"can not open encrypted archive",
|
||||||
|
)):
|
||||||
|
return "解压密码错误,请重新确认 package.bin 密码"
|
||||||
|
if "data error" in text:
|
||||||
|
return "资源包数据错误,可能是密码错误或 package.bin 损坏"
|
||||||
|
if "headers error" in text or "unexpected end" in text:
|
||||||
|
return "资源包损坏或不完整,请检查 package.bin"
|
||||||
|
if err_msg.strip():
|
||||||
|
return f"解压失败: {err_msg.strip()[:300]}"
|
||||||
|
return f"解压失败 (返回码 {return_code}),请检查密码是否正确"
|
||||||
|
|
||||||
def _decode_7z_output(self, output):
|
def _decode_7z_output(self, output):
|
||||||
for enc in ('gbk', 'utf-8'):
|
for enc in ('gbk', 'utf-8'):
|
||||||
try:
|
try:
|
||||||
@@ -812,14 +892,12 @@ class ADKAPKGUI:
|
|||||||
subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True)
|
subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True)
|
||||||
subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True)
|
subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True)
|
||||||
|
|
||||||
self.log("Preparing package...", "INFO")
|
self.log("正在准备资源包", "INFO")
|
||||||
|
|
||||||
ok, err_msg = self._extract_with_7za_progress()
|
ok, err_msg = self._extract_with_7za_progress()
|
||||||
if not ok:
|
if not ok:
|
||||||
if err_msg.strip():
|
self.log(self._format_extract_error(err_msg, 1), "ERROR")
|
||||||
self.log(f"Package preparation failed: {err_msg.strip()[:300]}", "ERROR")
|
self._clear_extracted_cache()
|
||||||
else:
|
|
||||||
self.log("Package preparation failed", "ERROR")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.apps_dir = None
|
self.apps_dir = None
|
||||||
@@ -834,10 +912,18 @@ class ADKAPKGUI:
|
|||||||
self.priv_apps_dir = priv_app_candidates[0]
|
self.priv_apps_dir = priv_app_candidates[0]
|
||||||
|
|
||||||
if not self.apps_dir and not self.priv_apps_dir:
|
if not self.apps_dir and not self.priv_apps_dir:
|
||||||
self.log("Warning: app/priv-app directory not found", "WARNING")
|
self.log("警告:未找到 app/priv-app 目录", "WARNING")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.log("Package prepared", "SUCCESS")
|
apk_count = len(list(self.apps_dir.glob("*.apk"))) if self.apps_dir else 0
|
||||||
|
priv_count = len(list(self.priv_apps_dir.glob("*.apk"))) if self.priv_apps_dir else 0
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return False
|
||||||
|
self.log(f"资源准备完成 (app: {apk_count}, priv-app: {priv_count})", "SUCCESS")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -846,7 +932,8 @@ class ADKAPKGUI:
|
|||||||
import traceback
|
import traceback
|
||||||
self.log(traceback.format_exc(), "ERROR")
|
self.log(traceback.format_exc(), "ERROR")
|
||||||
else:
|
else:
|
||||||
self.log("Package preparation failed, please check network and retry", "ERROR")
|
self.log("资源准备失败,请检查网络连接后重试", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_environment(self):
|
def check_environment(self):
|
||||||
@@ -889,6 +976,11 @@ class ADKAPKGUI:
|
|||||||
if has_priv:
|
if has_priv:
|
||||||
self.priv_apps_dir = priv_candidates[0]
|
self.priv_apps_dir = priv_candidates[0]
|
||||||
self.temp_dir = cache_dir
|
self.temp_dir = cache_dir
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"缓存资源无效,已清理: {reason}", "WARNING")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return
|
||||||
# self.log("已复用缓存的资源文件", "INFO")
|
# self.log("已复用缓存的资源文件", "INFO")
|
||||||
|
|
||||||
def refresh_device_status(self):
|
def refresh_device_status(self):
|
||||||
@@ -1026,6 +1118,35 @@ class ADKAPKGUI:
|
|||||||
|
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
|
def cleanup_after_language_push(self):
|
||||||
|
"""刷入语言包后禁用并卸载指定预装应用。"""
|
||||||
|
cleanup_packages = [
|
||||||
|
"com.incall.apps.softmanager",
|
||||||
|
"cn.com.os.changan.appstore",
|
||||||
|
"com.chinatsp.onecall",
|
||||||
|
"com.changan.oushangCos1",
|
||||||
|
]
|
||||||
|
|
||||||
|
self.log("正在执行刷入后应用清理...", "INFO")
|
||||||
|
failed_count = 0
|
||||||
|
for pkg in cleanup_packages:
|
||||||
|
disable_ok, disable_err = self.run_adb_command(
|
||||||
|
f'adb -d shell pm disable-user {pkg}')
|
||||||
|
uninstall_ok, uninstall_err = self.run_adb_command(
|
||||||
|
f'adb -d shell pm uninstall --user 0 {pkg}')
|
||||||
|
|
||||||
|
if disable_ok and uninstall_ok:
|
||||||
|
self.log(f"已禁用并卸载: {pkg}", "SUCCESS")
|
||||||
|
else:
|
||||||
|
failed_count += 1
|
||||||
|
detail = uninstall_err or disable_err or "应用可能不存在或已处理"
|
||||||
|
self.log(f"清理未完全成功: {pkg} ({detail})", "WARNING")
|
||||||
|
|
||||||
|
if failed_count:
|
||||||
|
self.log(f"应用清理完成,{failed_count} 个应用未完全成功", "WARNING")
|
||||||
|
else:
|
||||||
|
self.log("刷入后应用清理完成", "SUCCESS")
|
||||||
|
|
||||||
def push_all_apks(self):
|
def push_all_apks(self):
|
||||||
"""推送APK到系统分区(支持app和priv-app)"""
|
"""推送APK到系统分区(支持app和priv-app)"""
|
||||||
if not self.check_device_connection():
|
if not self.check_device_connection():
|
||||||
@@ -1138,6 +1259,8 @@ class ADKAPKGUI:
|
|||||||
self.run_adb_command('adb -d shell cp /data/local/tmp/FZLTHPro_GB18030.ttf /system/fonts/FZLTHPro_GB18030.ttf')
|
self.run_adb_command('adb -d shell cp /data/local/tmp/FZLTHPro_GB18030.ttf /system/fonts/FZLTHPro_GB18030.ttf')
|
||||||
self.run_adb_command('adb -d shell rm -f /data/local/tmp/FZLTHPro_GB18030.ttf')
|
self.run_adb_command('adb -d shell rm -f /data/local/tmp/FZLTHPro_GB18030.ttf')
|
||||||
|
|
||||||
|
self.cleanup_after_language_push()
|
||||||
|
|
||||||
if success_count == total:
|
if success_count == total:
|
||||||
self.log(f"刷入完成,共 {total} 个语言包", "SUCCESS")
|
self.log(f"刷入完成,共 {total} 个语言包", "SUCCESS")
|
||||||
self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING")
|
self.log("语言包已刷入完成,重启设备后生效,您可在适当时候重启", "WARNING")
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
@echo off
|
@echo off
|
||||||
chcp 65001 >nul
|
chcp 65001 >nul
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set "ROOT=%~dp0.."
|
||||||
|
set "TOOLS=%ROOT%\tools"
|
||||||
set NAME=X5plus刷入工具
|
set NAME=X5plus刷入工具
|
||||||
set SRC=X5plusTool.py
|
set SRC=X5plusTool.py
|
||||||
title %NAME% - Build
|
title %NAME% - Build
|
||||||
@@ -55,14 +58,14 @@ copy "%PYD%" _core.pyd >nul
|
|||||||
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
||||||
|
|
||||||
echo [4/6] Copy resources...
|
echo [4/6] Copy resources...
|
||||||
copy ..\adb.exe . >nul
|
copy "%TOOLS%\adb.exe" . >nul
|
||||||
copy ..\AdbWinApi.dll . >nul
|
copy "%TOOLS%\AdbWinApi.dll" . >nul
|
||||||
copy ..\AdbWinUsbApi.dll . >nul
|
copy "%TOOLS%\AdbWinUsbApi.dll" . >nul
|
||||||
copy ..\7za.exe . >nul
|
copy "%TOOLS%\7za.exe" . >nul
|
||||||
if exist "..\app.ico" copy "..\app.ico" . >nul
|
if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul
|
||||||
|
|
||||||
echo [5/6] PyInstaller...
|
echo [5/6] PyInstaller...
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin launcher.py
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin launcher.py
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
cd ..
|
cd ..
|
||||||
echo [ERROR] PyInstaller failed
|
echo [ERROR] PyInstaller failed
|
||||||
@@ -77,9 +80,8 @@ cd ..
|
|||||||
goto :DONE
|
goto :DONE
|
||||||
|
|
||||||
:NORMAL
|
:NORMAL
|
||||||
cd /d "%~dp0"
|
|
||||||
echo [INFO] Normal PyInstaller...
|
echo [INFO] Normal PyInstaller...
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin %SRC%
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin %SRC%
|
||||||
|
|
||||||
:DONE
|
:DONE
|
||||||
echo.
|
echo.
|
||||||
@@ -22,6 +22,44 @@ except ImportError:
|
|||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_dir():
|
||||||
|
return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def resource_candidates(file_name):
|
||||||
|
base_dir = get_app_dir()
|
||||||
|
candidates = []
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / file_name)
|
||||||
|
candidates.extend([
|
||||||
|
base_dir / file_name,
|
||||||
|
base_dir / 'tools' / file_name,
|
||||||
|
base_dir / 'shared' / file_name,
|
||||||
|
base_dir.parent / 'tools' / file_name,
|
||||||
|
base_dir.parent / 'shared' / file_name,
|
||||||
|
base_dir.parent / file_name,
|
||||||
|
])
|
||||||
|
unique = []
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate not in unique:
|
||||||
|
unique.append(candidate)
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
|
def find_resource(file_name):
|
||||||
|
candidates = resource_candidates(file_name)
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate.exists():
|
||||||
|
return candidate
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
|
def find_tool(file_name, fallback=None):
|
||||||
|
path = find_resource(file_name)
|
||||||
|
if path.exists():
|
||||||
|
return str(path)
|
||||||
|
return fallback or str(path)
|
||||||
class ADKAPKGUI:
|
class ADKAPKGUI:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
@@ -132,14 +170,10 @@ class ADKAPKGUI:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.base_dir = Path(sys.executable).parent if getattr(sys, 'frozen', False) else Path(__file__).parent
|
self.base_dir = get_app_dir()
|
||||||
if getattr(sys, 'frozen', False):
|
self.adb = find_tool('adb.exe', 'adb')
|
||||||
self.adb = str(Path(sys._MEIPASS) / 'adb.exe')
|
self.sz = find_tool('7za.exe')
|
||||||
self.sz = str(Path(sys._MEIPASS) / '7za.exe')
|
self.package_file = find_resource("package.bin")
|
||||||
else:
|
|
||||||
self.adb = 'adb'
|
|
||||||
self.sz = str(self.base_dir / '7za.exe')
|
|
||||||
self.package_file = self.base_dir / "package.bin"
|
|
||||||
self.extract_password = None
|
self.extract_password = None
|
||||||
self.apps_dir = None
|
self.apps_dir = None
|
||||||
self.temp_dir = None
|
self.temp_dir = None
|
||||||
@@ -592,7 +626,7 @@ class ADKAPKGUI:
|
|||||||
|
|
||||||
def run_on_ui_thread(self, func, *args, **kwargs):
|
def run_on_ui_thread(self, func, *args, **kwargs):
|
||||||
"""将函数调度到主线程执行,确保线程安全"""
|
"""将函数调度到主线程执行,确保线程安全"""
|
||||||
self.root.after(0, func, *args, **kwargs)
|
self.root.after(0, lambda: func(*args, **kwargs))
|
||||||
|
|
||||||
def _adb_cmd(self):
|
def _adb_cmd(self):
|
||||||
return subprocess.list2cmdline([self.adb])
|
return subprocess.list2cmdline([self.adb])
|
||||||
@@ -857,8 +891,51 @@ class ADKAPKGUI:
|
|||||||
def check_package_extracted(self):
|
def check_package_extracted(self):
|
||||||
"""检查语言包是否已解压"""
|
"""检查语言包是否已解压"""
|
||||||
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
||||||
|
if has_app:
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"已解压缓存无效: {reason}", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return False
|
||||||
return has_app
|
return has_app
|
||||||
|
|
||||||
|
def _validate_extracted_apks(self):
|
||||||
|
if not self.apps_dir or not self.apps_dir.exists():
|
||||||
|
return False, "缺少 apps 目录"
|
||||||
|
apks = list(self.apps_dir.glob("*.apk"))
|
||||||
|
if not apks:
|
||||||
|
return False, "apps 目录没有 APK"
|
||||||
|
zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0]
|
||||||
|
if zero_apks:
|
||||||
|
preview = ", ".join(zero_apks[:5])
|
||||||
|
suffix = "..." if len(zero_apks) > 5 else ""
|
||||||
|
return False, f"发现 0KB APK: {preview}{suffix}"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
def _clear_extracted_cache(self):
|
||||||
|
if self.temp_dir and self.temp_dir.exists():
|
||||||
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.apps_dir = None
|
||||||
|
|
||||||
|
def _format_extract_error(self, err_msg, return_code):
|
||||||
|
text = (err_msg or "").lower()
|
||||||
|
if any(marker in text for marker in (
|
||||||
|
"wrong password",
|
||||||
|
"incorrect password",
|
||||||
|
"password is incorrect",
|
||||||
|
"data error in encrypted file",
|
||||||
|
"can not open encrypted archive",
|
||||||
|
)):
|
||||||
|
return "解压密码错误,请重新确认 package.bin 密码"
|
||||||
|
if "data error" in text:
|
||||||
|
return "资源包数据错误,可能是密码错误或 package.bin 损坏"
|
||||||
|
if "headers error" in text or "unexpected end" in text:
|
||||||
|
return "资源包损坏或不完整,请检查 package.bin"
|
||||||
|
if err_msg.strip():
|
||||||
|
return f"解压失败: {err_msg.strip()[:300]}"
|
||||||
|
return f"解压失败 (返回码 {return_code}),请检查密码是否正确"
|
||||||
|
|
||||||
def _decode_7z_output(self, output):
|
def _decode_7z_output(self, output):
|
||||||
for enc in ('gbk', 'utf-8'):
|
for enc in ('gbk', 'utf-8'):
|
||||||
try:
|
try:
|
||||||
@@ -958,14 +1035,12 @@ class ADKAPKGUI:
|
|||||||
subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True)
|
subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True)
|
||||||
subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True)
|
subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True)
|
||||||
|
|
||||||
self.log("Preparing package...", "INFO")
|
self.log("正在准备资源包", "INFO")
|
||||||
|
|
||||||
ok, err_msg = self._extract_with_7za_progress()
|
ok, err_msg = self._extract_with_7za_progress()
|
||||||
if not ok:
|
if not ok:
|
||||||
if err_msg.strip():
|
self.log(self._format_extract_error(err_msg, 1), "ERROR")
|
||||||
self.log(f"Package preparation failed: {err_msg.strip()[:300]}", "ERROR")
|
self._clear_extracted_cache()
|
||||||
else:
|
|
||||||
self.log("Package preparation failed", "ERROR")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.apps_dir = None
|
self.apps_dir = None
|
||||||
@@ -974,11 +1049,17 @@ class ADKAPKGUI:
|
|||||||
self.apps_dir = app_candidates[0]
|
self.apps_dir = app_candidates[0]
|
||||||
|
|
||||||
if not self.apps_dir:
|
if not self.apps_dir:
|
||||||
self.log("Warning: app/apps directory not found", "WARNING")
|
self.log("警告:未找到 apps 目录", "WARNING")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
apk_count = len(list(self.apps_dir.glob("*.apk")))
|
apk_count = len(list(self.apps_dir.glob("*.apk")))
|
||||||
self.log(f"Package prepared (app: {apk_count})", "SUCCESS")
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return False
|
||||||
|
self.log(f"资源准备完成 (app: {apk_count})", "SUCCESS")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -987,7 +1068,8 @@ class ADKAPKGUI:
|
|||||||
import traceback
|
import traceback
|
||||||
self.log(traceback.format_exc(), "ERROR")
|
self.log(traceback.format_exc(), "ERROR")
|
||||||
else:
|
else:
|
||||||
self.log("Package preparation failed, please check network and retry", "ERROR")
|
self.log("资源准备失败,请检查网络连接后重试", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_environment(self):
|
def check_environment(self):
|
||||||
@@ -1107,7 +1189,7 @@ class ADKAPKGUI:
|
|||||||
if not ok:
|
if not ok:
|
||||||
return False, f"push失败: {err}"
|
return False, f"push失败: {err}"
|
||||||
|
|
||||||
ok, err = self.run_adb_shell(f'pm install -r {temp_apk_path}')
|
ok, err = self.run_adb_shell(f'pm install -r -d -f {temp_apk_path}')
|
||||||
self.run_adb_shell(f'rm -f {temp_apk_path}')
|
self.run_adb_shell(f'rm -f {temp_apk_path}')
|
||||||
if not ok:
|
if not ok:
|
||||||
return False, f"install失败: {err}"
|
return False, f"install失败: {err}"
|
||||||
@@ -1204,6 +1286,8 @@ class ADKAPKGUI:
|
|||||||
|
|
||||||
# 使用当前目录下的apks文件夹
|
# 使用当前目录下的apks文件夹
|
||||||
apk_dir = self.base_dir / "apks"
|
apk_dir = self.base_dir / "apks"
|
||||||
|
if not apk_dir.exists():
|
||||||
|
apk_dir = find_resource("apks")
|
||||||
|
|
||||||
# 检查apk文件夹是否存在
|
# 检查apk文件夹是否存在
|
||||||
if not apk_dir.exists():
|
if not apk_dir.exists():
|
||||||
@@ -22,6 +22,44 @@ except ImportError:
|
|||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_dir():
|
||||||
|
return Path(sys.executable).resolve().parent if getattr(sys, 'frozen', False) else Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def resource_candidates(file_name):
|
||||||
|
base_dir = get_app_dir()
|
||||||
|
candidates = []
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
candidates.append(Path(getattr(sys, '_MEIPASS', base_dir)) / file_name)
|
||||||
|
candidates.extend([
|
||||||
|
base_dir / file_name,
|
||||||
|
base_dir / 'tools' / file_name,
|
||||||
|
base_dir / 'shared' / file_name,
|
||||||
|
base_dir.parent / 'tools' / file_name,
|
||||||
|
base_dir.parent / 'shared' / file_name,
|
||||||
|
base_dir.parent / file_name,
|
||||||
|
])
|
||||||
|
unique = []
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate not in unique:
|
||||||
|
unique.append(candidate)
|
||||||
|
return unique
|
||||||
|
|
||||||
|
|
||||||
|
def find_resource(file_name):
|
||||||
|
candidates = resource_candidates(file_name)
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate.exists():
|
||||||
|
return candidate
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
|
def find_tool(file_name, fallback=None):
|
||||||
|
path = find_resource(file_name)
|
||||||
|
if path.exists():
|
||||||
|
return str(path)
|
||||||
|
return fallback or str(path)
|
||||||
class ADKAPKGUI:
|
class ADKAPKGUI:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
@@ -130,14 +168,10 @@ class ADKAPKGUI:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.base_dir = Path(sys.executable).parent if getattr(sys, 'frozen', False) else Path(__file__).parent
|
self.base_dir = get_app_dir()
|
||||||
if getattr(sys, 'frozen', False):
|
self.adb = find_tool('adb.exe', 'adb')
|
||||||
self.adb = str(Path(sys._MEIPASS) / 'adb.exe')
|
self.sz = find_tool('7za.exe')
|
||||||
self.sz = str(Path(sys._MEIPASS) / '7za.exe')
|
self.package_file = find_resource("package.bin")
|
||||||
else:
|
|
||||||
self.adb = 'adb'
|
|
||||||
self.sz = str(self.base_dir / '7za.exe')
|
|
||||||
self.package_file = self.base_dir / "package.bin"
|
|
||||||
self.extract_password = None
|
self.extract_password = None
|
||||||
self.apps_dir = None
|
self.apps_dir = None
|
||||||
self.temp_dir = None
|
self.temp_dir = None
|
||||||
@@ -556,7 +590,7 @@ class ADKAPKGUI:
|
|||||||
|
|
||||||
def run_on_ui_thread(self, func, *args, **kwargs):
|
def run_on_ui_thread(self, func, *args, **kwargs):
|
||||||
"""将函数调度到主线程执行,确保线程安全"""
|
"""将函数调度到主线程执行,确保线程安全"""
|
||||||
self.root.after(0, func, *args, **kwargs)
|
self.root.after(0, lambda: func(*args, **kwargs))
|
||||||
|
|
||||||
def _adb_cmd(self):
|
def _adb_cmd(self):
|
||||||
return subprocess.list2cmdline([self.adb])
|
return subprocess.list2cmdline([self.adb])
|
||||||
@@ -824,39 +858,93 @@ class ADKAPKGUI:
|
|||||||
def check_package_extracted(self):
|
def check_package_extracted(self):
|
||||||
"""检查语言包是否已解压"""
|
"""检查语言包是否已解压"""
|
||||||
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
has_app = self.apps_dir and self.apps_dir.exists() and len(list(self.apps_dir.glob("*.apk"))) > 0
|
||||||
|
if has_app:
|
||||||
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"已解压缓存无效: {reason}", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return False
|
||||||
return has_app
|
return has_app
|
||||||
|
|
||||||
def _decode_7z_output(self, output):
|
def _validate_extracted_apks(self):
|
||||||
for enc in ('gbk', 'utf-8'):
|
if not self.apps_dir or not self.apps_dir.exists():
|
||||||
try:
|
return False, "缺少 apps 目录"
|
||||||
return output.decode(enc)
|
apks = list(self.apps_dir.glob("*.apk"))
|
||||||
except UnicodeDecodeError:
|
if not apks:
|
||||||
|
return False, "apps 目录没有 APK"
|
||||||
|
zero_apks = [apk.name for apk in apks if apk.stat().st_size <= 0]
|
||||||
|
if zero_apks:
|
||||||
|
preview = ", ".join(zero_apks[:5])
|
||||||
|
suffix = "..." if len(zero_apks) > 5 else ""
|
||||||
|
return False, f"发现 0KB APK: {preview}{suffix}"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
def _clear_extracted_cache(self):
|
||||||
|
if self.temp_dir and self.temp_dir.exists():
|
||||||
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.apps_dir = None
|
||||||
|
|
||||||
|
def _format_extract_error(self, err_msg, return_code):
|
||||||
|
text = (err_msg or "").lower()
|
||||||
|
if any(marker in text for marker in (
|
||||||
|
"wrong password",
|
||||||
|
"incorrect password",
|
||||||
|
"password is incorrect",
|
||||||
|
"data error in encrypted file",
|
||||||
|
"can not open encrypted archive",
|
||||||
|
)):
|
||||||
|
return "解压密码错误,请重新确认 package.bin 密码"
|
||||||
|
if "data error" in text:
|
||||||
|
return "资源包数据错误,可能是密码错误或 package.bin 损坏"
|
||||||
|
if "headers error" in text or "unexpected end" in text:
|
||||||
|
return "资源包损坏或不完整,请检查 package.bin"
|
||||||
|
if err_msg.strip():
|
||||||
|
return f"解压失败: {err_msg.strip()[:300]}"
|
||||||
|
return f"解压失败 (返回码 {return_code}),请检查密码是否正确"
|
||||||
|
|
||||||
|
def _decode_7z_output(self, *outputs):
|
||||||
|
"""解码 7za 输出,兼容中文 Windows 控制台编码。"""
|
||||||
|
parts = []
|
||||||
|
for output in outputs:
|
||||||
|
if not output:
|
||||||
continue
|
continue
|
||||||
return output.decode('utf-8', errors='replace')
|
for enc in ['gbk', 'utf-8']:
|
||||||
|
try:
|
||||||
|
parts.append(output.decode(enc, errors='replace'))
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return ''.join(parts).strip()
|
||||||
|
|
||||||
def _seven_zip_supports_progress_stream(self):
|
def _seven_zip_supports_progress_stream(self):
|
||||||
|
"""检测 7za 是否支持 -bsp1 进度流参数。"""
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[self.sz],
|
[self.sz],
|
||||||
capture_output=True,
|
stdout=subprocess.PIPE,
|
||||||
text=True,
|
stderr=subprocess.PIPE,
|
||||||
errors='ignore',
|
|
||||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
|
||||||
)
|
)
|
||||||
return '-bs{o|e|p}' in (result.stdout + result.stderr)
|
output = self._decode_7z_output(result.stdout, result.stderr)
|
||||||
|
return '-bs{o|e|p}' in output
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _extract_with_7za_progress(self):
|
def _extract_with_7za_progress(self, use_progress_switches=False):
|
||||||
|
"""执行 7za 解压并尽量解析百分比进度。"""
|
||||||
self.update_progress(0, 100, "Loading resources...")
|
self.update_progress(0, 100, "Loading resources...")
|
||||||
cmd = [
|
cmd = [
|
||||||
self.sz, 'x', str(self.package_file),
|
self.sz, 'x', str(self.package_file),
|
||||||
f'-p{self.extract_password}',
|
f'-p{self.extract_password}',
|
||||||
f'-o{self.temp_dir}', '-y'
|
f'-o{self.temp_dir}', '-y'
|
||||||
]
|
]
|
||||||
if self._seven_zip_supports_progress_stream():
|
if use_progress_switches:
|
||||||
cmd.extend(['-bsp1', '-bso0', '-bse1'])
|
cmd.extend(['-bsp1', '-bso0', '-bse1'])
|
||||||
|
|
||||||
|
if getattr(self, 'debug_mode', False):
|
||||||
|
self.log(f"7ZA CMD: {subprocess.list2cmdline(cmd)}", "CMD")
|
||||||
|
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
@@ -891,8 +979,13 @@ class ADKAPKGUI:
|
|||||||
decoded_output = self._decode_7z_output(bytes(output))
|
decoded_output = self._decode_7z_output(bytes(output))
|
||||||
if return_code == 0:
|
if return_code == 0:
|
||||||
self.update_progress(100, 100, "Resources loaded")
|
self.update_progress(100, 100, "Resources loaded")
|
||||||
return True, decoded_output
|
|
||||||
return False, decoded_output
|
if getattr(self, 'debug_mode', False):
|
||||||
|
self.log(f"7ZA RET: {return_code}", "CMD" if return_code == 0 else "ERROR")
|
||||||
|
if decoded_output:
|
||||||
|
self.log(f"7ZA OUTPUT:\n{decoded_output}", "CMD" if return_code == 0 else "ERROR")
|
||||||
|
|
||||||
|
return return_code, decoded_output
|
||||||
|
|
||||||
def extract_package_silent(self):
|
def extract_package_silent(self):
|
||||||
"""Extract package.bin silently with progress; app directory only."""
|
"""Extract package.bin silently with progress; app directory only."""
|
||||||
@@ -925,14 +1018,18 @@ class ADKAPKGUI:
|
|||||||
subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True)
|
subprocess.run(f'attrib +h "{self.temp_dir.parent}"', shell=True, capture_output=True)
|
||||||
subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True)
|
subprocess.run(f'attrib +h "{self.temp_dir}"', shell=True, capture_output=True)
|
||||||
|
|
||||||
self.log("Preparing package...", "INFO")
|
self.log("正在准备资源包", "INFO")
|
||||||
|
|
||||||
ok, err_msg = self._extract_with_7za_progress()
|
use_progress_switches = self._seven_zip_supports_progress_stream()
|
||||||
if not ok:
|
return_code, err_msg = self._extract_with_7za_progress(use_progress_switches)
|
||||||
if err_msg.strip():
|
|
||||||
self.log(f"Package preparation failed: {err_msg.strip()[:300]}", "ERROR")
|
if return_code != 0 and use_progress_switches and "Incorrect command line" in err_msg:
|
||||||
else:
|
self.log("当前 7za 不支持进度参数,正在使用兼容模式重试解压...", "WARNING")
|
||||||
self.log("Package preparation failed", "ERROR")
|
return_code, err_msg = self._extract_with_7za_progress(False)
|
||||||
|
|
||||||
|
if return_code != 0:
|
||||||
|
self.log(self._format_extract_error(err_msg, return_code), "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.apps_dir = None
|
self.apps_dir = None
|
||||||
@@ -941,11 +1038,17 @@ class ADKAPKGUI:
|
|||||||
self.apps_dir = app_candidates[0]
|
self.apps_dir = app_candidates[0]
|
||||||
|
|
||||||
if not self.apps_dir:
|
if not self.apps_dir:
|
||||||
self.log("Warning: app/apps directory not found", "WARNING")
|
self.log("警告:未找到 apps 目录", "WARNING")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
apk_count = len(list(self.apps_dir.glob("*.apk")))
|
apk_count = len(list(self.apps_dir.glob("*.apk")))
|
||||||
self.log(f"Package prepared (app: {apk_count})", "SUCCESS")
|
ok, reason = self._validate_extracted_apks()
|
||||||
|
if not ok:
|
||||||
|
self.log(f"解压后的资源无效: {reason}。已停止刷入,请检查解压密码或资源包。", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
|
return False
|
||||||
|
self.log(f"资源准备完成 (app: {apk_count})", "SUCCESS")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -954,7 +1057,8 @@ class ADKAPKGUI:
|
|||||||
import traceback
|
import traceback
|
||||||
self.log(traceback.format_exc(), "ERROR")
|
self.log(traceback.format_exc(), "ERROR")
|
||||||
else:
|
else:
|
||||||
self.log("Package preparation failed, please check network and retry", "ERROR")
|
self.log("资源准备失败,请检查网络连接后重试", "ERROR")
|
||||||
|
self._clear_extracted_cache()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_environment(self):
|
def check_environment(self):
|
||||||
@@ -1184,6 +1288,8 @@ class ADKAPKGUI:
|
|||||||
|
|
||||||
# 使用当前目录下的apks文件夹
|
# 使用当前目录下的apks文件夹
|
||||||
apk_dir = self.base_dir / "apks"
|
apk_dir = self.base_dir / "apks"
|
||||||
|
if not apk_dir.exists():
|
||||||
|
apk_dir = find_resource("apks")
|
||||||
|
|
||||||
# 检查apk文件夹是否存在
|
# 检查apk文件夹是否存在
|
||||||
if not apk_dir.exists():
|
if not apk_dir.exists():
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
@echo off
|
@echo off
|
||||||
chcp 65001 >nul
|
chcp 65001 >nul
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set "ROOT=%~dp0.."
|
||||||
|
set "TOOLS=%ROOT%\tools"
|
||||||
set NAME=长安逸动刷入工具
|
set NAME=长安逸动刷入工具
|
||||||
set SRC=app-install.py
|
set SRC=app-install.py
|
||||||
title %NAME% - Build
|
title %NAME% - Build
|
||||||
@@ -55,14 +58,14 @@ copy "%PYD%" _core.pyd >nul
|
|||||||
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
||||||
|
|
||||||
echo [4/6] Copy resources...
|
echo [4/6] Copy resources...
|
||||||
copy ..\adb.exe . >nul
|
copy "%TOOLS%\adb.exe" . >nul
|
||||||
copy ..\AdbWinApi.dll . >nul
|
copy "%TOOLS%\AdbWinApi.dll" . >nul
|
||||||
copy ..\AdbWinUsbApi.dll . >nul
|
copy "%TOOLS%\AdbWinUsbApi.dll" . >nul
|
||||||
copy ..\7za.exe . >nul
|
copy "%TOOLS%\7za.exe" . >nul
|
||||||
if exist "..\app.ico" copy "..\app.ico" . >nul
|
if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul
|
||||||
|
|
||||||
echo [5/6] PyInstaller...
|
echo [5/6] PyInstaller...
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin launcher.py
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin launcher.py
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
cd ..
|
cd ..
|
||||||
echo [ERROR] PyInstaller failed
|
echo [ERROR] PyInstaller failed
|
||||||
@@ -77,9 +80,8 @@ cd ..
|
|||||||
goto :DONE
|
goto :DONE
|
||||||
|
|
||||||
:NORMAL
|
:NORMAL
|
||||||
cd /d "%~dp0"
|
|
||||||
echo [INFO] Normal PyInstaller...
|
echo [INFO] Normal PyInstaller...
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin %SRC%
|
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin %SRC%
|
||||||
|
|
||||||
:DONE
|
:DONE
|
||||||
echo.
|
echo.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set "ROOT=%~dp0.."
|
||||||
|
set "TOOLS=%ROOT%\tools"
|
||||||
|
set "NAME=Changan-Yidong-Language-Installer"
|
||||||
|
set "SRC=app-yidong.py"
|
||||||
|
title %NAME% - Build
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo %NAME% - Cython Build
|
||||||
|
echo ============================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
where python >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Python not found
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
for /f "delims=" %%i in ('where python') do set "PY=%%i"
|
||||||
|
echo Python: %PY%
|
||||||
|
|
||||||
|
echo [1/6] Installing deps...
|
||||||
|
"%PY%" -m pip install pyinstaller cython pyzipper -q
|
||||||
|
if errorlevel 1 (
|
||||||
|
"%PY%" -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [2/6] Clean...
|
||||||
|
if exist "dist_cy" rmdir /s /q dist_cy 2>nul
|
||||||
|
if exist "build" rmdir /s /q build 2>nul
|
||||||
|
if exist "dist" rmdir /s /q dist 2>nul
|
||||||
|
|
||||||
|
echo [3/6] Cython compile...
|
||||||
|
mkdir dist_cy 2>nul
|
||||||
|
copy "%SRC%" dist_cy\_core.py >nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [WARN] Copy source failed, fallback
|
||||||
|
goto :NORMAL
|
||||||
|
)
|
||||||
|
|
||||||
|
"%PY%" -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')"
|
||||||
|
|
||||||
|
cd dist_cy
|
||||||
|
"%PY%" setup_cython.py build_ext --inplace
|
||||||
|
if errorlevel 1 (
|
||||||
|
cd ..
|
||||||
|
echo [WARN] Cython failed, fallback
|
||||||
|
goto :NORMAL
|
||||||
|
)
|
||||||
|
|
||||||
|
for %%f in (_core*.pyd) do set PYD=%%f
|
||||||
|
if "%PYD%"=="" (
|
||||||
|
cd ..
|
||||||
|
echo [WARN] No pyd, fallback
|
||||||
|
goto :NORMAL
|
||||||
|
)
|
||||||
|
echo PYD: %PYD%
|
||||||
|
copy "%PYD%" _core.pyd >nul
|
||||||
|
|
||||||
|
"%PY%" -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
||||||
|
|
||||||
|
echo [4/6] Copy resources...
|
||||||
|
copy "%TOOLS%\adb.exe" . >nul
|
||||||
|
copy "%TOOLS%\AdbWinApi.dll" . >nul
|
||||||
|
copy "%TOOLS%\AdbWinUsbApi.dll" . >nul
|
||||||
|
copy "%TOOLS%\7za.exe" . >nul
|
||||||
|
if exist "%ROOT%\app.ico" copy "%ROOT%\app.ico" . >nul
|
||||||
|
|
||||||
|
echo [5/6] PyInstaller...
|
||||||
|
"%PY%" -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin launcher.py
|
||||||
|
if errorlevel 1 (
|
||||||
|
cd ..
|
||||||
|
echo [ERROR] PyInstaller failed
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [6/6] Cleanup...
|
||||||
|
del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul
|
||||||
|
rmdir /s /q build 2>nul
|
||||||
|
cd ..
|
||||||
|
goto :DONE
|
||||||
|
|
||||||
|
:NORMAL
|
||||||
|
echo [INFO] Normal PyInstaller...
|
||||||
|
"%PY%" -m PyInstaller --onefile --windowed --name="%NAME%" --icon="%ROOT%\app.ico" --add-data "%TOOLS%\adb.exe;." --add-data "%TOOLS%\AdbWinApi.dll;." --add-data "%TOOLS%\AdbWinUsbApi.dll;." --add-data "%TOOLS%\7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --collect-all tkinter --uac-admin "%SRC%"
|
||||||
|
|
||||||
|
:DONE
|
||||||
|
echo.
|
||||||
|
echo Done.
|
||||||
|
if exist "dist_cy\dist\%NAME%.exe" (
|
||||||
|
echo Output: dist_cy\dist\%NAME%.exe
|
||||||
|
) else if exist "dist\%NAME%.exe" (
|
||||||
|
echo Output: dist\%NAME%.exe
|
||||||
|
) else (
|
||||||
|
echo Check dist folder
|
||||||
|
)
|
||||||
|
pause
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
set NAME=长安逸动刷入工具
|
|
||||||
set SRC=app-yidong.py
|
|
||||||
title %NAME% - Build
|
|
||||||
|
|
||||||
echo ============================================================
|
|
||||||
echo %NAME% - Cython Build
|
|
||||||
echo ============================================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
where python >nul 2>&1
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo [ERROR] Python not found
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
for /f "delims=" %%i in ('where python') do set PY=%%i
|
|
||||||
echo Python: %PY%
|
|
||||||
|
|
||||||
echo [1/6] Installing deps...
|
|
||||||
%PY% -m pip install pyinstaller cython pyzipper -q
|
|
||||||
if errorlevel 1 (
|
|
||||||
%PY% -m pip install pyinstaller cython pyzipper -q -i https://pypi.tuna.tsinghua.edu.cn/simple
|
|
||||||
)
|
|
||||||
|
|
||||||
echo [2/6] Clean...
|
|
||||||
if exist "dist_cy" rmdir /s /q dist_cy 2>nul
|
|
||||||
if exist "build" rmdir /s /q build 2>nul
|
|
||||||
if exist "dist" rmdir /s /q dist 2>nul
|
|
||||||
|
|
||||||
echo [3/6] Cython compile...
|
|
||||||
mkdir dist_cy 2>nul
|
|
||||||
copy %SRC% dist_cy\_core.py >nul
|
|
||||||
|
|
||||||
%PY% -c "open('dist_cy/setup_cython.py','w').write('from setuptools import setup\nfrom Cython.Build import cythonize\nsetup(ext_modules=cythonize(\"_core.py\", compiler_directives={\"language_level\":\"3\"}))\n')"
|
|
||||||
|
|
||||||
cd dist_cy
|
|
||||||
%PY% setup_cython.py build_ext --inplace
|
|
||||||
if errorlevel 1 (
|
|
||||||
cd ..
|
|
||||||
echo [WARN] Cython failed, fallback
|
|
||||||
goto :NORMAL
|
|
||||||
)
|
|
||||||
|
|
||||||
for %%f in (_core*.pyd) do set PYD=%%f
|
|
||||||
if "%PYD%"=="" (
|
|
||||||
cd ..
|
|
||||||
echo [WARN] No pyd, fallback
|
|
||||||
goto :NORMAL
|
|
||||||
)
|
|
||||||
echo PYD: %PYD%
|
|
||||||
copy "%PYD%" _core.pyd >nul
|
|
||||||
|
|
||||||
%PY% -c "open('launcher.py','w').write('# -*- coding: utf-8 -*-\nfrom _core import main\nmain()\n')"
|
|
||||||
|
|
||||||
echo [4/6] Copy resources...
|
|
||||||
copy ..\adb.exe . >nul
|
|
||||||
copy ..\AdbWinApi.dll . >nul
|
|
||||||
copy ..\AdbWinUsbApi.dll . >nul
|
|
||||||
copy ..\7za.exe . >nul
|
|
||||||
if exist "..\app.ico" copy "..\app.ico" . >nul
|
|
||||||
|
|
||||||
echo [5/6] PyInstaller...
|
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --add-binary "_core.pyd;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin launcher.py
|
|
||||||
if errorlevel 1 (
|
|
||||||
cd ..
|
|
||||||
echo [ERROR] PyInstaller failed
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
echo [6/6] Cleanup...
|
|
||||||
del /q _core.py _core.c _core.pyd %PYD% launcher.py setup_cython.py 2>nul
|
|
||||||
rmdir /s /q build 2>nul
|
|
||||||
cd ..
|
|
||||||
goto :DONE
|
|
||||||
|
|
||||||
:NORMAL
|
|
||||||
cd /d "%~dp0"
|
|
||||||
echo [INFO] Normal PyInstaller...
|
|
||||||
%PY% -m PyInstaller --onefile --windowed --name="%NAME%" --icon=app.ico --add-data "adb.exe;." --add-data "AdbWinApi.dll;." --add-data "AdbWinUsbApi.dll;." --add-data "7za.exe;." --hidden-import=queue --hidden-import=threading --hidden-import=tkinter --hidden-import=tkinter.simpledialog --hidden-import=zipfile --hidden-import=json --hidden-import=urllib --hidden-import=urllib.parse --collect-all tkinter --uac-admin %SRC%
|
|
||||||
|
|
||||||
:DONE
|
|
||||||
echo.
|
|
||||||
echo Done.
|
|
||||||
if exist "dist_cy\dist\%NAME%.exe" (
|
|
||||||
echo Output: dist_cy\dist\%NAME%.exe
|
|
||||||
) else if exist "dist\%NAME%.exe" (
|
|
||||||
echo Output: dist\%NAME%.exe
|
|
||||||
) else (
|
|
||||||
echo Check dist folder
|
|
||||||
)
|
|
||||||
pause
|
|
||||||
+21905
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
AAD = b"Q05-Lidar init_boot resource v1"
|
||||||
|
|
||||||
|
|
||||||
|
def b64u(data):
|
||||||
|
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Encrypt a Q05-Lidar init_boot image into resource.dat"
|
||||||
|
)
|
||||||
|
parser.add_argument("input_img", help="Magisk-patched init_boot image")
|
||||||
|
parser.add_argument(
|
||||||
|
"-o", "--output",
|
||||||
|
default="resource.dat",
|
||||||
|
help="Output encrypted resource file, default: resource.dat",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--key",
|
||||||
|
help="Optional AES-256 key as base64url or 64-char hex. Omit to generate a random key.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-compress",
|
||||||
|
action="store_true",
|
||||||
|
help="Disable zlib compression before encryption.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||||
|
except ImportError:
|
||||||
|
raise SystemExit(
|
||||||
|
"Missing dependency: cryptography. Install it with: "
|
||||||
|
"python -m pip install cryptography"
|
||||||
|
)
|
||||||
|
|
||||||
|
input_path = Path(args.input_img)
|
||||||
|
plain = input_path.read_bytes()
|
||||||
|
sha256 = hashlib.sha256(plain).hexdigest()
|
||||||
|
|
||||||
|
if args.key:
|
||||||
|
text = args.key.strip()
|
||||||
|
if len(text) == 64 and all(c in "0123456789abcdefABCDEF" for c in text):
|
||||||
|
key = bytes.fromhex(text)
|
||||||
|
else:
|
||||||
|
key = base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
|
||||||
|
if len(key) != 32:
|
||||||
|
raise SystemExit("key must decode to 32 bytes")
|
||||||
|
else:
|
||||||
|
key = os.urandom(32)
|
||||||
|
|
||||||
|
compression = "none" if args.no_compress else "zlib"
|
||||||
|
body = plain if args.no_compress else zlib.compress(plain, level=9)
|
||||||
|
nonce = os.urandom(12)
|
||||||
|
ciphertext = AESGCM(key).encrypt(nonce, body, AAD)
|
||||||
|
|
||||||
|
header = {
|
||||||
|
"format": "q05-lidar-resource-v2",
|
||||||
|
"cipher": "AES-256-GCM",
|
||||||
|
"kdf": "none",
|
||||||
|
"compression": compression,
|
||||||
|
"aad": AAD.decode("utf-8"),
|
||||||
|
"nonce": b64u(nonce),
|
||||||
|
"sha256": sha256,
|
||||||
|
"plainSize": len(plain),
|
||||||
|
"packedSize": len(body),
|
||||||
|
}
|
||||||
|
|
||||||
|
output_path = Path(args.output)
|
||||||
|
header_bytes = json.dumps(header, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||||
|
output_path.write_bytes(b"Q05R2\x00" + struct.pack(">I", len(header_bytes)) + header_bytes + ciphertext)
|
||||||
|
|
||||||
|
print("resource.dat created")
|
||||||
|
print(f"input: {input_path}")
|
||||||
|
print(f"output: {output_path}")
|
||||||
|
print(f"plainSize: {len(plain)}")
|
||||||
|
print(f"packedSize: {len(body)}")
|
||||||
|
print(f"sha256: {sha256}")
|
||||||
|
print(f"cloudKey: {b64u(key)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
[defaults]
|
||||||
|
base_features = sparse_super,large_file,filetype,dir_index,ext_attr
|
||||||
|
default_mntopts = acl,user_xattr
|
||||||
|
enable_periodic_fsck = 0
|
||||||
|
blocksize = 4096
|
||||||
|
inode_size = 256
|
||||||
|
inode_ratio = 16384
|
||||||
|
reserved_ratio = 1.0
|
||||||
|
|
||||||
|
[fs_types]
|
||||||
|
ext3 = {
|
||||||
|
features = has_journal
|
||||||
|
}
|
||||||
|
ext4 = {
|
||||||
|
features = has_journal,extent,huge_file,dir_nlink,extra_isize,uninit_bg
|
||||||
|
inode_size = 256
|
||||||
|
}
|
||||||
|
ext4dev = {
|
||||||
|
features = has_journal,extent,huge_file,flex_bg,inline_data,64bit,dir_nlink,extra_isize
|
||||||
|
inode_size = 256
|
||||||
|
options = test_fs=1
|
||||||
|
}
|
||||||
|
small = {
|
||||||
|
blocksize = 1024
|
||||||
|
inode_size = 128
|
||||||
|
inode_ratio = 4096
|
||||||
|
}
|
||||||
|
floppy = {
|
||||||
|
blocksize = 1024
|
||||||
|
inode_size = 128
|
||||||
|
inode_ratio = 8192
|
||||||
|
}
|
||||||
|
big = {
|
||||||
|
inode_ratio = 32768
|
||||||
|
}
|
||||||
|
huge = {
|
||||||
|
inode_ratio = 65536
|
||||||
|
}
|
||||||
|
news = {
|
||||||
|
inode_ratio = 4096
|
||||||
|
}
|
||||||
|
largefile = {
|
||||||
|
inode_ratio = 1048576
|
||||||
|
blocksize = -1
|
||||||
|
}
|
||||||
|
largefile4 = {
|
||||||
|
inode_ratio = 4194304
|
||||||
|
blocksize = -1
|
||||||
|
}
|
||||||
|
hurd = {
|
||||||
|
blocksize = 4096
|
||||||
|
inode_size = 128
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Pkg.UserSrc=false
|
||||||
|
Pkg.Revision=37.0.0
|
||||||
Reference in New Issue
Block a user