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