mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-17 01:13:19 +08:00
273 lines
16 KiB
Python
273 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""Build a separate HarmonyOS HAP using the unchanged desktop Slint UI."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import urllib.request
|
|
|
|
import build as native_build
|
|
|
|
APP = Path(__file__).resolve().parents[1]
|
|
ROOT = APP.parents[1]
|
|
PORT = APP / 'desktop-port'
|
|
CACHE = APP / '.native/slint-poc'
|
|
SOURCE = CACHE / 'slint-1.17.1'
|
|
PROJECT = CACHE / 'desktop-project'
|
|
TARGET = 'aarch64-unknown-linux-ohos'
|
|
SHA256 = '68222567f8c70ff677cd4a98cd94fb4765ac0f797eb8f8608a646911c908dc2a'
|
|
|
|
|
|
def run(args, *, cwd=ROOT, env=None):
|
|
subprocess.run([str(arg) for arg in args], cwd=cwd, env=env, check=True)
|
|
|
|
|
|
def write(path, text):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
if not path.exists() or path.read_text() != text:
|
|
path.write_text(text)
|
|
|
|
|
|
def copy(source, dest):
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
if not dest.exists() or source.read_bytes() != dest.read_bytes():
|
|
shutil.copy2(source, dest)
|
|
|
|
|
|
def write_json(path, value):
|
|
write(path, json.dumps(value, ensure_ascii=False, indent=2) + '\n')
|
|
|
|
|
|
def main():
|
|
bundle = json.loads((APP / 'AppScope/app.json5').read_text())['app']['bundleName']
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--reuse-runtime', action='store_true', help='Skip an already completed Rust runtime build')
|
|
parser.add_argument('--install', action='store_true', help='Install/start the desktop app')
|
|
parser.add_argument('--hap-only', action='store_true', help='Reuse MiniRTC dependencies; rebuild the desktop HAP')
|
|
parser.add_argument('--release', action='store_true', help='Build a release HAP')
|
|
parser.add_argument('--version', default=os.environ.get('CROSSDESK_VERSION', '0.0.0'))
|
|
parser.add_argument('--device', default='127.0.0.1:5555')
|
|
parser.add_argument('--slint-compiler', default=os.environ.get('SLINT_COMPILER'))
|
|
parser.add_argument('--desktop-permissions', action='store_true',
|
|
help='Declare HOOK_KEY_EVENT/READ_PASTEBOARD (requires approved signing-profile ACLs)')
|
|
parser.add_argument('--host-permissions', action='store_true',
|
|
help='Declare CUSTOM_SCREEN_RECORDING/CONTROL_DEVICE for persistent host authorization (requires approved signing-profile ACLs)')
|
|
parser.add_argument('--cursor-position-permission', action='store_true',
|
|
help='Declare INPUT_MONITORING for host cursor position during recording (requires an approved signing-profile ACL)')
|
|
parser.add_argument('--signing-config', type=Path,
|
|
help='Private JSON file containing one DevEco signingConfigs entry; use absolute material paths')
|
|
parser.add_argument('--bundle-name', choices=[bundle],
|
|
help='Compatibility option; PC and mobile use the shared AppScope bundle name (requires --signing-config)')
|
|
args = parser.parse_args()
|
|
if sys.version_info < (3, 12):
|
|
parser.error('Use Python >= 3.12 (for example /opt/homebrew/bin/python3).')
|
|
if args.bundle_name and not args.signing_config:
|
|
parser.error('--bundle-name requires --signing-config')
|
|
signing = None
|
|
if args.signing_config:
|
|
signing = json.loads(args.signing_config.read_text())
|
|
if not isinstance(signing, dict) or not signing.get('name') or not isinstance(signing.get('material'), dict):
|
|
parser.error('--signing-config must contain one named DevEco signingConfigs entry with material')
|
|
for name in ['certpath', 'storeFile', 'profile']:
|
|
path = Path(signing['material'].get(name, ''))
|
|
if not path.is_absolute() or not path.is_file():
|
|
parser.error(f'signing material {name} must be an existing absolute path')
|
|
if not args.version or not all(char.isascii() and (char.isalnum() or char in '.-+') for char in args.version):
|
|
parser.error('Invalid application version')
|
|
studio, native, xmake, env = native_build.environment()
|
|
sdk = Path(env['DEVECO_SDK_HOME'])
|
|
env['OHOS_NATIVE'] = str(native)
|
|
env['PATH'] = str(Path.home() / '.cargo/bin') + os.pathsep + env['PATH']
|
|
if not args.hap_only:
|
|
native_build.build_native(native, xmake, env)
|
|
compiler = Path(args.slint_compiler) if args.slint_compiler else next(
|
|
(Path.home() / '.xmake/packages/s/slint/1.17.1').glob('*/bin/slint-compiler'), None)
|
|
if not compiler or not compiler.is_file():
|
|
raise SystemExit('Set SLINT_COMPILER to a host slint-compiler 1.17.1 executable.')
|
|
version = subprocess.check_output([compiler, '--version'], text=True).strip()
|
|
if version != 'slint-compiler 1.17.1':
|
|
raise SystemExit(f'Expected slint-compiler 1.17.1, got {version}')
|
|
CACHE.mkdir(parents=True, exist_ok=True)
|
|
archive = CACHE / 'slint-1.17.1.tar.gz'
|
|
if not archive.exists():
|
|
urllib.request.urlretrieve('https://codeload.github.com/slint-ui/slint/tar.gz/refs/tags/v1.17.1', archive)
|
|
if hashlib.sha256(archive.read_bytes()).hexdigest() != SHA256:
|
|
raise SystemExit('Slint source checksum mismatch')
|
|
if not SOURCE.exists():
|
|
with tarfile.open(archive) as tar:
|
|
tar.extractall(CACHE, filter='data')
|
|
patches = [PORT / name for name in ['font-discovery.patch', 'os-services.patch', 'window-properties.patch']]
|
|
# A changed patch cannot reverse its older installed version. Restore only
|
|
# our patch targets from the verified archive before applying the full set.
|
|
targets = {line[6:] for patch in patches for line in patch.read_text().splitlines()
|
|
if line.startswith('+++ b/')}
|
|
with tarfile.open(archive) as tar:
|
|
for relative in targets:
|
|
if Path(relative).is_absolute() or '..' in Path(relative).parts:
|
|
raise SystemExit('Invalid runtime patch path')
|
|
with tar.extractfile(f'slint-1.17.1/{relative}') as original:
|
|
(SOURCE / relative).write_bytes(original.read())
|
|
for patch in patches:
|
|
run(['git', 'apply', patch], cwd=SOURCE)
|
|
linker = CACHE / 'linker.sh'
|
|
write(linker, '#!/bin/sh\nexec "${OHOS_NATIVE}/llvm/bin/clang" --target=aarch64-linux-ohos --sysroot="${OHOS_NATIVE}/sysroot" "$@"\n')
|
|
linker.chmod(0o755)
|
|
env.update(CARGO_TARGET_AARCH64_UNKNOWN_LINUX_OHOS_LINKER=str(linker),
|
|
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_OHOS_RUSTFLAGS='-C link-arg=-Wl,-soname,libslint_cpp.so',
|
|
SLINT_GENERATED_INCLUDE_DIR=str(CACHE / 'include'))
|
|
runtime_signature = hashlib.sha256(b''.join(path.read_bytes() for path in patches)).hexdigest()
|
|
runtime_stamp = CACHE / 'runtime.sha256'
|
|
if not args.reuse_runtime or not runtime_stamp.exists() or runtime_stamp.read_text() != runtime_signature:
|
|
run(['rustup', 'target', 'add', TARGET], env=env)
|
|
run(['cargo', 'build', '--manifest-path', SOURCE / 'Cargo.toml', '--locked', '-p', 'slint-cpp',
|
|
'--target', TARGET, '--release', '--no-default-features', '--features', 'std,renderer-software'], env=env)
|
|
write(runtime_stamp, runtime_signature)
|
|
|
|
# Reuse the actual entry point, including MainWindow/StreamWindow/ServerWindow
|
|
# and their imported components. No copied or rewritten Slint layout.
|
|
generated = CACHE / 'generated'
|
|
generated.mkdir(exist_ok=True)
|
|
cpp_files = [generated / f'crossdesk_ui_{index}.cpp' for index in range(4)]
|
|
command = [compiler, ROOT / 'apps/desktop/src/gui/ui/crossdesk_ui.slint', '-f', 'cpp',
|
|
'-o', generated / 'crossdesk_ui.h', '--style', 'fluent-light',
|
|
'--embed-resources=embed-files', '--cpp-namespace', 'crossdesk::ui']
|
|
for path in cpp_files:
|
|
command += ['--cpp-file', path]
|
|
digest = hashlib.sha256('\0'.join(map(str, command)).encode())
|
|
for path in sorted((ROOT / 'apps/desktop/src/gui').rglob('*')):
|
|
if '/ui/' not in str(path) and '/assets/' not in str(path):
|
|
continue
|
|
if path.is_file():
|
|
digest.update(str(path).encode())
|
|
digest.update(path.read_bytes())
|
|
stamp = generated / 'input.sha256'
|
|
signature = digest.hexdigest()
|
|
if not stamp.exists() or stamp.read_text() != signature or not all(path.exists() for path in [generated / 'crossdesk_ui.h', *cpp_files]):
|
|
run(command, env=env)
|
|
write(stamp, signature)
|
|
deps = CACHE / 'desktop-deps'
|
|
deps.mkdir(exist_ok=True)
|
|
header = deps / 'httplib.h'
|
|
if not header.exists():
|
|
urllib.request.urlretrieve('https://raw.githubusercontent.com/yhirose/cpp-httplib/v0.26.0/httplib.h', header)
|
|
if hashlib.sha256(header.read_bytes()).hexdigest() != '4a42da28ce477d06f53942c172197d3b83b4fe42b7e37b8af78c70e063c06da7':
|
|
raise SystemExit('cpp-httplib source checksum mismatch')
|
|
legacy_types = PROJECT / 'entry/src/main/cpp/types/libslint_probe'
|
|
if legacy_types.exists():
|
|
run([studio / 'tools/hvigor/bin/hvigorw', 'clean', '--no-daemon'], cwd=PROJECT, env=env)
|
|
shutil.rmtree(legacy_types)
|
|
(PROJECT / 'entry/libs/arm64-v8a/libslint_probe.so').unlink(missing_ok=True)
|
|
for name in ['build-profile.json5', 'hvigorfile.ts', 'hvigor/hvigor-config.json5', 'entry/hvigorfile.ts',
|
|
'entry/src/main/resources/base/profile/main_pages.json',
|
|
'entry/src/main/resources/base/element/color.json', 'AppScope/resources/base/media/app_icon.png',
|
|
'AppScope/app.json5', 'AppScope/resources/base/element/string.json']:
|
|
copy(APP / name, PROJECT / name)
|
|
profile = json.loads((APP / 'build-profile.json5').read_text())
|
|
# The PC project contains only its own Index page.
|
|
write_json(PROJECT / 'entry/src/main/resources/base/profile/main_pages.json', {'src': ['pages/Index']})
|
|
# The source project can have an IDE-managed signature for a different
|
|
# bundle. Only use signing material explicitly selected for this build.
|
|
profile['app']['signingConfigs'] = [signing] if signing else []
|
|
for product in profile['app']['products']:
|
|
product.pop('signingConfig', None)
|
|
product['compatibleSdkVersion'] = '26.0.0'
|
|
if signing:
|
|
product['signingConfig'] = signing['name']
|
|
write_json(PROJECT / 'build-profile.json5', profile)
|
|
(PROJECT / 'build-profile.json5').chmod(0o600)
|
|
write_json(PROJECT / 'oh-package.json5', {'modelVersion': '6.0.0', 'name': 'crossdesk-slint-desktop',
|
|
'version': '0.1.0', 'dependencies': {}})
|
|
write_json(PROJECT / 'entry/src/main/resources/base/element/string.json',
|
|
{'string': [{'name': 'keyboard_permission_reason', 'value': '控制远程桌面时将系统快捷键发送到远端'},
|
|
{'name': 'screen_recording_permission_reason', 'value': '用于向已通过连接密码验证的远程设备共享本机屏幕'},
|
|
{'name': 'control_device_permission_reason', 'value': '用于接收已通过连接密码验证的远程设备的键盘和鼠标操作'},
|
|
{'name': 'clipboard_permission_reason', 'value': '在远程会话中同步本机和远端的剪贴板文本'}]})
|
|
module = json.loads((APP / 'entry/src/main/module.json5').read_text())
|
|
module['module']['deviceTypes'] = ['2in1']
|
|
module['module']['requestPermissions'] = [
|
|
{'name': 'ohos.permission.INTERNET'},
|
|
{'name': 'ohos.permission.FILE_ACCESS_PERSIST'},
|
|
{'name': 'ohos.permission.PREPARE_APP_TERMINATE'}]
|
|
for ability in module['module']['abilities']:
|
|
ability['srcEntry'] = './ets/entryability/EntryAbility.ets'
|
|
ability.pop('backgroundModes', None)
|
|
if args.desktop_permissions:
|
|
module['module']['requestPermissions'] += [
|
|
{'name': 'ohos.permission.HOOK_KEY_EVENT', 'reason': '$string:keyboard_permission_reason',
|
|
'usedScene': {'abilities': ['EntryAbility'], 'when': 'inuse'}},
|
|
{'name': 'ohos.permission.READ_PASTEBOARD', 'reason': '$string:clipboard_permission_reason',
|
|
'usedScene': {'abilities': ['EntryAbility'], 'when': 'always'}}]
|
|
if args.host_permissions:
|
|
module['module']['requestPermissions'] += [
|
|
{'name': 'ohos.permission.CUSTOM_SCREEN_RECORDING', 'reason': '$string:screen_recording_permission_reason',
|
|
'usedScene': {'abilities': ['EntryAbility'], 'when': 'always'}},
|
|
{'name': 'ohos.permission.CONTROL_DEVICE', 'reason': '$string:control_device_permission_reason',
|
|
'usedScene': {'abilities': ['EntryAbility'], 'when': 'always'}}]
|
|
if args.cursor_position_permission:
|
|
module['module']['requestPermissions'].append({'name': 'ohos.permission.INPUT_MONITORING'})
|
|
write(PROJECT / 'entry/src/main/ets/pages/DesktopCapabilities.ets',
|
|
f'export const desktopPermissions: boolean = {str(args.desktop_permissions).lower()};\n'
|
|
f'export const hostPermissions: boolean = {str(args.host_permissions).lower()};\n')
|
|
write_json(PROJECT / 'entry/src/main/module.json5', module)
|
|
profile = json.loads((APP / 'entry/build-profile.json5').read_text())
|
|
profile['buildOption']['externalNativeOptions']['cppFlags'] = '-std=c++20'
|
|
write_json(PROJECT / 'entry/build-profile.json5', profile)
|
|
write_json(PROJECT / 'entry/oh-package.json5', {'name': 'entry', 'version': '0.1.0',
|
|
'dependencies': {'libslint_desktop.so': 'file:./src/main/cpp/types/libslint_desktop'}})
|
|
types = PROJECT / 'entry/src/main/cpp/types/libslint_desktop'
|
|
write_json(types / 'oh-package.json5', {'name': 'libslint_desktop.so', 'version': '0.1.0',
|
|
'types': './index.d.ts'})
|
|
copy(PORT / 'index.d.ts', types / 'index.d.ts')
|
|
copy(PORT / 'Index.ets', PROJECT / 'entry/src/main/ets/pages/Index.ets')
|
|
copy(PORT / 'EntryAbility.ets', PROJECT / 'entry/src/main/ets/entryability/EntryAbility.ets')
|
|
for name in ['DesktopHost.ets', 'DesktopAccess.ets', 'AutostartHost.ets', 'TextInputHost.ets', 'KeyboardHost.ets', 'TrayHost.ets', 'FileHost.ets', 'DisplayHost.ets']:
|
|
copy(PORT / name, PROJECT / 'entry/src/main/ets/pages' / name)
|
|
runtime = PROJECT / 'entry/libs/arm64-v8a/libslint_cpp.so'
|
|
copy(SOURCE / f'target/{TARGET}/release/libslint_cpp.so', runtime)
|
|
def quote(path):
|
|
return '"' + str(path).replace('\\', '/') + '"'
|
|
cmake = f'''cmake_minimum_required(VERSION 3.16)
|
|
project(crossdesk_slint_desktop LANGUAGES CXX)
|
|
set(CMAKE_CXX_STANDARD 20)
|
|
add_library(slint_cpp SHARED IMPORTED)
|
|
set_target_properties(slint_cpp PROPERTIES IMPORTED_LOCATION {quote(runtime)} IMPORTED_SONAME "libslint_cpp.so")
|
|
add_library(slint_desktop SHARED {quote(PORT / 'desktop.cpp')} {' '.join(map(quote, cpp_files))})
|
|
set_target_properties(slint_desktop PROPERTIES SKIP_BUILD_RPATH TRUE)
|
|
target_compile_options(slint_desktop PRIVATE -Os)
|
|
target_compile_definitions(slint_desktop PRIVATE _LIBCPP_ENABLE_EXPERIMENTAL)
|
|
target_include_directories(slint_desktop PRIVATE
|
|
{quote(SOURCE / 'api/cpp/include')} {quote(CACHE / 'include')} {quote(generated)}
|
|
{quote(ROOT / 'apps/desktop/src/gui/ui')}
|
|
{quote(ROOT / 'apps/desktop/src/gui/assets/localization')}
|
|
{quote(ROOT / 'apps/desktop/src/gui/assets/fonts')})
|
|
target_link_libraries(slint_desktop PRIVATE slint_cpp ace_napi.z hilog_ndk.z EGL GLESv3 native_window)
|
|
'''
|
|
cmake += f'set(CROSSDESK_ROOT {quote(ROOT)})\nset(SLINT_DESKTOP_DEPS {quote(deps)})\nset(CROSSDESK_VERSION "{args.version}")\ninclude({quote(PORT / "CMakeLists.txt")})\n'
|
|
write(PROJECT / 'entry/src/main/cpp/CMakeLists.txt', cmake)
|
|
run([studio / 'tools/ohpm/bin/ohpm', 'install'], cwd=PROJECT, env=env)
|
|
run([studio / 'tools/hvigor/bin/hvigorw', '--mode', 'module', '-p', 'product=default',
|
|
'-p', 'module=entry@default', '-p', 'buildMode=' + ('release' if args.release else 'debug'), 'assembleHap', '--no-daemon'], cwd=PROJECT, env=env)
|
|
hap = PROJECT / ('entry/build/default/outputs/default/entry-default-' +
|
|
('signed' if signing else 'unsigned') + '.hap')
|
|
verify = [sys.executable, APP / 'tests/verify_hap.py', hap, '--bundle-name', bundle]
|
|
for option in ['desktop_permissions', 'host_permissions', 'cursor_position_permission']:
|
|
if getattr(args, option):
|
|
verify.append('--' + option.replace('_', '-'))
|
|
run(verify, env=env)
|
|
print(f'Desktop HAP: {hap}')
|
|
if args.install:
|
|
hdc = sdk / 'default/openharmony/toolchains/hdc'
|
|
run([hdc, '-t', args.device, 'install', '-r', hap], env=env)
|
|
run([hdc, '-t', args.device, 'shell', 'aa', 'force-stop', bundle], env=env)
|
|
run([hdc, '-t', args.device, 'shell', 'aa', 'start', '-a', 'EntryAbility', '-b', bundle], env=env)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
with native_build.build_lock():
|
|
main()
|