feat: 服务端集成自动打包流程
19
.env.example
@ -1,23 +1,22 @@
|
||||
# === iOS 自动打包服务配置 ===
|
||||
# 复制此文件为 .env 并修改为实际值
|
||||
|
||||
# ---- 项目路径 ----
|
||||
# iOS 源码项目根目录(也用于 AutoPacking 脚本路径)
|
||||
PROJECT_ROOT=/Users/shen/Work/Code/Readoor
|
||||
# ---- 目录配置 ----
|
||||
# 分支源码根目录,每个分支一个子目录(如 ReadoorBranches/main、ReadoorBranches/dev)
|
||||
GIT_SOURCE_BASE=./ReadoorBranches
|
||||
|
||||
# 打包输出基础目录(每次打包会在此目录下创建子目录)
|
||||
BUILD_BASE_DIR=/Users/shen/Documents
|
||||
BUILD_BASE_DIR=./build
|
||||
|
||||
# ---- 分支源码管理 ----
|
||||
# 分支源码根目录,每个分支一个子目录(如 ReadoorBranches/main、ReadoorBranches/dev)
|
||||
GIT_SOURCE_BASE=/Users/shen/Work/Code/ReadoorBranches
|
||||
|
||||
# Git 远程仓库地址,分支目录不存在时自动 clone
|
||||
GIT_REMOTE_URL=git@github.com:your-org/readoor.git
|
||||
GIT_REMOTE_URL=https://pineapple.readoor.cn:9080/trn/triapp.git
|
||||
GIT_USERNAME=shenlei
|
||||
GIT_PASSWORD='&s9U5fbreA'
|
||||
|
||||
# ---- 服务端口 ----
|
||||
BACKEND_PORT=8000
|
||||
FRONTEND_PORT=3000
|
||||
BACKEND_PORT=5002
|
||||
FRONTEND_PORT=5999
|
||||
|
||||
# ---- 管理员账号 ----
|
||||
ADMIN_USERNAME=admin
|
||||
|
||||
3
.gitignore
vendored
@ -12,6 +12,9 @@ __pycache__/
|
||||
|
||||
# 运行时数据
|
||||
backend/data/*.bak
|
||||
config.json
|
||||
ReadoorBranches/
|
||||
build/
|
||||
|
||||
# 日志
|
||||
logs/
|
||||
|
||||
40
README.md
@ -44,14 +44,17 @@ cp .env.example .env
|
||||
vim .env
|
||||
```
|
||||
|
||||
**必须修改的配置项:**
|
||||
**建议优先检查的配置项:**
|
||||
|
||||
```bash
|
||||
# iOS 源码项目路径(包含 readoor.xcworkspace 的目录)
|
||||
PROJECT_ROOT=/path/to/your/Readoor
|
||||
# 分支源码根目录,默认在当前项目下
|
||||
GIT_SOURCE_BASE=/path/to/iOSBuildServer/ReadoorBranches
|
||||
|
||||
# 打包输出目录
|
||||
BUILD_BASE_DIR=/path/to/build/output
|
||||
# 打包输出目录,默认在当前项目下
|
||||
BUILD_BASE_DIR=/path/to/iOSBuildServer/build
|
||||
|
||||
# Git 远程仓库地址(配置后可自动拉取分支)
|
||||
GIT_REMOTE_URL=git@github.com:org/repo.git
|
||||
|
||||
# 管理员密码(建议修改)
|
||||
ADMIN_PASSWORD=your_secure_password
|
||||
@ -71,17 +74,19 @@ ADMIN_PASSWORD=your_secure_password
|
||||
|
||||
### 4.(可选)初始化分支源码目录
|
||||
|
||||
如果需要按分支打包,先准备分支源码目录:
|
||||
系统现在默认直接从分支源码目录打包,每个分支对应 `GIT_SOURCE_BASE` 下的一个子目录。
|
||||
|
||||
如果已配置 `GIT_REMOTE_URL`,首次打包某个分支时会自动 clone;如果未配置,则需要先手动准备分支目录:
|
||||
|
||||
```bash
|
||||
# 创建分支源码根目录
|
||||
mkdir -p /path/to/ReadoorBranches
|
||||
|
||||
# 手动 clone 主分支(后续自动 pull 更新)
|
||||
# 手动 clone 主分支(后续可继续手动维护)
|
||||
git clone -b main git@github.com:org/repo.git /path/to/ReadoorBranches/main
|
||||
```
|
||||
|
||||
其他分支无需手动 clone,首次打包时会自动从远程仓库 clone。
|
||||
如果已配置 `GIT_REMOTE_URL`,其他分支无需手动 clone,首次打包时会自动拉取。
|
||||
|
||||
### 5.(可选)启用健康监测
|
||||
|
||||
@ -101,13 +106,12 @@ git clone -b main git@github.com:org/repo.git /path/to/ReadoorBranches/main
|
||||
|
||||
所有配置通过项目根目录的 `.env` 文件管理,修改后重启服务生效。
|
||||
|
||||
### 项目路径
|
||||
### 目录配置
|
||||
|
||||
| 变量 | 示例 | 说明 |
|
||||
|------|------|------|
|
||||
| `PROJECT_ROOT` | `/Users/shen/Work/Code/Readoor` | iOS 源码项目根目录(AutoPacking 脚本路径) |
|
||||
| `BUILD_BASE_DIR` | `/Users/shen/Documents` | 打包产物输出基础目录 |
|
||||
| `GIT_SOURCE_BASE` | `/path/to/ReadoorBranches` | 分支源码根目录,每个分支一个子目录 |
|
||||
| `GIT_SOURCE_BASE` | `/Users/shen/Work/Code/iOSBuildServer/ReadoorBranches` | 分支源码根目录,每个分支一个子目录,仅提供 iOS 工程源码 |
|
||||
| `BUILD_BASE_DIR` | `/Users/shen/Work/Code/iOSBuildServer/build` | 打包产物输出基础目录 |
|
||||
| `GIT_REMOTE_URL` | `git@github.com:org/repo.git` | 远程仓库地址,分支目录不存在时自动 clone |
|
||||
|
||||
### 服务配置
|
||||
@ -131,14 +135,20 @@ git clone -b main git@github.com:org/repo.git /path/to/ReadoorBranches/main
|
||||
|
||||
以下配置通过管理后台 Web 界面管理(存储在 `config.json`):
|
||||
|
||||
首次部署时,如果项目根目录下存在 `config.json`,系统会自动导入其中的 `apps` 和 `schemes` 作为初始配置;导入后即可继续在后台增删改。
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|--------|------|
|
||||
| App 管理 | 应用名称、Scheme、服务器环境、upload_key 等 |
|
||||
| Scheme 管理 | 打包 Scheme 配置 |
|
||||
| 服务器环境 | 测试/正式/自定义环境的 API 地址、Universal Link 等 |
|
||||
| 分支管理 | 可打包的代码分支列表 |
|
||||
| 版本号 | App_Ver 和 Build_Ver(写入 AutoPacking 脚本) |
|
||||
| 上传配置 | OSS / WebDAV 文件上传设置 |
|
||||
| 版本号 | 服务端统一管理 App_Ver 和 Build_Ver,不修改分支源码 |
|
||||
| 上传配置 | 服务端内置 OSS / WebDAV 分发,生成 IPA、manifest、下载页和二维码 |
|
||||
|
||||
### 服务端自动化资源
|
||||
|
||||
项目内的 `backend/automation/` 是 Web 打包服务唯一使用的自动化资源:默认皮肤和混淆工具都在此处维护。分支源码中的 `AutoPacking/` 不会被复制、执行或修改,仍可供开发者手动打包使用。
|
||||
|
||||
### Watchdog 配置
|
||||
|
||||
@ -496,7 +506,7 @@ tail -50 logs/server.log
|
||||
```
|
||||
|
||||
常见原因:
|
||||
- `PROJECT_ROOT` 路径不存在
|
||||
- `GIT_SOURCE_BASE` 下对应分支目录不存在,且未配置 `GIT_REMOTE_URL`
|
||||
- 磁盘空间不足
|
||||
- Python 依赖缺失(重新执行 `./deploy.sh build`)
|
||||
|
||||
|
||||
202
backend/automation/obfuscation/generate_image_whitelist.py
Normal file
@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
构建期 dylib 白名单生成脚本
|
||||
|
||||
优先扫描最终 .app Bundle 内实际嵌入并会被 dyld 加载的镜像,生成:
|
||||
1. RDKnownImages.plist — 白名单 + HMAC-SHA256 摘要
|
||||
2. Generated/RDWhitelistSecret.swift — 仅在缺失时补生成
|
||||
|
||||
在 Xcode Build Phase 中调用时,会复用现有密钥并更新最终 app bundle 内的 plist;
|
||||
手动执行时若无法定位 app bundle,则退回扫描仓库中的 framework/dylib 来源。
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import hmac
|
||||
import hashlib
|
||||
import plistlib
|
||||
import secrets
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# 所有 framework 来源目录(相对于项目根目录)
|
||||
FRAMEWORK_SEARCH_PATHS = [
|
||||
'Pods',
|
||||
'readoor/BookShelf/Classes/Factorys',
|
||||
'readoor/3.0/Factorys',
|
||||
'readoor/3.0/Features/ThirdPartyService',
|
||||
]
|
||||
|
||||
# 输出路径
|
||||
PLIST_OUTPUT = 'readoor/3.0/Base/Security/RDKnownImages.plist'
|
||||
SECRET_OUTPUT = 'readoor/3.0/Base/Security/Generated/RDWhitelistSecret.swift'
|
||||
def scan_source_frameworks(search_paths, app_name):
|
||||
"""扫描仓库中的 framework 和 dylib 来源,作为无法定位 app bundle 时的兜底方案。"""
|
||||
known = []
|
||||
seen = set()
|
||||
for search_dir in search_paths:
|
||||
if not os.path.isdir(search_dir):
|
||||
continue
|
||||
for root, dirs, files in os.walk(search_dir):
|
||||
# .framework 在 macOS 上是目录,出现在 dirs 中
|
||||
for d in dirs:
|
||||
if d.endswith('.framework'):
|
||||
basename = d.replace('.framework', '')
|
||||
rel_path = os.path.relpath(os.path.join(root, d), '.')
|
||||
key = (basename, rel_path)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
known.append({'name': basename, 'path': rel_path})
|
||||
for f in files:
|
||||
if f.endswith('.dylib'):
|
||||
basename = f.replace('.dylib', '')
|
||||
rel_path = os.path.relpath(os.path.join(root, f), '.')
|
||||
key = (basename, rel_path)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
known.append({'name': basename, 'path': rel_path})
|
||||
|
||||
# App 自身
|
||||
app_key = (app_name, app_name)
|
||||
if app_key not in seen:
|
||||
known.append({'name': app_name, 'path': app_name})
|
||||
|
||||
# 按 name 排序保证稳定性
|
||||
known.sort(key=lambda x: x['name'])
|
||||
return known
|
||||
|
||||
|
||||
def scan_bundled_images(app_bundle_path, executable_path):
|
||||
"""扫描最终 app bundle 中会实际被加载的镜像,路径使用 bundle 相对路径。"""
|
||||
known = []
|
||||
seen = set()
|
||||
|
||||
executable_name = Path(executable_path).name if executable_path else Path(app_bundle_path).stem
|
||||
main_key = (executable_name, executable_path or executable_name)
|
||||
seen.add(main_key)
|
||||
known.append({'name': executable_name, 'path': executable_path or executable_name})
|
||||
|
||||
for root, dirs, files in os.walk(app_bundle_path):
|
||||
dirs.sort()
|
||||
files.sort()
|
||||
|
||||
for d in dirs:
|
||||
if not d.endswith('.framework'):
|
||||
continue
|
||||
framework_dir = os.path.join(root, d)
|
||||
basename = d.replace('.framework', '')
|
||||
|
||||
# if basename == 'MJExtension': # 故意排除
|
||||
# continue
|
||||
|
||||
binary_path = os.path.join(framework_dir, basename)
|
||||
if not os.path.isfile(binary_path):
|
||||
continue
|
||||
rel_path = os.path.relpath(binary_path, app_bundle_path)
|
||||
key = (basename, rel_path)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
known.append({'name': basename, 'path': rel_path})
|
||||
|
||||
for f in files:
|
||||
if not f.endswith('.dylib'):
|
||||
continue
|
||||
dylib_path = os.path.join(root, f)
|
||||
basename = f.replace('.dylib', '')
|
||||
rel_path = os.path.relpath(dylib_path, app_bundle_path)
|
||||
key = (basename, rel_path)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
known.append({'name': basename, 'path': rel_path})
|
||||
|
||||
known.sort(key=lambda x: (x['name'], x['path']))
|
||||
return known
|
||||
|
||||
|
||||
def compute_digest(entries, secret):
|
||||
"""对排序后的白名单内容计算 HMAC-SHA256 摘要"""
|
||||
parts = [f"{e['name']}|{e['path']}" for e in entries]
|
||||
message = '\n'.join(parts).encode('utf-8')
|
||||
return hmac.new(secret, message, hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def generate_swift_secret(secret_bytes):
|
||||
"""将构建期密钥写入 Swift 源码"""
|
||||
hex_bytes = ', '.join(f'0x{b:02x}' for b in secret_bytes)
|
||||
source = f"""// Auto-generated by generate_image_whitelist.py — do not edit manually
|
||||
import Foundation
|
||||
|
||||
enum RDWhitelistSecret {{
|
||||
static let key: [UInt8] = [{hex_bytes}]
|
||||
}}
|
||||
"""
|
||||
os.makedirs(os.path.dirname(SECRET_OUTPUT), exist_ok=True)
|
||||
with open(SECRET_OUTPUT, 'w') as f:
|
||||
f.write(source)
|
||||
|
||||
|
||||
def load_existing_secret():
|
||||
if not os.path.isfile(SECRET_OUTPUT):
|
||||
return None
|
||||
with open(SECRET_OUTPUT, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
hex_values = re.findall(r'0x([0-9a-fA-F]{2})', content)
|
||||
if not hex_values:
|
||||
return None
|
||||
return bytes(int(value, 16) for value in hex_values)
|
||||
|
||||
|
||||
def resolve_app_bundle_path():
|
||||
target_build_dir = os.environ.get('TARGET_BUILD_DIR')
|
||||
wrapper_name = os.environ.get('WRAPPER_NAME')
|
||||
if target_build_dir and wrapper_name:
|
||||
app_bundle_path = os.path.join(target_build_dir, wrapper_name)
|
||||
if os.path.isdir(app_bundle_path):
|
||||
return app_bundle_path
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
os.chdir(root)
|
||||
|
||||
build_secret = load_existing_secret()
|
||||
if build_secret is None:
|
||||
build_secret = secrets.token_bytes(32)
|
||||
generate_swift_secret(build_secret)
|
||||
print(f'[whitelist] Generated {SECRET_OUTPUT}')
|
||||
|
||||
app_bundle_path = resolve_app_bundle_path()
|
||||
executable_path = os.environ.get('EXECUTABLE_PATH', 'readoor')
|
||||
if app_bundle_path:
|
||||
frameworks = scan_bundled_images(app_bundle_path, executable_path)
|
||||
print(f'[whitelist] Scanned app bundle images from {app_bundle_path}')
|
||||
else:
|
||||
frameworks = scan_source_frameworks(FRAMEWORK_SEARCH_PATHS, 'readoor')
|
||||
print('[whitelist] App bundle not found, fallback to source framework scan')
|
||||
|
||||
digest = compute_digest(frameworks, build_secret)
|
||||
|
||||
output = {
|
||||
'known_images': frameworks,
|
||||
'digest': digest
|
||||
}
|
||||
|
||||
if app_bundle_path:
|
||||
destination = os.path.join(app_bundle_path, os.path.basename(PLIST_OUTPUT))
|
||||
os.makedirs(os.path.dirname(destination), exist_ok=True)
|
||||
with open(destination, 'wb') as f:
|
||||
plistlib.dump(output, f)
|
||||
print(f'[whitelist] Generated app bundle plist {destination} with {len(frameworks)} dynamic entries')
|
||||
else:
|
||||
os.makedirs(os.path.dirname(PLIST_OUTPUT), exist_ok=True)
|
||||
with open(PLIST_OUTPUT, 'wb') as f:
|
||||
plistlib.dump(output, f)
|
||||
print(f'[whitelist] Generated {PLIST_OUTPUT} with {len(frameworks)} dynamic entries')
|
||||
|
||||
print(f'[whitelist] digest={digest[:16]}...')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
4293
backend/automation/obfuscation/obfuscate_symbols.py
Normal file
144
backend/automation/obfuscation/obfuscation_runtime/canonical.py
Normal file
@ -0,0 +1,144 @@
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
|
||||
def build_canonical_symbol_tables(
|
||||
swift_files: list,
|
||||
objc_files: list,
|
||||
method_files: list,
|
||||
*,
|
||||
enable_objc_symbols: bool,
|
||||
prefilter_files,
|
||||
type_pattern: str,
|
||||
method_pattern: str,
|
||||
extract_swift_symbols,
|
||||
extract_objc_symbols,
|
||||
extract_methods,
|
||||
objc_symbol_prefixes: tuple,
|
||||
already_obfuscated_re,
|
||||
symbol_whitelist,
|
||||
symbol_prefix_whitelist: tuple,
|
||||
is_system_method,
|
||||
split_swift_signature_key,
|
||||
) -> dict:
|
||||
swift_symbol_files = prefilter_files(swift_files, type_pattern)
|
||||
objc_symbol_files = prefilter_files(objc_files, type_pattern) if enable_objc_symbols else []
|
||||
method_symbol_files = prefilter_files(method_files, method_pattern)
|
||||
|
||||
swift_symbols = set()
|
||||
objc_symbols = set()
|
||||
direct_methods = set()
|
||||
objc_selectors = set()
|
||||
swift_signatures = {}
|
||||
|
||||
for file_path in swift_symbol_files:
|
||||
swift_symbols.update(extract_swift_symbols(file_path))
|
||||
|
||||
for file_path in objc_symbol_files:
|
||||
objc_symbols.update(extract_objc_symbols(file_path))
|
||||
|
||||
for file_path in method_symbol_files:
|
||||
method_data = extract_methods(file_path)
|
||||
direct_methods.update(method_data["direct_names"])
|
||||
objc_selectors.update(method_data["objc_selectors"])
|
||||
swift_signatures.update(method_data["swift_signatures"])
|
||||
|
||||
canonical = {
|
||||
"swift_symbols": sorted(swift_symbols),
|
||||
"objc_symbols": [
|
||||
name for name in sorted(objc_symbols)
|
||||
if any(name.startswith(prefix) for prefix in objc_symbol_prefixes)
|
||||
and not already_obfuscated_re.match(name)
|
||||
and name not in symbol_whitelist
|
||||
and not name.startswith(symbol_prefix_whitelist)
|
||||
],
|
||||
"direct_methods": [
|
||||
name for name in sorted(direct_methods)
|
||||
if name and not already_obfuscated_re.match(name) and name not in symbol_whitelist and not is_system_method(name)
|
||||
],
|
||||
"objc_selectors": [
|
||||
name for name in sorted(objc_selectors)
|
||||
if name and not already_obfuscated_re.match(name) and name not in symbol_whitelist and not is_system_method(name)
|
||||
],
|
||||
"swift_signatures": {},
|
||||
}
|
||||
|
||||
for signature_key in sorted(swift_signatures.keys()):
|
||||
base_name, _ = split_swift_signature_key(signature_key)
|
||||
if not base_name:
|
||||
continue
|
||||
if already_obfuscated_re.match(base_name) or base_name in symbol_whitelist or is_system_method(base_name):
|
||||
continue
|
||||
canonical["swift_signatures"][signature_key] = swift_signatures[signature_key]
|
||||
|
||||
digest_payload = {
|
||||
key: canonical[key]
|
||||
for key in ("swift_symbols", "objc_symbols", "direct_methods", "objc_selectors", "swift_signatures")
|
||||
}
|
||||
canonical["digest"] = hashlib.sha256(
|
||||
json.dumps(digest_payload, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
||||
).hexdigest()
|
||||
return canonical
|
||||
|
||||
|
||||
def build_type_mappings_from_canonical_table(canonical_tables: dict, salt: str, *, enable_objc_symbols: bool, build_unique_mapping, type_prefix: str) -> tuple:
|
||||
ordered_names = []
|
||||
seen = set()
|
||||
for bucket_name in ("objc_symbols", "swift_symbols"):
|
||||
for name in canonical_tables.get(bucket_name, []):
|
||||
if name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
ordered_names.append(name)
|
||||
|
||||
combined_mapping = build_unique_mapping(ordered_names, salt, prefix=type_prefix)
|
||||
mapping_swift = {
|
||||
name: combined_mapping[name]
|
||||
for name in canonical_tables.get("swift_symbols", [])
|
||||
if name in combined_mapping
|
||||
}
|
||||
mapping_objc = {}
|
||||
if enable_objc_symbols:
|
||||
mapping_objc = {
|
||||
name: combined_mapping[name]
|
||||
for name in canonical_tables.get("objc_symbols", [])
|
||||
if name in combined_mapping
|
||||
}
|
||||
return mapping_swift, mapping_objc
|
||||
|
||||
|
||||
def build_method_mapping_from_canonical_table(canonical_tables: dict, salt: str, *, build_unique_mapping, method_prefix: str) -> dict:
|
||||
swift_signature_mapping = canonical_tables.get("swift_signatures", {})
|
||||
ordered_names = []
|
||||
seen = set()
|
||||
for bucket in (
|
||||
canonical_tables.get("direct_methods", []),
|
||||
canonical_tables.get("objc_selectors", []),
|
||||
sorted(swift_signature_mapping.keys()),
|
||||
):
|
||||
for name in bucket:
|
||||
if name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
ordered_names.append(name)
|
||||
|
||||
combined_tokens = build_unique_mapping(ordered_names, salt, prefix=method_prefix)
|
||||
method_mapping = {
|
||||
name: combined_tokens[name]
|
||||
for name in canonical_tables.get("direct_methods", [])
|
||||
if name in combined_tokens
|
||||
}
|
||||
method_mapping.update({
|
||||
name: combined_tokens[name]
|
||||
for name in canonical_tables.get("objc_selectors", [])
|
||||
if name in combined_tokens
|
||||
})
|
||||
|
||||
for signature_key in sorted(swift_signature_mapping.keys()):
|
||||
if signature_key not in combined_tokens:
|
||||
continue
|
||||
entry = dict(swift_signature_mapping[signature_key])
|
||||
entry["obfuscated"] = combined_tokens[signature_key]
|
||||
method_mapping[signature_key] = entry
|
||||
|
||||
return method_mapping
|
||||
143
backend/automation/obfuscation/obfuscation_runtime/discovery.py
Normal file
@ -0,0 +1,143 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def path_matches_dir_names(path: str, dir_names: tuple) -> bool:
|
||||
if not dir_names:
|
||||
return False
|
||||
wanted = {name.lower() for name in dir_names}
|
||||
return any(part.lower() in wanted for part in Path(path).parts)
|
||||
|
||||
|
||||
def should_exclude_path(path: str, exclude_path_fragments) -> bool:
|
||||
return any(fragment in path for fragment in exclude_path_fragments)
|
||||
|
||||
|
||||
def _run_command_capture_stdout(args: list) -> str:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
if completed.returncode not in (0, 1):
|
||||
return ""
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def _discover_files_via_os_walk(scan_dirs: list, allowed_exts: tuple, exclude_path_fragments, skip_dir_names: tuple = (), basename_prefixes: tuple = ()) -> list:
|
||||
files = []
|
||||
allowed_exts = set(allowed_exts)
|
||||
for scan_dir in scan_dirs:
|
||||
for root, dirs, filenames in os.walk(scan_dir):
|
||||
dirs[:] = [
|
||||
directory for directory in dirs
|
||||
if not any(fragment.strip("/") in directory for fragment in exclude_path_fragments)
|
||||
]
|
||||
if path_matches_dir_names(root, skip_dir_names):
|
||||
continue
|
||||
for filename in filenames:
|
||||
ext = os.path.splitext(filename)[1]
|
||||
if ext not in allowed_exts:
|
||||
continue
|
||||
name_no_ext = os.path.splitext(filename)[0]
|
||||
if basename_prefixes and not any(name_no_ext.startswith(prefix) for prefix in basename_prefixes):
|
||||
continue
|
||||
full_path = os.path.normpath(os.path.join(root, filename))
|
||||
if should_exclude_path(full_path, exclude_path_fragments):
|
||||
continue
|
||||
if path_matches_dir_names(full_path, skip_dir_names):
|
||||
continue
|
||||
files.append(full_path)
|
||||
return sorted(set(files))
|
||||
|
||||
|
||||
def _discover_files_via_rg(scan_dirs: list, allowed_exts: tuple, exclude_path_fragments, skip_dir_names: tuple = (), basename_prefixes: tuple = ()) -> list:
|
||||
rg = shutil.which("rg")
|
||||
if not rg:
|
||||
return []
|
||||
|
||||
cmd = [rg, "--files", "--no-ignore", "--hidden"]
|
||||
for ext in allowed_exts:
|
||||
cmd.extend(["-g", f"*{ext}"])
|
||||
cmd.extend(scan_dirs)
|
||||
|
||||
output = _run_command_capture_stdout(cmd)
|
||||
if not output:
|
||||
return []
|
||||
|
||||
files = []
|
||||
for line in output.splitlines():
|
||||
path = os.path.normpath(line.strip())
|
||||
if not path or should_exclude_path(path, exclude_path_fragments):
|
||||
continue
|
||||
if path_matches_dir_names(path, skip_dir_names):
|
||||
continue
|
||||
if basename_prefixes:
|
||||
name_no_ext = os.path.splitext(os.path.basename(path))[0]
|
||||
if not any(name_no_ext.startswith(prefix) for prefix in basename_prefixes):
|
||||
continue
|
||||
files.append(path)
|
||||
|
||||
return sorted(set(files))
|
||||
|
||||
|
||||
def discover_files(scan_dirs: list, allowed_exts: tuple, exclude_path_fragments, skip_dir_names: tuple = (), basename_prefixes: tuple = ()) -> list:
|
||||
files = _discover_files_via_rg(
|
||||
scan_dirs,
|
||||
allowed_exts,
|
||||
exclude_path_fragments,
|
||||
skip_dir_names=skip_dir_names,
|
||||
basename_prefixes=basename_prefixes,
|
||||
)
|
||||
if files:
|
||||
return files
|
||||
return _discover_files_via_os_walk(
|
||||
scan_dirs,
|
||||
allowed_exts,
|
||||
exclude_path_fragments,
|
||||
skip_dir_names=skip_dir_names,
|
||||
basename_prefixes=basename_prefixes,
|
||||
)
|
||||
|
||||
|
||||
def _chunked(items: list, chunk_size: int):
|
||||
for index in range(0, len(items), chunk_size):
|
||||
yield items[index:index + chunk_size]
|
||||
|
||||
|
||||
def prefilter_files_with_rg(files: list, pattern: str, chunk_size: int = 256) -> list:
|
||||
files = sorted(set(files))
|
||||
if not files:
|
||||
return []
|
||||
|
||||
rg = shutil.which("rg")
|
||||
if not rg:
|
||||
return files
|
||||
|
||||
matched = set()
|
||||
for chunk in _chunked(files, chunk_size):
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[rg, "-l", "--no-messages", "-e", pattern, *chunk],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except Exception:
|
||||
return files
|
||||
|
||||
if completed.returncode not in (0, 1):
|
||||
return files
|
||||
for line in completed.stdout.splitlines():
|
||||
path = os.path.normpath(line.strip())
|
||||
if path:
|
||||
matched.add(path)
|
||||
|
||||
return sorted(matched)
|
||||
128
backend/automation/obfuscation/obfuscation_runtime/json_cache.py
Normal file
@ -0,0 +1,128 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_json_file(path: str, default):
|
||||
try:
|
||||
return json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return default() if callable(default) else default
|
||||
|
||||
|
||||
def save_json_file(path: str, payload):
|
||||
Path(path).write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
class IncrementalAnalysisCache:
|
||||
def __init__(self, cache_path: str, version: int):
|
||||
self.cache_path = cache_path
|
||||
self.version = version
|
||||
self._state = None
|
||||
self._dirty = False
|
||||
self._memo = {}
|
||||
|
||||
def _default_state(self) -> dict:
|
||||
return {
|
||||
"version": self.version,
|
||||
"files": {},
|
||||
}
|
||||
|
||||
def load_state(self) -> dict:
|
||||
if self._state is not None:
|
||||
return self._state
|
||||
|
||||
payload = load_json_file(self.cache_path, self._default_state)
|
||||
if payload.get("version") != self.version or not isinstance(payload.get("files"), dict):
|
||||
payload = self._default_state()
|
||||
|
||||
self._state = payload
|
||||
return self._state
|
||||
|
||||
def save_if_dirty(self):
|
||||
if not self._dirty:
|
||||
return
|
||||
|
||||
try:
|
||||
save_json_file(self.cache_path, self.load_state())
|
||||
self._dirty = False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def build_cache_key(analysis_kind: str, path: str) -> str:
|
||||
return f"{analysis_kind}:{os.path.realpath(path)}"
|
||||
|
||||
@staticmethod
|
||||
def _read_text_with_hash(path: str):
|
||||
try:
|
||||
text = Path(path).read_text(encoding="utf-8", errors="ignore")
|
||||
except Exception:
|
||||
return None, None
|
||||
return text, hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
def get_or_compute(self, path: str, analysis_kind: str, analyzer):
|
||||
cache_key = self.build_cache_key(analysis_kind, path)
|
||||
memo_key = (analysis_kind, os.path.realpath(path))
|
||||
if memo_key in self._memo:
|
||||
return self._memo[memo_key]
|
||||
|
||||
cache_state = self.load_state()
|
||||
files_cache = cache_state.setdefault("files", {})
|
||||
cached_entry = files_cache.get(cache_key)
|
||||
|
||||
try:
|
||||
stat = os.stat(path)
|
||||
stat_signature = {
|
||||
"mtime_ns": stat.st_mtime_ns,
|
||||
"size": stat.st_size,
|
||||
}
|
||||
except Exception:
|
||||
stat_signature = {
|
||||
"mtime_ns": None,
|
||||
"size": None,
|
||||
}
|
||||
|
||||
if (
|
||||
isinstance(cached_entry, dict)
|
||||
and cached_entry.get("mtime_ns") == stat_signature["mtime_ns"]
|
||||
and cached_entry.get("size") == stat_signature["size"]
|
||||
and isinstance(cached_entry.get("analysis"), dict)
|
||||
):
|
||||
analysis = cached_entry["analysis"]
|
||||
self._memo[memo_key] = analysis
|
||||
return analysis
|
||||
|
||||
text, text_hash = self._read_text_with_hash(path)
|
||||
if text is None:
|
||||
analysis = analyzer("", path)
|
||||
self._memo[memo_key] = analysis
|
||||
return analysis
|
||||
|
||||
if (
|
||||
isinstance(cached_entry, dict)
|
||||
and cached_entry.get("sha256") == text_hash
|
||||
and isinstance(cached_entry.get("analysis"), dict)
|
||||
):
|
||||
cached_entry.update(stat_signature)
|
||||
self._dirty = True
|
||||
analysis = cached_entry["analysis"]
|
||||
self._memo[memo_key] = analysis
|
||||
return analysis
|
||||
|
||||
analysis = analyzer(text, path)
|
||||
files_cache[cache_key] = {
|
||||
"path": os.path.realpath(path),
|
||||
"analysis_kind": analysis_kind,
|
||||
"mtime_ns": stat_signature["mtime_ns"],
|
||||
"size": stat_signature["size"],
|
||||
"sha256": text_hash,
|
||||
"analysis": analysis,
|
||||
}
|
||||
self._dirty = True
|
||||
self._memo[memo_key] = analysis
|
||||
return analysis
|
||||
44
backend/automation/obfuscation/scripts/archive_obfuscate_post.sh
Executable file
@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Xcode Run Script Phase (Post) for Archive obfuscation.
|
||||
# Add this as a late Build Phase (after Compile Sources / near end).
|
||||
# It restores original files backed up by archive_obfuscate_pre.sh.
|
||||
|
||||
if [[ "${ACTION:-}" != "install" || "${CONFIGURATION:-}" != "Release" ]]; then
|
||||
echo "[obf-post] Skip: ACTION=${ACTION:-}, CONFIGURATION=${CONFIGURATION:-}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
DERIVED_DIR="${DERIVED_FILE_DIR:-/tmp}"
|
||||
MARKER_FILE="${DERIVED_DIR}/rd_obf_marker_${TARGET_NAME:-app}.env"
|
||||
|
||||
if [[ ! -f "${MARKER_FILE}" ]]; then
|
||||
echo "[obf-post] No marker file found. Skip restore."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "${MARKER_FILE}"
|
||||
|
||||
if [[ -z "${BACKUP_DIR:-}" || -z "${ROOT:-}" ]]; then
|
||||
echo "[obf-post] Marker invalid, skip restore."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -d "${BACKUP_DIR}" ]]; then
|
||||
echo "[obf-post] Backup dir missing: ${BACKUP_DIR}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[obf-post] Restoring source files from ${BACKUP_DIR}"
|
||||
cd "${ROOT}"
|
||||
|
||||
while IFS= read -r file; do
|
||||
rel="${file#${BACKUP_DIR}/}"
|
||||
mkdir -p "$(dirname "${ROOT}/$rel")"
|
||||
cp -f "$file" "${ROOT}/$rel"
|
||||
done < <(find "${BACKUP_DIR}" -type f)
|
||||
|
||||
rm -rf "${BACKUP_DIR}" "${MARKER_FILE}"
|
||||
echo "[obf-post] Done."
|
||||
110
backend/automation/obfuscation/scripts/archive_obfuscate_pre.sh
Executable file
@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Xcode Run Script Phase (Pre) for Archive obfuscation.
|
||||
# Add this as an early Build Phase (before Compile Sources).
|
||||
# It only runs on Archive + Release, and obfuscates source in-place,
|
||||
# then the post phase restores original files from backup.
|
||||
|
||||
if [[ "${ACTION:-}" != "install" || "${CONFIGURATION:-}" != "Release" ]]; then
|
||||
echo "[obf-pre] Skip: ACTION=${ACTION:-}, CONFIGURATION=${CONFIGURATION:-}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Allow external driver (CI/script) to opt-out of running this phase
|
||||
if [[ "${SKIP_OBF_PHASE:-}" = "1" ]]; then
|
||||
echo "[obf-pre] SKIP_OBF_PHASE=1 set; skipping pre-archive obfuscation phase."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ROOT="${SRCROOT:-$(cd "$(dirname "$0")/../../.." && pwd)}"
|
||||
DERIVED_DIR="${DERIVED_FILE_DIR:-/tmp}"
|
||||
STAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
BACKUP_DIR="${DERIVED_DIR}/rd_obf_backup_${STAMP}"
|
||||
MARKER_FILE="${DERIVED_DIR}/rd_obf_marker_${TARGET_NAME:-app}.env"
|
||||
MAP_DIR="${ROOT}/obfuscation_maps"
|
||||
MAP_PATH="${MAP_DIR}/${TARGET_NAME:-app}_${STAMP}.json"
|
||||
METHOD_MAP_PATH="${MAP_DIR}/method_map_${TARGET_NAME:-app}_${STAMP}.json"
|
||||
PRE_SUCCESS=0
|
||||
|
||||
restore_from_backup_dir() {
|
||||
local backup_dir="$1"
|
||||
if [[ ! -d "${backup_dir}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS= read -r file; do
|
||||
rel="${file#${backup_dir}/}"
|
||||
mkdir -p "$(dirname "${ROOT}/$rel")"
|
||||
cp -f "$file" "${ROOT}/$rel"
|
||||
done < <(find "${backup_dir}" -type f)
|
||||
|
||||
rm -rf "${backup_dir}"
|
||||
}
|
||||
|
||||
restore_stale_backup_if_needed() {
|
||||
if [[ ! -f "${MARKER_FILE}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "[obf-pre] Detected stale marker from previous build, restoring workspace first..."
|
||||
# shellcheck disable=SC1090
|
||||
source "${MARKER_FILE}"
|
||||
if [[ -n "${BACKUP_DIR:-}" && -n "${ROOT:-}" ]]; then
|
||||
restore_from_backup_dir "${BACKUP_DIR}"
|
||||
fi
|
||||
rm -f "${MARKER_FILE}"
|
||||
}
|
||||
|
||||
restore_on_error() {
|
||||
local exit_code=$?
|
||||
if [[ ${PRE_SUCCESS} -eq 1 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "[obf-pre] Failed (exit=${exit_code}), restoring source files from backup..."
|
||||
restore_from_backup_dir "${BACKUP_DIR}"
|
||||
|
||||
rm -f "${MARKER_FILE}"
|
||||
echo "[obf-pre] Restore complete."
|
||||
exit ${exit_code}
|
||||
}
|
||||
|
||||
# Run repository self-check to ensure no residual obfuscated tokens remain.
|
||||
trap restore_on_error EXIT INT TERM
|
||||
|
||||
cd "${ROOT}"
|
||||
|
||||
restore_stale_backup_if_needed
|
||||
|
||||
echo "[obf-pre] Running repository self-check"
|
||||
# Self-check must run before creating backups to avoid restoring a pre-check backup on failure.
|
||||
bash "${ROOT}/AutoPacking/obfuscation/scripts/obfuscation_self_check.sh" "${ROOT}"
|
||||
|
||||
echo "[obf-pre] Generating dylib whitelist with HMAC"
|
||||
python3 "${ROOT}/AutoPacking/obfuscation/generate_image_whitelist.py"
|
||||
|
||||
mkdir -p "${BACKUP_DIR}" "${MAP_DIR}"
|
||||
|
||||
echo "[obf-pre] Backing up source files to ${BACKUP_DIR}"
|
||||
# Backup all source files that may be touched by obfuscation. Keep this for rollback safety.
|
||||
while IFS= read -r file; do
|
||||
rel="${file#./}"
|
||||
mkdir -p "${BACKUP_DIR}/$(dirname "$rel")"
|
||||
cp "$file" "${BACKUP_DIR}/$rel"
|
||||
done < <(
|
||||
find . \
|
||||
\( -path './Pods' -o -path './Pods/*' -o -path './.git' -o -path './.git/*' -o -path './build_output' -o -path './build_output/*' -o -path './DerivedData' -o -path './DerivedData/*' -o -path './readoorTests' -o -path './readoorTests/*' -o -name '*.framework' -o -name '*.xcframework' \) -prune -o \
|
||||
-type f \( -name '*.swift' -o -name '*.m' -o -name '*.mm' -o -name '*.h' -o -name '*.pch' \) -print
|
||||
)
|
||||
|
||||
echo "[obf-pre] Running obfuscation in archive mode (auto apply + type/method maps)..."
|
||||
OBF_TIMESTAMP="${STAMP}" python3 "${ROOT}/AutoPacking/obfuscation/obfuscate_symbols.py" --archive-mode
|
||||
|
||||
echo "BACKUP_DIR=${BACKUP_DIR}" > "${MARKER_FILE}"
|
||||
echo "ROOT=${ROOT}" >> "${MARKER_FILE}"
|
||||
|
||||
PRE_SUCCESS=1
|
||||
trap - EXIT INT TERM
|
||||
|
||||
echo "[obf-pre] Done. map=${MAP_PATH} method_map=${METHOD_MAP_PATH} marker=${MARKER_FILE}"
|
||||
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Scan repository for residual obfuscated tokens like RdXXXXXX and fail if any found.
|
||||
# Exits 0 when clean, non-zero when tokens are present.
|
||||
|
||||
ROOT="${1:-$(cd "$(dirname "$0")/../../.." && pwd)}"
|
||||
|
||||
echo "[obf-check] Scanning for residual obfuscated tokens in ${ROOT}"
|
||||
|
||||
EXCLUDE=("./Pods" "./.git" "./obfuscation_maps" "./build_output" "./DerivedData" "./readoorTests")
|
||||
|
||||
GREP_EXCLUDE_ARGS=()
|
||||
for e in "${EXCLUDE[@]}"; do
|
||||
GREP_EXCLUDE_ARGS+=(--exclude-dir "${e#./}")
|
||||
done
|
||||
|
||||
FOUND=0
|
||||
TOKEN_SUFFIX_RE='(?:[a-f0-9]{6}|[a-f0-9]{8}|[a-f0-9]{10})'
|
||||
|
||||
echo "[obf-check] Looking for token pattern '\b(?:Rd|YMH)${TOKEN_SUFFIX_RE}\b' in source files..."
|
||||
if grep -R --line-number -E "\\b(?:Rd|YMH)${TOKEN_SUFFIX_RE}\\b" --include='*.swift' --include='*.m' --include='*.mm' --include='*.h' "${ROOT}" "${GREP_EXCLUDE_ARGS[@]}"; then
|
||||
FOUND=1
|
||||
fi
|
||||
|
||||
echo "[obf-check] Looking for obfuscated header imports like \"(Rd|YMH)XXXXXX.h\"..."
|
||||
if grep -R --line-number -E "#import\\s+\"(?:Rd|YMH)${TOKEN_SUFFIX_RE}\\.h\"" --include='*.pch' --include='*.h' --include='*.m' --include='*.mm' "${ROOT}" "${GREP_EXCLUDE_ARGS[@]}"; then
|
||||
FOUND=1
|
||||
fi
|
||||
|
||||
if [[ ${FOUND} -ne 0 ]]; then
|
||||
echo "[obf-check] ERROR: Found residual obfuscated tokens; aborting to avoid double-obfuscation."
|
||||
echo "[obf-check] Run: python3 AutoPacking/obfuscation/obfuscate_symbols.py --restore-source-from-map-dir obfuscation_maps or run the auto-restore tool before archiving."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "[obf-check] Clean: no residual obfuscated tokens found."
|
||||
exit 0
|
||||
21
backend/automation/obfuscation/system_methods_ignore.txt
Normal file
@ -0,0 +1,21 @@
|
||||
# BEGIN AUTO-DETECTED-OBJC-DOT-SYNTAX
|
||||
# Auto-generated: Obj-C zero-arg methods that are referenced with dot syntax.
|
||||
# These names are excluded from direct-method obfuscation to avoid half-rewritten call sites.
|
||||
XMLString
|
||||
imageLayer
|
||||
isSquareCover
|
||||
middleX
|
||||
middleY
|
||||
needsUpdateFont
|
||||
observingKeys
|
||||
orgidStr
|
||||
placeholderTextView
|
||||
refreshUI
|
||||
removeAudioView
|
||||
rootElement
|
||||
searchHotkeyBlock
|
||||
showRhythmicRedivisionView
|
||||
socket4FD
|
||||
socket6FD
|
||||
vocabularyCount
|
||||
# END AUTO-DETECTED-OBJC-DOT-SYNTAX
|
||||
120
backend/automation/themes/ECNUP/AppIcon.appiconset/Contents.json
Normal file
@ -0,0 +1,120 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-20@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-20@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-29@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-29@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "38x38",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-38@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "38x38",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-38@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-40@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-40@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "60x60",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-60@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "60x60",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-60@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "64x64",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-64@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "64x64",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-64@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "68x68",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-68@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "76x76",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-76@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "83.5x83.5",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-83.5@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "1024x1024",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-1024.png",
|
||||
"scale": "1x",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "icon.wuruihong.com"
|
||||
}
|
||||
}
|
||||
BIN
backend/automation/themes/ECNUP/AppIcon.appiconset/icon-1024.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
BIN
backend/automation/themes/ECNUP/splash.imageset/1242_2688.png
vendored
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
backend/automation/themes/ECNUP/splash.imageset/2048_2732.png
vendored
Normal file
|
After Width: | Height: | Size: 428 KiB |
BIN
backend/automation/themes/ECNUP/splash.imageset/828_1792.png
vendored
Normal file
|
After Width: | Height: | Size: 140 KiB |
23
backend/automation/themes/ECNUP/splash.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "828_1792.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "1242_2688.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"filename" : "2048_2732.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
120
backend/automation/themes/bj/AppIcon.appiconset/Contents.json
Normal file
@ -0,0 +1,120 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-20@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-20@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-29@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-29@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "38x38",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-38@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "38x38",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-38@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-40@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-40@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "60x60",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-60@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "60x60",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-60@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "64x64",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-64@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "64x64",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-64@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "68x68",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-68@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "76x76",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-76@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "83.5x83.5",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-83.5@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "1024x1024",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-1024.png",
|
||||
"scale": "1x",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "icon.wuruihong.com"
|
||||
}
|
||||
}
|
||||
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-1024.png
Normal file
|
After Width: | Height: | Size: 341 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-20@2x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-20@3x.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-29@2x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-29@3x.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-38@2x.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-38@3x.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-40@2x.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-40@3x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-60@2x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-60@3x.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-64@2x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-64@3x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-68@2x.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-76@2x.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
backend/automation/themes/bj/AppIcon.appiconset/icon-83.5@2x.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
backend/automation/themes/bj/splash.imageset/1242_2688.png
vendored
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
backend/automation/themes/bj/splash.imageset/2048_2732.png
vendored
Normal file
|
After Width: | Height: | Size: 731 KiB |
BIN
backend/automation/themes/bj/splash.imageset/828_1792.png
vendored
Normal file
|
After Width: | Height: | Size: 119 KiB |
23
backend/automation/themes/bj/splash.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "828_1792.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "1242_2688.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"filename" : "2048_2732.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
120
backend/automation/themes/byxt/AppIcon.appiconset/Contents.json
Normal file
@ -0,0 +1,120 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-20@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-20@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-29@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-29@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "38x38",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-38@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "38x38",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-38@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-40@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-40@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "60x60",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-60@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "60x60",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-60@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "64x64",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-64@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "64x64",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-64@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "68x68",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-68@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "76x76",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-76@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "83.5x83.5",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-83.5@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "1024x1024",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-1024.png",
|
||||
"scale": "1x",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "icon.wuruihong.com"
|
||||
}
|
||||
}
|
||||
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-1024.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-20@2x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-20@3x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-29@2x.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-29@3x.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-38@2x.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-38@3x.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-40@2x.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-40@3x.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-60@2x.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-60@3x.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-64@2x.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-64@3x.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-68@2x.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
backend/automation/themes/byxt/AppIcon.appiconset/icon-76@2x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
BIN
backend/automation/themes/byxt/splash.imageset/1242_2688.png
vendored
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
backend/automation/themes/byxt/splash.imageset/2048_2732.png
vendored
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
backend/automation/themes/byxt/splash.imageset/828_1792.png
vendored
Normal file
|
After Width: | Height: | Size: 84 KiB |
23
backend/automation/themes/byxt/splash.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "828_1792.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "1242_2688.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"filename" : "2048_2732.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
120
backend/automation/themes/dxsx/AppIcon.appiconset/Contents.json
Normal file
@ -0,0 +1,120 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-20@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-20@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-29@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-29@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "38x38",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-38@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "38x38",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-38@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-40@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-40@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "60x60",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-60@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "60x60",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-60@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "64x64",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-64@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "64x64",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-64@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "68x68",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-68@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "76x76",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-76@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "83.5x83.5",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-83.5@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "1024x1024",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-1024.png",
|
||||
"scale": "1x",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "icon.wuruihong.com"
|
||||
}
|
||||
}
|
||||
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-1024.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-20@2x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-20@3x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-29@2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-29@3x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-38@2x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-38@3x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-40@2x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-40@3x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-60@2x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-60@3x.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-64@2x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-64@3x.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-68@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
backend/automation/themes/dxsx/AppIcon.appiconset/icon-76@2x.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
28
backend/automation/themes/dxsx/license.crt
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN ALI VIDEO CERT-----
|
||||
LmlsQS5jaUwAAWRjY2FhYTU1OTAxNzE0NDg2OWJlOGJkNzczZTc5ZjAzM2YwMDhiO
|
||||
WY5ODUwNTc5MGY4NDEyOTRiMzNjMmZiYjg1NGUxZThlNjAwNzRmNTgxZTU0MDY2Mj
|
||||
RiMGFiM2JkNWVmOTE3ZDE0MDJlNDQ0ZDUwNmExMTc5ZjFlYjYyODQ2MDRiYzdiYjA
|
||||
zOTI2ZjYyOTE1OWVjYjY0ZDk1ZDhhN2Q5ZWZlOTRjOTdkMTA0Yzc1MWU5OGEwNGY3
|
||||
OTdkOTdiYTAzMWEyMDFlMzc2Mjg0NGI1MTEyYWRkZDQzNTgyZDA2NTJkYTk0MDk1Y
|
||||
TIxYjY2NWJmYjY0ZTkwMWQyZGNjNmVjMjY1ODFkYmJjZjBhY2ZmZjFhNDU3NTJjND
|
||||
QwNTMzZmJlZDk3YTIzMmMyOWQ4YWVkZDY0YmZhZDc2ZDY1MTRiZGU5MmM3YTdjMGZ
|
||||
iYjI3OTNhNjZiMDEzMjBkODgxYzBhMjA1YTExN2Q1YmU3ZTgyMzYyYzI5MDJhZDNk
|
||||
MmFiZTcxNTQ5NzI2YzI4NTVjOTc5MWVmZDVjMGU4NDZmMDZlZDhlYTY1MjgzYWUzM
|
||||
2JkOTYzOWJiZTkwOGU4ZTBmY2QwNDc1OWQ5N2U3OTk1NDNhMzdkNDQ2YzZlMDJhMj
|
||||
BkODI2YTUzZGIzNTNjNWU3MDVjYmJmYzg5ODVjYjRkMWIxAAAB4y7ljgAlrpr0vwG
|
||||
geEX9x69jHpLId+kMOlcrZQfEF6SUS+ACr/SK46qq7Eoy08BaH0DPOY5wioF3TFXt
|
||||
PYkIzbs6nOK1dV2UJcB7FNusUrrNngoWw2qEateHRVXCjNBFW0UM5SNVWT9lriH3/
|
||||
IntvmpfVlr5fspmZmJ71PUDk1d7zCj2Lk4Y/B3WXndJ5XCfx82miYVbaTlS+ZOKDO
|
||||
0npHl22hqbxLSDN5a0RqKfj12MlUiNdGbRhuUA/NasXCulcPuvkEje47zW6o+Qtvb
|
||||
uBmP8+5gXO3ZMi0Le3TRue3dApzVtCe1lXr6Xrt1+jCNijIhBRgHgXfPZ3HSfZNuz
|
||||
of7ks1FEAAAAxH5P9HMl2FvUtxV4KPcco1PxKYr0+29gpvTMAhZ2PxDUlnUEmdRNN
|
||||
tD3plAyk+D91IbXC8w5Fq0WZy8iE8twCheABJRuwUqDeJKQbgEkmhr98HTq2877wX
|
||||
mkZAoPFjHwlIEOdf2Sih3SraFH0GzfYExG4ZItOTigyXOM5BjZPg8At1soyU1ldNx
|
||||
CWdL7Zn5OWfkkUPsXLvKyYMYpunRpei46nJMIQ9aICDokMwZfCpkU83pVP30bjea+
|
||||
E6keAf6K5x6Nfn7kw2Njo8kNT4ZUDz1vpCZR0OZjSeUplLPK357JCVwxactLVcT8p
|
||||
1VJFvvzEcNdBmxQ37/MkzWAWJ7NEf4AAAGZsb2HiAAAAGoAAAAgZzE1MDYyZTVjNz
|
||||
EyNDgxMWFkN2E3Njg1MjJhYWM1NGQAAAACAAAAHQAAAAAAAAACAAAAEWNvbS5lY25
|
||||
1cC5hYVMxNmQzAAAAHQAAAAAAAAABAAAAEWNvbS5lY251cC5hYVMxNmQzAAAAAgAA
|
||||
ACUAAAACAAAAAAAAAAEAACPxAAABoTX9/AAAAAAACAAAAAEAAAAAAAAAJQAAAAIAA
|
||||
AAAAAAAAgAAI/EAAAGhNf38AAAAAAAIAAAAAQAAAAA=
|
||||
-----END ALI VIDEO CERT-----
|
||||
BIN
backend/automation/themes/dxsx/splash.imageset/1242_2688.png
vendored
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
backend/automation/themes/dxsx/splash.imageset/2048_2732.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
backend/automation/themes/dxsx/splash.imageset/828_1792.png
vendored
Normal file
|
After Width: | Height: | Size: 168 KiB |
23
backend/automation/themes/dxsx/splash.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "828_1792.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "1242_2688.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"filename" : "2048_2732.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
120
backend/automation/themes/hsd/AppIcon.appiconset/Contents.json
Normal file
@ -0,0 +1,120 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-20@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "20x20",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-20@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-29@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "29x29",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-29@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "38x38",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-38@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "38x38",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-38@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-40@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "40x40",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-40@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "60x60",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-60@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "60x60",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-60@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "64x64",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-64@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "64x64",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-64@3x.png",
|
||||
"scale": "3x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "68x68",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-68@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "76x76",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-76@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "83.5x83.5",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-83.5@2x.png",
|
||||
"scale": "2x",
|
||||
"platform": "ios"
|
||||
},
|
||||
{
|
||||
"size": "1024x1024",
|
||||
"idiom": "universal",
|
||||
"filename": "icon-1024.png",
|
||||
"scale": "1x",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "icon.wuruihong.com"
|
||||
}
|
||||
}
|
||||
BIN
backend/automation/themes/hsd/AppIcon.appiconset/icon-1024.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
backend/automation/themes/hsd/AppIcon.appiconset/icon-20@2x.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |