[feat] add HarmonyOS desktop and mobile clients,issue #117

This commit is contained in:
kunkundi
2026-10-16 00:42:55 +08:00
parent 43a0789c1e
commit 45bf6ec4ee
149 changed files with 11587 additions and 93 deletions
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""Build the Slint PC client (default), or the ArkUI phone/tablet client (--mobile)."""
import argparse
from contextlib import contextmanager
import fcntl
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
APP = Path(__file__).resolve().parents[1]
ROOT = APP.parents[1]
CACHE = APP / '.native'
PACKAGE_REVISION = '2eedd731d1e7e377bc0eea970b9f0c120ca5f00d'
def run(args, *, cwd=APP, env=None):
subprocess.run([str(arg) for arg in args], cwd=cwd, env=env, check=True)
def environment():
studio = Path(os.environ.get('DEVECO_STUDIO_HOME', '/Applications/DevEco-Studio.app/Contents'))
sdk = Path(os.environ.get('DEVECO_SDK_HOME', studio / 'sdk'))
native = sdk / 'default/openharmony/native'
if not (native / 'llvm/bin/clang++').is_file():
raise SystemExit('Set DEVECO_SDK_HOME to the DevEco SDK directory containing default/openharmony/native.')
xmake = os.environ.get('XMAKE_BIN') or shutil.which('xmake')
if not xmake:
raise SystemExit('Install Xmake >= 3.1.1 or set XMAKE_BIN.')
output = subprocess.check_output([xmake, '--version'], text=True)
version = re.search(r'xmake v(\d+)\.(\d+)\.(\d+)', output)
if not version or tuple(map(int, version.groups())) < (3, 1, 1):
raise SystemExit('Xmake >= 3.1.1 is required.')
env = os.environ.copy()
env.update(NODE_HOME=str(studio / 'tools/node'), DEVECO_SDK_HOME=str(sdk),
JAVA_HOME=str(studio / 'jbr/Contents/Home'),
XMAKE_PKG_INSTALLDIR=str(CACHE / 'packages'),
XMAKE_GLOBALDIR=str(CACHE / 'global'), NO_COLOR='1')
env['PATH'] = os.pathsep.join([str(CACHE / 'venv/bin'), str(studio / 'tools/node/bin'),
str(studio / 'tools/ohpm/bin'),
str(native / 'build-tools/cmake/bin'), env.get('PATH', '')])
return studio, native, xmake, env
def bootstrap(env):
if sys.version_info < (3, 10):
raise SystemExit('Use Python >= 3.10 (for example /opt/homebrew/bin/python3).')
venv = CACHE / 'venv'
if not (venv / 'bin/python').exists():
run([sys.executable, '-m', 'venv', venv])
if not (venv / '.crossdesk-tools-v1').exists():
run([venv / 'bin/python', '-m', 'pip', 'install', 'setuptools==80.9.0',
'wheel==0.45.1', 'meson==1.12.1'], env=env)
(venv / '.crossdesk-tools-v1').touch()
repository = CACHE / 'global/.xmake/repositories/xmake-repo'
if not (repository / '.git').exists():
repository.parent.mkdir(parents=True, exist_ok=True)
seed = Path.home() / '.xmake/repositories/xmake-repo'
source = seed if (seed / '.git').exists() else 'https://github.com/xmake-io/xmake-repo.git'
run(['git', 'clone', '--no-hardlinks', source, repository], env=env)
run(['git', 'remote', 'set-url', 'origin', 'https://github.com/xmake-io/xmake-repo.git'], cwd=repository, env=env)
revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=repository, text=True).strip()
if revision != PACKAGE_REVISION:
run(['git', 'fetch', 'origin', PACKAGE_REVISION], cwd=repository, env=env)
run(['git', 'checkout', '--detach', PACKAGE_REVISION], cwd=repository, env=env)
def mobile_project():
"""Stage only the mobile source tree; never carry stale PC files forward."""
project = CACHE / 'mobile-project'
project.mkdir(exist_ok=True)
for name in ['entry/src', 'entry/libs']:
shutil.rmtree(project / name, ignore_errors=True)
for name in ['AppScope', 'hvigor', 'entry/src/main/cpp', 'entry/src/main/ets/mobile']:
shutil.copytree(APP / name, project / name, dirs_exist_ok=True)
for name in ['element/color.json', 'media/mobile_brand.png']:
target = project / 'entry/src/main/resources/base' / name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(APP / 'entry/src/main/resources/base' / name, target)
pages = project / 'entry/src/main/ets/pages'
pages.mkdir(parents=True)
shutil.copy2(APP / 'entry/src/main/ets/pages/Mobile.ets', pages / 'Mobile.ets')
shutil.copytree(CACHE / 'mobile-libs', project / 'entry/libs')
(project / 'native').mkdir(exist_ok=True)
(project / 'native/session.h').unlink(missing_ok=True)
shutil.copy2(APP / 'native/mobile_session.h', project / 'native/mobile_session.h')
for name in ['hvigorfile.ts', 'oh-package.json5', 'entry/hvigorfile.ts', 'entry/build-profile.json5', 'entry/oh-package.json5']:
shutil.copy2(APP / name, project / name)
profile = json.loads((APP / 'build-profile.json5').read_text())
profile['app']['signingConfigs'] = []
for product in profile['app']['products']:
product.pop('signingConfig', None)
(project / 'build-profile.json5').write_text(json.dumps(profile, indent=2)+'\n')
for name in ['entry/src/main/module.json5', 'entry/src/main/resources/base/profile/main_pages.json']:
target = project / name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(APP / name, target)
return project
def build_native(native, xmake, env, *, mobile=False):
if not (ROOT / 'deps/submodules/minirtc/src/api/minirtc.h').exists():
raise SystemExit('Run git submodule update --init --recursive first.')
bootstrap(env)
run([xmake, 'f', '-P', APP / 'native', '-y', '-p', 'harmony', '-a', 'arm64-v8a',
'-m', 'release', '--toolchain=hdk', '--sdk=' + str(native),
'--policies=package.precompiled:n', '-o', CACHE / 'build'], cwd=APP / 'native', env=env)
# The PC client links MiniRTC directly; it has no separate session core.
run([xmake, '-P', APP / 'native', '-y', 'crossdesk_harmony_mobile' if mobile else 'minirtc'],
cwd=APP / 'native', env=env)
@contextmanager
def build_lock():
CACHE.mkdir(parents=True, exist_ok=True)
with (CACHE / 'build.lock').open('w') as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
yield
def main():
parser = argparse.ArgumentParser(description=__doc__,
epilog='PC options (including permissions/install): python3 apps/harmony/scripts/build_desktop.py --help')
group = parser.add_mutually_exclusive_group()
group.add_argument('--native-only', action='store_true')
group.add_argument('--hap-only', action='store_true', help='Use an already built native library')
parser.add_argument('--release', action='store_true')
parser.add_argument('--mobile', action='store_true', help='Build a controller-only phone/tablet HAP without PC permissions or signing material')
args, desktop_args = parser.parse_known_args()
if not args.mobile and not args.native_only:
run([sys.executable, APP / 'scripts/build_desktop.py', *sys.argv[1:]], cwd=ROOT)
return
if desktop_args:
parser.error('unrecognized arguments: ' + ' '.join(desktop_args))
with build_lock():
studio, native, xmake, env = environment()
if not args.hap_only:
build_native(native, xmake, env, mobile=args.mobile)
if not args.native_only:
if not (CACHE / 'mobile-libs/arm64-v8a/libcrossdesk_harmony_core.so').is_file():
raise SystemExit('Build the mobile core first: python3 apps/harmony/scripts/build.py --mobile --native-only')
project = mobile_project()
run([sys.executable, APP / 'scripts/generate_mobile_resources.py', '--output',
project / 'entry/src/main/resources/rawfile'], env=env)
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)
for hap in sorted((project / 'entry/build/default/outputs').rglob('*.hap')):
run([sys.executable, APP / 'tests/verify_hap.py', hap, '--mobile'], env=env)
if __name__ == '__main__':
main()
+272
View File
@@ -0,0 +1,272 @@
#!/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()
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Package source-backed, offline mobile legal and build documents."""
import hashlib
import argparse
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import shutil
APP = Path(__file__).resolve().parents[1]
ROOT = APP.parents[1]
OUTPUT = APP / 'entry/src/main/resources/rawfile'
def git(*args, cwd=ROOT):
return subprocess.check_output(['git', *args], cwd=cwd, text=True).strip()
def main(output=OUTPUT):
spec = importlib.util.spec_from_file_location('android_notices', ROOT / 'apps/android/scripts/generate_notices.py')
android = importlib.util.module_from_spec(spec)
spec.loader.exec_module(android)
catalog = json.loads((ROOT / 'apps/ios/CrossDeskMobile/Resources/ThirdPartyLicenses.json').read_text())
components = [dict(c) for c in catalog['components'] if c['id'] in android.COMPONENTS | {'crossdesk'}]
additions = json.loads((ROOT / 'apps/android/licenses/additional-notices.json').read_text())
components += [c for c in additions if c['id'] != 'android-ndk']
native = Path(os.environ.get('DEVECO_SDK_HOME', '/Applications/DevEco-Studio.app/Contents/sdk')) / 'default/openharmony/native'
components.append({'id': 'ohos-llvm', 'name': 'HarmonyOS LLVM runtime', 'license': 'Apache-2.0 WITH LLVM-exception',
'version': 'SDK bundled', 'sourceURL': 'https://gitee.com/openharmony/third_party_llvm-project',
'documents': [{'title': 'LLVM NOTICE', 'text': (native / 'llvm/NOTICE').read_text()}]})
revision = git('rev-parse', 'HEAD')
mini = git('rev-parse', 'HEAD', cwd=ROOT / 'deps/submodules/minirtc')
source = f'https://github.com/kunkundi/crossdesk/tree/{revision}'
notice = ('CrossDesk HarmonyOS is GPL-3.0-only; MiniRTC is LGPL-3.0-only. Other components retain their licenses.\n'
'You may modify, rebuild and relink these libraries, then install a HAP signed with your own HarmonyOS key.\n'
'Open-source licenses govern use, modification and redistribution; their warranty limitations apply.\n')
output.mkdir(parents=True, exist_ok=True)
# The catalog points to individual, hash-checked documents. A second full
# concatenation was unused and duplicated every bundled license.
(output / 'THIRD_PARTY_NOTICES.txt').unlink(missing_ok=True)
shutil.rmtree(output / 'about', ignore_errors=True)
for filename in ['LICENSE', 'PRIVACY.md']:
(output / filename).write_text((ROOT / filename).read_text())
(output / 'OPEN_SOURCE_NOTICE.txt').write_text(notice)
for component in components:
if component['id'] == 'crossdesk':
component['sourceURL'] = source
component['documents'] = [{'title': 'LICENSE', 'text': (ROOT / 'LICENSE').read_text()}, {'title': 'Open-source rights', 'text': notice}]
if component['id'] in {'minirtc', 'inih', 'webrtc'}:
component.update(version=mini, sourceURL=f'https://github.com/kunkundi/minirtc/tree/{mini}')
docs = []
for index, doc in enumerate(component['documents']):
asset = f"about/licenses/{component['id']}/{index}.txt"
target = output / asset
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(doc['text'])
docs.append({'title': doc['title'], 'asset': asset, 'sha256': hashlib.sha256(doc['text'].encode()).hexdigest()})
component['documents'] = docs
(output / 'ThirdPartyLicenses.json').write_text(json.dumps({'schemaVersion': 1, 'components': components}, ensure_ascii=False, indent=2)+'\n')
documents = []
for i, (title, path) in enumerate([
('鸿蒙构建说明', APP / 'README.md'), ('移动端功能对照', APP / 'MOBILE_PARITY.md'),
('原生构建配方', APP / 'native/xmake.lua'), ('构建脚本', APP / 'scripts/build.py'),
('MiniRTC 构建配方', ROOT / 'deps/submodules/minirtc/xmake.lua'),
('OpenSSL 构建配方', ROOT / 'deps/submodules/minirtc/thirdparty/openssl/xmake.lua')]):
if not path.exists(): continue
asset = f'about/build/{i}.txt'; target = output / asset; target.parent.mkdir(parents=True, exist_ok=True); target.write_text(path.read_text())
documents.append({'title': title, 'asset': asset})
(output / 'SourceMetadata.json').write_text(json.dumps({'revision': revision, 'miniRTCRevision': mini, 'sourceURL': source,
'isModified': bool(git('status', '--porcelain', '--untracked-files=normal')), 'buildDocuments': documents}, ensure_ascii=False, indent=2)+'\n')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, default=OUTPUT)
main(parser.parse_args().output)