Files
crossdesk/apps/harmony/scripts/build.py
T

158 lines
7.9 KiB
Python

#!/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()