1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright (c) 2023 Huawei Device Co., Ltd.
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import argparse
17import os
18import sys
19import subprocess
20import shutil
21import json5
22
23from util import build_utils
24from util import file_utils
25
26
27def parse_args(args):
28    parser = argparse.ArgumentParser()
29    build_utils.add_depfile_option(parser)
30
31    parser.add_argument('--nodejs', help='nodejs path')
32    parser.add_argument('--cwd', help='app project directory')
33    parser.add_argument('--sdk-home', help='sdk home')
34    parser.add_argument('--hvigor-home', help='hvigor home')
35    parser.add_argument('--enable-debug', action='store_true', help='if enable debuggable')
36    parser.add_argument('--build-level', default='project', help='module or project')
37    parser.add_argument('--assemble-type', default='assembleApp', help='assemble type')
38    parser.add_argument('--output-file', help='output file')
39    parser.add_argument('--build-profile', help='build profile file')
40    parser.add_argument('--system-lib-module-info-list', nargs='+', help='system lib module info list')
41    parser.add_argument('--ohos-app-abi', help='ohos app abi')
42    parser.add_argument('--ohpm-registry', help='ohpm registry', nargs='?')
43    parser.add_argument('--hap-out-dir', help='hap out dir')
44    parser.add_argument('--hap-name', help='hap name')
45    parser.add_argument('--test-hap', help='build ohosTest if enable', action='store_true')
46    parser.add_argument('--test-module', help='specify the module within ohosTest', default='entry')
47    parser.add_argument('--module-libs-dir', help='', default='entry')
48    parser.add_argument('--sdk-type-name', help='sdk type name', nargs='+', default=['sdk.dir'])
49    parser.add_argument('--build-modules', help='build modules', nargs='+', default=[])
50    parser.add_argument('--use-hvigor-cache', help='use hvigor cache', action='store_true')
51    parser.add_argument('--hvigor-obfuscation', help='hvigor obfuscation', action='store_true')
52
53    options = parser.parse_args(args)
54    return options
55
56
57def make_env(build_profile: str, cwd: str, ohpm_registry: str, options):
58    '''
59    Set up the application compilation environment and run "ohpm install"
60    :param build_profile: module compilation information file
61    :param cwd: app project directory
62    :param ohpm_registry: ohpm registry
63    :return: None
64    '''
65    print(f"build_profile:{build_profile}; cwd:{cwd}")
66    cur_dir = os.getcwd()
67    with open(build_profile, 'r') as input_f:
68        build_info = json5.load(input_f)
69        modules_list = build_info.get('modules')
70        print(f"modules_list:{modules_list}")
71        ohpm_install_cmd = ['ohpm', 'install']
72        if ohpm_registry:
73            ohpm_install_cmd.append('--registry=' + ohpm_registry)
74        env = {
75            'PATH': f"{os.path.dirname(os.path.abspath(options.nodejs))}:{os.environ.get('PATH')}",
76            'NODE_HOME': os.path.dirname(os.path.abspath(options.nodejs)),
77        }
78        os.chdir(cwd)
79        subprocess.run(['chmod', '+x', 'hvigorw'])
80        if os.path.exists(os.path.join(cwd, '.arkui-x/android/gradlew')):
81            subprocess.run(['chmod', '+x', '.arkui-x/android/gradlew'])
82        print(f"[0/0] ohpm_install_cmd:{ohpm_install_cmd}")
83        proc = subprocess.Popen(ohpm_install_cmd,
84                                stdout=subprocess.PIPE,
85                                stderr=subprocess.PIPE,
86                                env=env,
87                                encoding='utf-8')
88        stdout, stderr = proc.communicate()
89        print(f"[0/0] {stdout}")
90        print(f"[0/0] {stderr}")
91        if proc.returncode:
92            raise Exception('ReturnCode:{}. ohpm install failed. {}'.format(
93                proc.returncode, stderr))
94    os.chdir(cur_dir)
95
96
97def gen_unsigned_hap_path_json(build_profile: str, cwd: str, options):
98    '''
99    Generate unsigned_hap_path_list
100    :param build_profile: module compilation information file
101    :param cwd: app project directory
102    :return: None
103    '''
104    unsigned_hap_path_json = {}
105    unsigned_hap_path_list = []
106    with open(build_profile, 'r') as input_f:
107        build_info = json5.load(input_f)
108        modules_list = build_info.get('modules')
109        for module in modules_list:
110            src_path = module.get('srcPath')
111            if options.test_hap:
112                unsigned_hap_path = os.path.join(
113                    cwd, src_path, 'build/default/outputs/ohosTest')
114            else:
115                unsigned_hap_path = os.path.join(
116                    cwd, src_path, 'build/default/outputs/default')
117            hap_file = build_utils.find_in_directory(
118                unsigned_hap_path, '*-unsigned.hap')
119            hsp_file = build_utils.find_in_directory(
120                unsigned_hap_path, '*-unsigned.hsp')
121            unsigned_hap_path_list.extend(hap_file)
122            unsigned_hap_path_list.extend(hsp_file)
123        unsigned_hap_path_json['unsigned_hap_path_list'] = unsigned_hap_path_list
124    file_utils.write_json_file(options.output_file, unsigned_hap_path_json)
125
126
127def copy_libs(cwd: str, system_lib_module_info_list: list, ohos_app_abi: str, module_libs_dir: str):
128    '''
129    Obtain the output location of system library .so by reading the module compilation information file,
130    and copy it to the app project directory
131    :param cwd: app project directory
132    :param system_lib_module_info_list: system library module compilation information file
133    :param ohos_app_abi: app abi
134    :return: None
135    '''
136    for _lib_info in system_lib_module_info_list:
137        lib_info = file_utils.read_json_file(_lib_info)
138        lib_path = lib_info.get('source')
139        if os.path.exists(lib_path):
140            lib_name = os.path.basename(lib_path)
141            dest = os.path.join(cwd, f'{module_libs_dir}/libs', ohos_app_abi, lib_name)
142            if not os.path.exists(os.path.dirname(dest)):
143                os.makedirs(os.path.dirname(dest), exist_ok=True)
144            shutil.copyfile(lib_path, dest)
145
146
147def hvigor_write_log(cmd, cwd, env):
148    proc = subprocess.Popen(cmd,
149                            cwd=cwd,
150                            env=env,
151                            stdout=subprocess.PIPE,
152                            stderr=subprocess.PIPE,
153                            encoding='utf-8')
154    stdout, stderr = proc.communicate()
155    for line in stdout.splitlines():
156        print(f"[1/1] Hvigor info: {line}")
157    for line in stderr.splitlines():
158        print(f"[2/2] Hvigor warning: {line}")
159    os.makedirs(os.path.join(cwd, 'build'), exist_ok=True)
160    with open(os.path.join(cwd, 'build', 'build.log'), 'w') as f:
161        f.write(f'{stdout}\n')
162        f.write(f'{stderr}\n')
163    if proc.returncode or "ERROR: BUILD FAILED" in stderr or "ERROR: BUILD FAILED" in stdout:
164        raise Exception('ReturnCode:{}. Hvigor build failed: {}'.format(proc.returncode, stderr))
165    print("[0/0] Hvigor build end")
166
167
168def get_integrated_project_config(cwd: str):
169    print(f"[0/0] project dir: {cwd}")
170    with open(os.path.join(cwd, 'hvigor/hvigor-config.json5'), 'r') as input_f:
171        hvigor_info = json5.load(input_f)
172        model_version = hvigor_info.get('modelVersion')
173    return model_version
174
175
176def build_hvigor_cmd(cwd: str, model_version: str, options):
177    cmd = ['bash']
178    if model_version:
179        if options.hvigor_home:
180            cmd.extend([f'{os.path.abspath(options.hvigor_home)}/hvigorw'])
181        else:
182            cmd.extend(['hvigorw'])
183    else:
184        cmd.extend(['./hvigorw'])
185
186    if options.test_hap:
187        cmd.extend(['--mode', 'module', '-p',
188               f'module={options.test_module}@ohosTest', 'assembleHap'])
189    elif options.build_modules:
190        cmd.extend(['assembleHap', '--mode',
191               'module', '-p', 'product=default', '-p', 'module=' + ','.join(options.build_modules)])
192    else:
193        cmd.extend(['--mode',
194               options.build_level, '-p', 'product=default', options.assemble_type])
195
196    if options.enable_debug:
197        cmd.extend(['-p', 'debuggable=true'])
198    else:
199        cmd.extend(['-p', 'debuggable=false'])
200
201    if options.use_hvigor_cache and os.environ.get('CACHE_BASE'):
202        hvigor_cache_dir = os.path.join(os.environ.get('CACHE_BASE'), 'hvigor_cache')
203        os.makedirs(hvigor_cache_dir, exist_ok=True)
204        cmd.extend(['-p', f'build-cache-dir={hvigor_cache_dir}'])
205
206    if options.hvigor_obfuscation:
207        cmd.extend(['-p', 'buildMode=release'])
208    else:
209        cmd.extend(['-p', 'hvigor-obfuscation=false'])
210
211    cmd.extend(['--no-daemon'])
212
213    print("[0/0] hvigor cmd: " + ' '.join(cmd))
214    return cmd
215
216
217def set_sdk_path(cwd: str, model_version: str, options, env):
218    if 'sdk.dir' not in options.sdk_type_name and model_version:
219        write_env_sdk(options, env)
220    else:
221        write_local_properties(cwd, options)
222
223
224def write_local_properties(cwd: str, options):
225    sdk_dir = options.sdk_home
226    nodejs_dir = os.path.abspath(
227        os.path.dirname(os.path.dirname(options.nodejs)))
228    with open(os.path.join(cwd, 'local.properties'), 'w') as f:
229        for sdk_type in options.sdk_type_name:
230            f.write(f'{sdk_type}={sdk_dir}\n')
231        f.write(f'nodejs.dir={nodejs_dir}\n')
232
233
234def write_env_sdk(options, env):
235    sdk_dir = options.sdk_home
236    env['DEVECO_SDK_HOME'] = sdk_dir
237
238
239def hvigor_sync(cwd: str, model_version: str, env):
240    if not model_version:
241        subprocess.run(['bash', './hvigorw', '--sync', '--no-daemon'],
242                   cwd=cwd,
243                   env=env,
244                   stdout=subprocess.DEVNULL,
245                   stderr=subprocess.DEVNULL)
246
247
248def hvigor_build(cwd: str, options):
249    '''
250    Run hvigorw to build the app or hap
251    :param cwd: app project directory
252    :param options: command line parameters
253    :return: None
254    '''
255    model_version = get_integrated_project_config(cwd)
256    print(f"[0/0] model_version: {model_version}")
257
258    cmd = build_hvigor_cmd(cwd, model_version, options)
259
260    print("[0/0] Hvigor clean start")
261    env = os.environ.copy()
262    env['CI'] = 'true'
263
264    set_sdk_path(cwd, model_version, options, env)
265
266    hvigor_sync(cwd, model_version, env)
267
268    print("[0/0] Hvigor build start")
269    hvigor_write_log(cmd, cwd, env)
270
271
272def main(args):
273    options = parse_args(args)
274    cwd = os.path.abspath(options.cwd)
275
276    # copy system lib deps to app libs dir
277    if options.system_lib_module_info_list:
278        copy_libs(cwd, options.system_lib_module_info_list,
279                  options.ohos_app_abi, options.module_libs_dir)
280
281    os.environ['PATH'] = '{}:{}'.format(os.path.dirname(
282        os.path.abspath(options.nodejs)), os.environ.get('PATH'))
283
284    # add arkui-x to PATH
285    os.environ['PATH'] = f'{cwd}/.arkui-x/android:{os.environ.get("PATH")}'
286
287    # generate unsigned_hap_path_list and run ohpm install
288    make_env(options.build_profile, cwd, options.ohpm_registry, options)
289
290    # invoke hvigor to build hap or app
291    hvigor_build(cwd, options)
292
293    # generate a json file to record the path of all unsigned haps, and When signing hap later,
294    # this json file will serve as input to provide path information for each unsigned hap.
295    gen_unsigned_hap_path_json(options.build_profile, cwd, options)
296
297if __name__ == '__main__':
298    sys.exit(main(sys.argv[1:]))