mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-17 17:33:21 +08:00
[feat] add HarmonyOS desktop and mobile clients,issue #117
This commit is contained in:
@@ -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()
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user