• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4# Copyright (c) 2025 Huawei Device Co., Ltd.
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import argparse
18import json
19import os
20import shutil
21import subprocess
22import sys
23import time
24from typing import Dict, List, Optional
25
26
27EXIT_CODE = {
28    "SUCCESS": 0,
29    "TIMEOUT": 1,
30    "EXECUTION_FAILURE": 2,
31    "CONFIG_ERROR": 3,
32    "FILE_NOT_FOUND": 4,
33    "MISSING_KEY": 5,
34    "UNKNOWN_ERROR": 6,
35    "PERMISSION_ERROR": 7,
36    "INVALID_CONFIG": 8,
37    "PATHS_LENGTH_MISMATCH": 9,
38}
39
40
41class SubprocessTimeoutError(Exception):
42    """Exception raised when subprocess execution times out."""
43
44
45class SubprocessRunError(Exception):
46    """Exception raised when subprocess fails to execute."""
47
48
49class ConfigValidationError(Exception):
50    """Exception raised when configuration validation fails."""
51
52
53class PathsLengthMismatchError(Exception):
54    """Exception raised when paths_keys and paths_values have different lengths."""
55
56
57def set_environment(env_path: str, node_path: Optional[str] = None) -> Dict[str, str]:
58    """Create environment variables with updated LD_LIBRARY_PATH."""
59    env = os.environ.copy()
60    env["LD_LIBRARY_PATH"] = env_path
61    if node_path is not None:
62        env["PATH"] = f"{node_path}:{env['PATH']}"
63    return env
64
65
66def build_es2panda_command(es2panda_path: str, arktsconfig: str) -> List[str]:
67    """Construct es2panda command arguments."""
68    return [es2panda_path, "--arktsconfig", arktsconfig, "--ets-module"]
69
70
71def build_driver_command(entry_path: str, build_config_path: str) -> List[str]:
72    """Construct driver command arguments."""
73    return ["node", entry_path, build_config_path]
74
75
76def execute_driver(
77    entry_path: str, build_config_path: str, env_path: str, node_path: str, timeout: str
78) -> str:
79    """Execute es2panda compilation process."""
80    cmd = build_driver_command(entry_path, build_config_path)
81    env = set_environment(env_path, node_path)
82    return run_subprocess(cmd, timeout, env)
83
84
85def build_es2panda_command_stdlib(
86    es2panda_path: str, arktsconfig: str, dst_path: str
87) -> List[str]:
88    """Construct es2panda command arguments."""
89    return [
90        es2panda_path,
91        "--arktsconfig",
92        arktsconfig,
93        "--ets-module",
94        "--gen-stdlib=true",
95        "--output=" + dst_path,
96        "--extension=ets",
97        "--opt-level=2",
98    ]
99
100
101def run_subprocess(cmd: List[str], timeout: str, env: Dict[str, str]) -> str:
102    """
103    Execute a subprocess with timeout and environment settings.
104
105    Args:
106        cmd: Command sequence to execute
107        timeout: Maximum execution time in seconds (as string)
108        env: Environment variables dictionary
109
110    Returns:
111        Captured standard output
112
113    Raises:
114        SubprocessTimeoutError: When process exceeds timeout
115        SubprocessRunError: When process returns non-zero status
116    """
117    try:
118        timeout_sec = int(timeout)
119        if timeout_sec <= 0:
120            raise ValueError("Timeout must be a positive integer")
121
122        process = subprocess.Popen(
123            cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
124        )
125
126        try:
127            stdout, stderr = process.communicate(timeout=timeout_sec)
128        except subprocess.TimeoutExpired:
129            process.kill()
130            stdout, stderr = process.communicate(timeout=timeout_sec)
131            raise SubprocessTimeoutError(
132                f"Command '{' '.join(cmd)}' timed out after {timeout_sec} seconds"
133            )
134
135        if process.returncode != 0:
136            raise SubprocessRunError(
137                f"Command '{' '.join(cmd)}' failed with return code {process.returncode}\n"
138                f"Standard Error:\n{stderr}\n"
139                f"Standard Output:\n{stdout}"
140            )
141
142        return stdout
143
144    except ValueError as e:
145        raise SubprocessRunError(f"Invalid timeout value: {e}")
146    except OSError as e:
147        raise SubprocessRunError(f"OS error occurred: {e}")
148    except Exception as e:
149        raise SubprocessRunError(f"Unexpected error: {e}")
150
151
152def execute_es2panda(
153    es2panda_path: str, arktsconfig: str, env_path: str, timeout: str
154) -> str:
155    """Execute es2panda compilation process."""
156    cmd = build_es2panda_command(es2panda_path, arktsconfig)
157    env = set_environment(env_path)
158    return run_subprocess(cmd, timeout, env)
159
160
161def execute_es2panda_stdlib(
162    es2panda_path: str, arktsconfig: str, env_path: str, timeout: str, dst_path: str
163) -> str:
164    """Execute es2panda compilation process."""
165    cmd = build_es2panda_command_stdlib(es2panda_path, arktsconfig, dst_path)
166    env = set_environment(env_path)
167    return run_subprocess(cmd, timeout, env)
168
169
170def collect_abc_files(output_dir: str) -> List[str]:
171    """Recursively collect all .abc files in directory."""
172    abc_files = []
173    for root, _, files in os.walk(output_dir):
174        for file in files:
175            if file.endswith(".abc"):
176                abc_files.append(os.path.join(root, file))
177    return abc_files
178
179
180def build_ark_link_command(
181    ark_link_path: str, output_path: str, abc_files: List[str]
182) -> List[str]:
183    """Construct ark_link command arguments."""
184    return [ark_link_path, f"--output={output_path}", "--", *abc_files]
185
186
187def execute_ark_link(
188    ark_link_path: str, output_path: str, output_dir: str, env_path: str, timeout: str
189) -> str:
190    """Execute ark_link process to bundle ABC files."""
191    abc_files = collect_abc_files(output_dir)
192    cmd = build_ark_link_command(ark_link_path, output_path, abc_files)
193    env = set_environment(env_path)
194    return run_subprocess(cmd, timeout, env)
195
196
197def create_base_parser() -> argparse.ArgumentParser:
198    """Create and configure the base argument parser."""
199    parser = argparse.ArgumentParser()
200    add_required_arguments(parser)
201    add_optional_arguments(parser)
202    add_ui_arguments(parser)
203    return parser
204
205
206def add_required_arguments(parser: argparse.ArgumentParser) -> None:
207    """Add required arguments to the parser."""
208    parser.add_argument("--dst-file", type=str, required=True,
209                      help="Path for final dst file")
210    parser.add_argument("--env-path", type=str, required=True,
211                      help="Value for LD_LIBRARY_PATH environment variable")
212    parser.add_argument("--bootpath-json-file", type=str, required=True,
213                      help="bootpath.json file records the path in device for boot abc files")
214
215
216def add_optional_arguments(parser: argparse.ArgumentParser) -> None:
217    """Add optional arguments to the parser."""
218    parser.add_argument("--arktsconfig", type=str, required=False,
219                    help="Path to arktsconfig.json configuration file")
220    parser.add_argument("--es2panda", type=str, required=False,
221                      help="Path to es2panda executable")
222    parser.add_argument("--ark-link", type=str, required=False,
223                      help="Path to ark_link executable")
224    parser.add_argument("--timeout-limit", type=str, default="12000",
225                      help="Process timeout in seconds (default: 12000)")
226    parser.add_argument("--cache-path", type=str, default=None,
227                      help="Path to cache directory")
228    parser.add_argument("--is-boot-abc", type=bool, default=False,
229                      help="Flag indicating if the file is a boot abc")
230    parser.add_argument("--device-dst-file", type=str, default=None,
231                      help="Path for device dst file. Required if 'is-boot-abc' is True")
232    parser.add_argument("--target-name", type=str,
233                      help="target name")
234    parser.add_argument("--is-stdlib", type=bool, default=False,
235                      help="Flag indicating if the compile target is etsstdlib")
236    parser.add_argument("--root-dir", required=False,
237                      help="Root directory for the project")
238    parser.add_argument("--base-url", required=False,
239                      help="Base URL for the project")
240    parser.add_argument("--package", required=False,
241                      help="Package name for the project")
242    parser.add_argument("--std-path", required=False,
243                      help="Path to the standard library")
244    parser.add_argument("--escompat-path", required=False,
245                      help="Path to the escompat library")
246    parser.add_argument("--scan-path", nargs="+", required=False,
247                      help="List of directories to scan for target files")
248    parser.add_argument("--include", nargs="+", required=False,
249                      help="List of file patterns to include in the compilation")
250    parser.add_argument("--exclude", nargs="+", required=False,
251                      help="List of file patterns to exclude from the compilation")
252    parser.add_argument("--files", nargs="+", required=False,
253                      help="List of specific files to compile")
254    parser.add_argument("--paths-keys", nargs="+", required=False,
255                      help="List of keys for custom paths")
256    parser.add_argument("--paths-values", nargs="+", required=False,
257                      help="List of values for custom paths. Each value corresponds to a key in --paths-keys")
258
259
260def add_ui_arguments(parser: argparse.ArgumentParser) -> None:
261    """Add UI-related arguments to the parser."""
262    parser.add_argument("--ui-enable", default=False, required=False,
263                      help="Flag indicating if the compile supports ui syntax")
264    parser.add_argument("--build-sdk-path", default=None, required=False,
265                      help="Path for sdk. Required if 'ui-enable' is True")
266    parser.add_argument("--panda-stdlib-path", default=None, required=False,
267                      help="Path for stdlib")
268    parser.add_argument("--ui-plugin", default=None, required=False,
269                      help="Path for ui plugin. Required if 'ui-enable' is True")
270    parser.add_argument("--memo-plugin", default=None, required=False,
271                      help="Path for memo plugin. Required if 'ui-enable' is True")
272    parser.add_argument("--entry-path", default=None, required=False,
273                      help="Path for driver entry. Required if 'ui-enable' is True")
274    parser.add_argument("--driver-config-path", default=None, required=False,
275                    help="Path for driver config. Required if 'ui-enable' is True")
276    parser.add_argument("--node-path", default=None, required=False,
277                    help="Path for node")
278
279
280def parse_arguments() -> argparse.Namespace:
281    """Configure and parse command line arguments."""
282    parser = create_base_parser()
283    return parser.parse_args()
284
285
286def validate_arktsconfig(config: Dict) -> None:
287    """Validate the structure and content of arktsconfig."""
288    if "compilerOptions" not in config:
289        raise ConfigValidationError("Missing 'compilerOptions' in config")
290    if "outDir" not in config["compilerOptions"]:
291        raise ConfigValidationError("Missing 'outDir' in compilerOptions")
292
293
294def modify_arktsconfig_with_cache(arktsconfig_path: str, cache_path: str) -> None:
295    """
296    Modify arktsconfig.json to use cache_path as outDir.
297    Backup the original file, modify it, and restore it after use.
298    """
299    backup_path = arktsconfig_path + ".bak"
300    shutil.copy(arktsconfig_path, backup_path)
301
302    try:
303        config = {}
304        if os.path.exists(arktsconfig_path):
305            with open(arktsconfig_path, "r") as f:
306                content = f.read()
307                config = json.loads(content)
308            if "compilerOptions" in config:
309                config["compilerOptions"]["outDir"] = cache_path
310
311        if os.path.exists(arktsconfig_path):
312            fd = os.open(arktsconfig_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o777)
313            with os.fdopen(fd, "w", encoding="utf-8") as f:
314                json.dump(config, f, indent=2, ensure_ascii=False)
315    except json.JSONDecodeError as e:
316        print(f"{arktsconfig_path} Invalid JSON format (cache): {e}", file=sys.stderr)
317        sys.exit()
318    except Exception as e:
319        restore_arktsconfig(arktsconfig_path)
320        raise e
321
322
323def restore_arktsconfig(arktsconfig_path: str) -> None:
324    """Restore the original arktsconfig.json from backup."""
325    backup_path = arktsconfig_path + ".bak"
326    if os.path.exists(backup_path):
327        shutil.move(backup_path, arktsconfig_path)
328
329
330def add_to_bootpath(device_dst_file: str, bootpath_json_file: str, target_name: str) -> None:
331    print(f"Received target name {target_name}")
332    try:
333        directory = os.path.dirname(bootpath_json_file)
334        new_json_file = os.path.join(directory, f"{target_name}_bootpath.json")
335
336        data = {}
337        if os.path.exists(new_json_file):
338            with open(new_json_file, "r", encoding="utf-8") as f:
339                data = json.load(f)
340
341        current_value = data.get("bootpath", "")
342        abc_set = set(current_value.split(":")) if current_value else set()
343        abc_set.add(device_dst_file)
344        new_value = ":".join(abc_set)
345        data["bootpath"] = new_value
346
347        os.makedirs(os.path.dirname(new_json_file), exist_ok=True)
348        fd = os.open(new_json_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o777)
349        with os.fdopen(fd, "w", encoding="utf-8") as f:
350            json.dump(data, f, indent=4, ensure_ascii=False)
351        print(f"{target_name}_bootpath.json has been created")
352    except json.JSONDecodeError as e:
353        print(f"{new_json_file} Invalid JSON format (bootpath): {e}", file=sys.stderr)
354        sys.exit()
355
356
357def is_target_file(file_name: str) -> bool:
358    """
359    Check if the given file name is a target file.
360    """
361    target_extensions = [".d.ets", ".ets"]
362    return any(file_name.endswith(ext) for ext in target_extensions)
363
364
365def get_key_from_file_name(file_name: str) -> str:
366    """
367    Extract the key from the given file name.
368    """
369    if ".d." in file_name:
370        file_name = file_name.replace(".d.", ".")
371    return os.path.splitext(file_name)[0]
372
373
374def scan_directory_for_paths(directory: str) -> Dict[str, List[str]]:
375    """
376    Scan the specified directory to find all target files and organize their paths by key.
377    If the first-level directory is 'arkui' and the second-level directory is 'runtime-api',
378    the key is the file name. Otherwise, the key is the relative path with '/' replaced by '.'.
379    """
380    paths = {}
381    for root, _, files in os.walk(directory):
382        for file in files:
383            if not is_target_file(file):
384                continue
385            file_path = os.path.abspath(os.path.join(root, file))
386            file_name = get_key_from_file_name(file)
387            file_abs_path = os.path.abspath(os.path.join(root, file_name))
388            file_rel_path = os.path.relpath(file_abs_path, start=directory)
389            # Split the relative path into components
390            path_components = file_rel_path.split(os.sep)
391            first_level_dir = path_components[0] if len(path_components) > 0 else ""
392            second_level_dir = path_components[1] if len(path_components) > 1 else ""
393            # Determine the key based on directory structure
394            if first_level_dir == "arkui" and second_level_dir == "runtime-api":
395                key = file_name
396            else:
397                key = file_rel_path.replace(os.sep, ".")
398            if key in paths:
399                paths[key].append(file_path)
400            else:
401                paths[key] = [file_path]
402    return paths
403
404
405def build_config(args: argparse.Namespace) -> None:
406    """
407    Build the configuration dictionary based on command-line arguments.
408    """
409    paths = {}
410    for scan_path in args.scan_path:
411        scanned_paths = scan_directory_for_paths(scan_path)
412        for key, value in scanned_paths.items():
413            if key in paths:
414                paths[key].extend(value)
415            else:
416                paths[key] = value
417    paths["std"] = [args.std_path]
418    paths["escompat"] = [args.escompat_path]
419
420    if args.paths_keys and args.paths_values:
421        if len(args.paths_keys) != len(args.paths_values):
422            raise PathsLengthMismatchError(
423                "paths_keys and paths_values must have the same length"
424            )
425        for key, value in zip(args.paths_keys, args.paths_values):
426            paths[key] = [os.path.abspath(value)]
427
428    config = {
429        "compilerOptions": {
430            "rootDir": args.root_dir,
431            "baseUrl": args.base_url,
432            "paths": paths,
433            "outDir": args.cache_path,
434            "package": args.package if args.package else "",
435            "useEmptyPackage": True
436        }
437    }
438
439    if args.include:
440        config["include"] = args.include
441    if args.exclude:
442        config["exclude"] = args.exclude
443    if args.files:
444        config["files"] = args.files
445
446    os.makedirs(os.path.dirname(args.arktsconfig), exist_ok=True)
447    fd = os.open(args.arktsconfig, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o777)
448    with os.fdopen(fd, "w", encoding="utf-8") as f:
449        json.dump(config, f, indent=2, ensure_ascii=False)
450
451
452def build_driver_config(args: argparse.Namespace) -> None:
453    """
454    Build the driver configuration dictionary based on command-line arguments.
455    """
456    paths = {}
457    if args.paths_keys and args.paths_values:
458        if len(args.paths_keys) != len(args.paths_values):
459            raise PathsLengthMismatchError(
460                "paths_keys and paths_values must have the same length"
461            )
462        for key, value in zip(args.paths_keys, args.paths_values):
463            paths[key] = [os.path.abspath(value)]
464
465    config = {
466        "plugins": {},
467        "compileFiles": args.files,
468        "packageName": args.package if args.package else "",
469        "buildType": "build",
470        "buildMode": "Release",
471        "moduleRootPath": args.base_url,
472        "sourceRoots": ["./"],
473        "paths": paths,
474        "loaderOutPath": args.dst_file,
475        "cachePath": args.cache_path,
476        "buildSdkPath": args.build_sdk_path,
477        "dependentModuleList": [],
478        "frameworkMode": True,
479        "useEmptyPackage": True,
480    }
481
482    plugins = {}
483    if args.ui_plugin is not None:
484        plugins["ui_plugin"] = args.ui_plugin
485    if args.memo_plugin is not None:
486        plugins["memo_plugin"] = args.memo_plugin
487    if plugins:
488        config["plugins"] = plugins
489
490    if args.panda_stdlib_path:
491        config["pandaStdlibPath"] = args.panda_stdlib_path
492    if args.paths_keys:
493        config["pathsKeys"] = args.paths_keys
494    if args.paths_values:
495        config["pathsValues"] = args.paths_values
496
497    os.makedirs(os.path.dirname(args.arktsconfig), exist_ok=True)
498    fd = os.open(args.arktsconfig, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o777)
499    with os.fdopen(fd, "w", encoding="utf-8") as f:
500        json.dump(config, f, indent=2, ensure_ascii=False)
501
502
503def handle_configuration(args: argparse.Namespace) -> None:
504    """
505    Handle the configuration setup based on command-line arguments.
506    """
507    if args.ui_enable == "True":
508        build_driver_config(args)
509    elif args.base_url:
510        build_config(args)
511    else:
512        modify_arktsconfig_with_cache(args.arktsconfig, args.cache_path)
513
514
515def main() -> None:
516    """Main compilation workflow."""
517    start_time = time.time()
518    args = parse_arguments()
519
520    try:
521        handle_configuration(args)
522
523        if args.ui_enable == "True":
524            execute_driver(args.entry_path, args.arktsconfig, args.env_path, args.node_path, args.timeout_limit)
525        elif args.is_stdlib:
526            execute_es2panda_stdlib(args.es2panda, args.arktsconfig, args.env_path, args.timeout_limit, args.dst_file)
527        else:
528            execute_es2panda(args.es2panda, args.arktsconfig, args.env_path, args.timeout_limit)
529            execute_ark_link(args.ark_link, args.dst_file, args.cache_path, args.env_path, args.timeout_limit)
530
531        if args.is_boot_abc:
532            add_to_bootpath(args.device_dst_file, args.bootpath_json_file, args.target_name)
533
534        print(f"Compilation succeeded in {time.time() - start_time:.2f} seconds")
535        sys.exit(EXIT_CODE["SUCCESS"])
536    except SubprocessTimeoutError as e:
537        print(f"[FATAL] Process timeout: {e}", file=sys.stderr)
538        sys.exit(EXIT_CODE["TIMEOUT"])
539    except SubprocessRunError as e:
540        print(f"[ERROR] Execution failed: {e}", file=sys.stderr)
541        sys.exit(EXIT_CODE["EXECUTION_FAILURE"])
542    except FileNotFoundError as e:
543        print(f"[IO ERROR] File not found: {e}", file=sys.stderr)
544        sys.exit(EXIT_CODE["FILE_NOT_FOUND"])
545    except KeyError as e:
546        print(f"[CONFIG] Missing required key: {e}", file=sys.stderr)
547        sys.exit(EXIT_CODE["MISSING_KEY"])
548    except PermissionError as e:
549        print(f"[PERMISSION] Access denied: {e}", file=sys.stderr)
550        sys.exit(EXIT_CODE["PERMISSION_ERROR"])
551    except ConfigValidationError as e:
552        print(f"[CONFIG] Invalid configuration: {e}", file=sys.stderr)
553        sys.exit(EXIT_CODE["INVALID_CONFIG"])
554    except PathsLengthMismatchError as e:
555        print(f"[CONFIG] PathsLengthMismatchError: {e}", file=sys.stderr)
556        sys.exit(EXIT_CODE["PATHS_LENGTH_MISMATCH"])
557    except Exception as e:
558        print(f"[UNKNOWN] Unexpected error: {e}", file=sys.stderr)
559        sys.exit(EXIT_CODE["UNKNOWN_ERROR"])
560    finally:
561        if not args.base_url:
562            restore_arktsconfig(args.arktsconfig)
563
564
565if __name__ == "__main__":
566    main()
567