1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# Copyright (c) 2021 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 optparse
17import os
18import sys
19import json
20
21from zipfile import ZipFile  # noqa: E402
22from util import build_utils  # noqa: E402
23
24
25def parse_args(args):
26    args = build_utils.expand_file_args(args)
27
28    parser = optparse.OptionParser()
29    build_utils.add_depfile_option(parser)
30    parser.add_option('--output', help='stamp file')
31    parser.add_option('--js-assets-dir', help='js assets directory')
32    parser.add_option('--ets-assets-dir', help='ets assets directory')
33    parser.add_option('--js-forms-dir', help='js forms directory')
34    parser.add_option('--testrunner-dir', help='testrunner directory')
35    parser.add_option('--nodejs-path', help='path to nodejs app')
36    parser.add_option('--webpack-js', help='path to js webpack.js')
37    parser.add_option('--webpack-ets', help='path to ets webpack.js')
38    parser.add_option('--webpack-config-js', help='path to webpack.config.js')
39    parser.add_option('--webpack-config-ets', help='path to webpack.rich.config.js')
40    parser.add_option('--hap-profile', help='path to hap profile')
41    parser.add_option('--build-mode', help='debug mode or release mode')
42    parser.add_option('--js-sources-file', help='path to js sources file')
43    parser.add_option('--js2abc',
44                      action='store_true',
45                      default=False,
46                      help='whether to transform js to ark bytecode')
47    parser.add_option('--ets2abc',
48                      action='store_true',
49                      default=False,
50                      help='whether to transform ets to ark bytecode')
51    parser.add_option('--ark-es2abc-dir', help='path to ark es2abc dir')
52    parser.add_option('--ace-loader-home', help='path to ace-loader dir.')
53    parser.add_option('--ets-loader-home', help='path to ets-loader dir.')
54    parser.add_option('--app-profile', default=False, help='path to app-profile.')
55    parser.add_option('--manifest-file-path', help='path to manifest.json dir.')
56
57    options, _ = parser.parse_args(args)
58    options.js_assets_dir = build_utils.parse_gn_list(options.js_assets_dir)
59    options.ets_assets_dir = build_utils.parse_gn_list(options.ets_assets_dir)
60    options.js_forms_dir = build_utils.parse_gn_list(options.js_forms_dir)
61    options.testrunner_dir = build_utils.parse_gn_list(options.testrunner_dir)
62    return options
63
64
65def make_my_env(options, js2abc: bool) -> dict:
66    out_dir = os.path.abspath(os.path.dirname(options.output))
67    gen_dir = os.path.join(out_dir, "gen")
68    assets_dir = os.path.join(out_dir, "assets")
69    if options.app_profile:
70        if js2abc:
71            assets_dir = os.path.join(assets_dir, "js")
72        else:
73            assets_dir = os.path.join(assets_dir, "ets")
74    my_env = {
75        "aceModuleBuild": assets_dir,
76        "buildMode": options.build_mode,
77        "PATH": os.environ.get('PATH'),
78        "appResource": os.path.join(gen_dir, "ResourceTable.txt")
79    }
80    if options.app_profile:
81        my_env["aceProfilePath"] = os.path.join(gen_dir, "resources/base/profile")
82        my_env["aceModuleJsonPath"] = os.path.abspath(options.hap_profile)
83    return my_env
84
85
86def make_manifest_data(config: dict, options, js2abc: bool, asset_index: int, assets_cnt: int, src_path: str) -> dict:
87    data = dict()
88    data['appID'] = config['app']['bundleName']
89    if options.app_profile:
90        data['versionName'] = config['app']['versionName']
91        data['versionCode'] = config['app']['versionCode']
92        data['pages'] = config['module']['pages']
93        data['deviceType'] = config['module']['deviceTypes']
94    else:
95        data['appName'] = config['module']['abilities'][asset_index].get('label')
96        data['versionName'] = config['app']['version']['name']
97        data['versionCode'] = config['app']['version']['code']
98        data['deviceType'] = config['module']['deviceType']
99        for js_module in config['module']['js']:
100            js_module_name = js_module.get('name').split('.')[-1]
101
102            # According to the page name and ability entry match the corresponding page for ability
103            # Compatibility with mismatches due to "MainAbility" and "default"
104            if js_module_name == src_path or (js_module_name == 'MainAbility' and src_path == 'default') \
105               or (js_module_name == 'default' and src_path == 'MainAbility'):
106                data['pages'] = js_module.get('pages')
107                data['window'] = js_module.get('window')
108                if js_module.get('type') == 'form' and options.js_forms_dir:
109                    data['type'] = 'form'
110            if not js2abc:
111                data['mode'] = js_module.get('mode')
112    return data
113
114
115def build_ace(cmd: str, options, js2abc: bool, loader_home: str, assets_dir: str, assets_name: str):
116    my_env = make_my_env(options, js2abc)
117    gen_dir = my_env.get("aceModuleBuild")
118    assets_cnt = len(assets_dir)
119    use_compile_cache = os.environ.get("USE_COMPILE_CACHE", "false").lower() == "true"
120    for asset_index in range(assets_cnt):
121        ability_dir = os.path.relpath(assets_dir[asset_index], loader_home)
122        my_env["aceModuleRoot"] = ability_dir
123        if options.js_sources_file:
124            with open(options.js_sources_file, 'wb') as js_sources_file:
125                sources = get_all_js_sources(ability_dir)
126                js_sources_file.write('\n'.join(sources).encode())
127        src_path = os.path.basename(assets_dir[asset_index])
128
129        # Create a fixed directory for manifest.json
130        if js2abc:
131            build_dir = os.path.abspath(os.path.join(options.manifest_file_path, 'js', src_path))
132            my_env.update({"cachePath": os.path.join(build_dir, ".cache")})
133        else:
134            build_dir = os.path.abspath(os.path.join(options.manifest_file_path, 'ets', src_path))
135        if not os.path.exists(build_dir):
136            os.makedirs(build_dir, exist_ok=True)
137        manifest = os.path.join(build_dir, 'manifest.json')
138
139        # Determine abilityType according to config.json
140        if assets_name == 'testrunner_dir':
141            my_env["abilityType"] = 'testrunner'
142        elif assets_name != 'js_forms_dir' and not options.app_profile and assets_cnt > 1:
143            with open(options.hap_profile) as profile:
144                config = json.load(profile)
145                if config['module']['abilities'][asset_index].__contains__('forms'):
146                    my_env["abilityType"] = 'form'
147                else:
148                    my_env["abilityType"] = config['module']['abilities'][asset_index]['type']
149        else:
150            my_env["abilityType"] = 'page'
151
152        # Generate manifest.json only when abilityType is page
153        data = dict()
154        if my_env["abilityType"] == 'page':
155            with open(options.hap_profile) as profile:
156                config = json.load(profile)
157                data = make_manifest_data(config, options, js2abc, asset_index, assets_cnt, src_path)
158                build_utils.write_json(data, manifest)
159
160            # If missing page, skip it
161            if not data.__contains__('pages'):
162                print('Warning: There is no page matching this {}'.format(src_path))
163                continue
164
165        if not options.app_profile:
166            my_env["aceManifestPath"] = manifest
167            my_env["aceModuleBuild"] = os.path.join(gen_dir, src_path)
168
169        enable_compile_cache(asset_index, assets_cnt, my_env, use_compile_cache)
170        build_utils.check_output(cmd, cwd=loader_home, env=my_env)
171
172    if options.app_profile:
173        gen_dir = os.path.dirname(gen_dir)
174        build_utils.zip_dir(options.output, gen_dir)
175    else:
176        build_utils.zip_dir(options.output, gen_dir, zip_prefix_path='assets/js/')
177
178
179def enable_compile_cache(asset_index: int, assets_cnt: int, env: dict, use_compile_cache: bool):
180    if use_compile_cache:
181        env["useCompileCache"] = "true"
182        if assets_cnt > 1 and asset_index == assets_cnt - 1:
183            env["addTestRunner"] = "true"
184
185
186def get_all_js_sources(base) -> list:
187    sources = []
188    for root, _, files in os.walk(base):
189        for file in files:
190            if file[-3:] in ('.js', '.ts'):
191                sources.append(os.path.join(root, file))
192
193    return sources
194
195
196def main(args):
197    options = parse_args(args)
198
199    inputs = [
200        options.nodejs_path, options.webpack_js, options.webpack_ets,
201        options.webpack_config_js, options.webpack_config_ets
202    ]
203    depfiles = []
204    if not options.js_assets_dir and not options.ets_assets_dir:
205        with ZipFile(options.output, 'w') as file:
206            return
207
208    if options.ark_es2abc_dir:
209        depfiles.extend(build_utils.get_all_files(options.ark_es2abc_dir))
210
211    depfiles.append(options.webpack_js)
212    depfiles.append(options.webpack_ets)
213    depfiles.append(options.webpack_config_js)
214    depfiles.append(options.webpack_config_ets)
215    depfiles.extend(build_utils.get_all_files(options.ace_loader_home))
216    depfiles.extend(build_utils.get_all_files(options.ets_loader_home))
217
218    node_js = os.path.relpath(options.nodejs_path, options.ace_loader_home)
219    assets_dict = dict()
220    if options.js_assets_dir:
221        assets_dict['js_assets_dir'] = options.js_assets_dir
222    if options.ets_assets_dir:
223        assets_dict['ets_assets_dir'] = options.ets_assets_dir
224    if options.js_forms_dir:
225        assets_dict['js_forms_dir'] = options.js_forms_dir
226    if options.testrunner_dir:
227        assets_dict['testrunner_dir'] = options.testrunner_dir
228
229    for assets_name, assets_dir in assets_dict.items():
230        for asset in assets_dir:
231            depfiles.extend(build_utils.get_all_files(asset))
232        if assets_name == 'ets_assets_dir':
233            js2abc = False
234            loader_home = options.ets_loader_home
235            webpack_config = options.webpack_config_ets
236            webpack_path = options.webpack_ets
237        else:
238            js2abc = True
239            loader_home = options.ace_loader_home
240            webpack_config = options.webpack_config_js
241            webpack_path = options.webpack_js
242        cmd = [
243            node_js,
244            os.path.relpath(webpack_path, loader_home),
245            '--config',
246            os.path.relpath(webpack_config, loader_home)
247        ]
248        ark_es2abc_dir = os.path.relpath(options.ark_es2abc_dir, loader_home)
249        if options.app_profile:
250            cmd.extend(['--env', 'buildMode={}'.format(options.build_mode), 'compilerType=ark',
251                        'arkFrontendDir={}'.format(ark_es2abc_dir), 'nodeJs={}'.format(node_js)])
252        else:
253            cmd.extend(['--env', 'compilerType=ark',
254                        'arkFrontendDir={}'.format(ark_es2abc_dir), 'nodeJs={}'.format(node_js)])
255        build_utils.call_and_write_depfile_if_stale(
256            lambda: build_ace(cmd, options, js2abc, loader_home, assets_dir, assets_name),
257            options,
258            depfile_deps=depfiles,
259            input_paths=depfiles + inputs,
260            input_strings=cmd + [options.build_mode],
261            output_paths=([options.output]),
262            force=False,
263            add_pydeps=False)
264
265if __name__ == '__main__':
266    sys.exit(main(sys.argv[1:]))
267