• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# coding=utf-8
2#
3# Copyright (c) 2025 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 json
18import logging
19import os
20import shutil
21import subprocess
22import sys
23import tarfile
24import time
25from collections.abc import Iterable, Mapping, Sequence
26from dataclasses import dataclass
27from enum import Enum
28from pathlib import Path
29
30from taihe.driver.backend import BackendRegistry
31from taihe.driver.contexts import CompilerInstance, CompilerInvocation
32from taihe.utils.logging import setup_logger
33from taihe.utils.outputs import CMakeOutputConfig, DebugLevel, OutputConfig
34from taihe.utils.resources import ResourceLocator, ResourceType
35
36# A lower value means more verbosity
37TRACE_CONCISE = logging.DEBUG - 1
38TRACE_VERBOSE = TRACE_CONCISE - 1
39
40
41class UserType(Enum):
42    """User type for the build system."""
43
44    STS = "sts"
45    CPP = "cpp"
46
47
48@dataclass
49class UserInfo:
50    """User information for authentication."""
51
52    username: str
53    password: str
54
55
56class BuildUtils:
57    """Utility class for common operations."""
58
59    def __init__(self, verbosity: int):
60        setup_logger(verbosity)
61        self.logger = logging.getLogger("build_system")
62
63    def run_command(
64        self,
65        command: Sequence[Path | str],
66        capture_output: bool = False,
67        env: Mapping[str, Path | str] | None = None,
68    ) -> float:
69        """Run a command with environment variables."""
70        command_str = " ".join(map(str, command))
71
72        env_str = ""
73        for key, val in (env or {}).items():
74            env_str += f"{key}={val} "
75
76        self.logger.debug("+ %s%s", env_str, command_str)
77
78        try:
79            start_time = time.time()
80            subprocess.run(
81                command,
82                check=True,
83                text=True,
84                env=env,
85                capture_output=capture_output,
86            )
87            end_time = time.time()
88            elapsed_time = end_time - start_time
89            return elapsed_time
90        except subprocess.CalledProcessError as e:
91            self.logger.error("Command failed with exit code %s", e.returncode)
92            if e.stdout:
93                self.logger.error("Standard output: %s", e.stdout)
94            if e.stderr:
95                self.logger.error("Standard error: %s", e.stderr)
96            raise
97
98    def create_directory(self, directory: Path) -> None:
99        directory.mkdir(parents=True, exist_ok=True)
100        self.logger.debug("Created directory: %s", directory)
101
102    def clean_directory(self, directory: Path) -> None:
103        if not directory.exists():
104            return
105        shutil.rmtree(directory)
106        self.logger.debug("Cleaned directory: %s", directory)
107
108    def move_directory(self, src: Path, dst: Path) -> None:
109        if not src.exists():
110            raise FileNotFoundError(f"Source directory does not exist: {src}")
111        shutil.move(src, dst)
112        self.logger.debug("Moved directory from %s to %s", src, dst)
113
114    def copy_directory(self, src: Path, dst: Path) -> None:
115        if not src.exists():
116            raise FileNotFoundError(f"Source directory does not exist: {src}")
117        shutil.copytree(src, dst, dirs_exist_ok=True)
118        self.logger.debug("Copied directory from %s to %s", src, dst)
119
120    def download_file(
121        self,
122        target_file: Path,
123        url: str,
124        user_info: UserInfo | None = None,
125    ) -> None:
126        """Download a file from a URL."""
127        if target_file.exists():
128            self.logger.info("Already found %s, skipping download", target_file)
129            return
130
131        temp_file = target_file.with_suffix(".tmp")
132
133        command = ["curl", "-L", "--progress-bar", url, "-o", temp_file]
134
135        if user_info:
136            command.extend(["-u", f"{user_info.username}:{user_info.password}"])
137
138        self.run_command(command)
139
140        if temp_file.exists():
141            temp_file.rename(target_file)
142            self.logger.info("Downloaded %s to %s", url, target_file)
143        else:
144            self.logger.error("Failed to download %s", url)
145            raise FileNotFoundError(f"Failed to download {url}")
146
147    def extract_file(
148        self,
149        target_file: Path,
150        extract_dir: Path,
151    ) -> None:
152        """Extract a tar.gz file."""
153        if not target_file.exists():
154            raise FileNotFoundError(f"File to extract does not exist: {target_file}")
155
156        self.create_directory(extract_dir)
157
158        with tarfile.open(target_file, "r:gz") as tar:
159            # Check for any unsafe paths before extraction
160            for member in tar.getmembers():
161                member_path = Path(member.name)
162                if member_path.is_absolute() or ".." in member_path.parts:
163                    raise ValueError(f"Unsafe path in archive: {member.name}")
164            # Extract safely
165            tar.extractall(path=extract_dir)
166
167        self.logger.info("Extracted %s to %s", target_file, extract_dir)
168
169
170class BuildConfig:
171    """Configuration for the build process."""
172
173    def __init__(self):
174        self.cxx = os.getenv("CXX", "clang++")
175        self.cc = os.getenv("CC", "clang")
176        self.panda_userinfo = UserInfo(
177            username=os.getenv("PANDA_USERNAME", "koala-pub"),
178            password=os.getenv("PANDA_PASSWORD", "y3t!n0therP"),
179        )
180        self.panda_url = "https://nexus.bz-openlab.ru:10443/repository/koala-npm/%40panda/sdk/-/sdk-1.5.0-dev.36922.tgz"
181
182        self.locator = ResourceLocator.detect()
183        self.panda_extract_dir = self.locator.get(ResourceType.DEV_PANDA_VM)
184        self.panda_package_dir = self.panda_extract_dir / "package"
185        self.panda_ets_dir = self.panda_package_dir / "ets"
186        self.panda_tool_dir = self.panda_package_dir / "linux_host_tools"
187        self.panda_include_dir = (
188            self.panda_package_dir / "ohos_arm64/include/plugins/ets/runtime/ani"
189        )
190        # TODO this should be removed
191        self.taihe_version_file = self.locator.root_dir / "version.txt"
192        self.panda_version_file = self.panda_package_dir / "version.txt"
193
194
195def _map_output_debug_level(verbosity: int) -> DebugLevel:
196    if verbosity <= TRACE_VERBOSE:
197        return DebugLevel.VERBOSE
198    if verbosity <= TRACE_CONCISE:
199        return DebugLevel.CONCISE
200    return DebugLevel.NONE
201
202
203class BuildSystem(BuildUtils):
204    """Main build system class."""
205
206    def __init__(
207        self,
208        target_dir: str,
209        user: UserType,
210        config: BuildConfig,
211        verbosity: int = logging.INFO,
212    ):
213        super().__init__(verbosity)
214        self.config = config
215        self.should_run_pretty_print = verbosity <= logging.DEBUG
216        self.codegen_debug_level = _map_output_debug_level(verbosity)
217
218        self.user = user
219
220        # Build paths
221        self.target_path = Path(target_dir).resolve()
222
223        self.idl_dir = self.target_path / "idl"
224        self.build_dir = self.target_path / "build"
225
226        self.generated_dir = self.target_path / "generated"
227        self.generated_include_dir = self.generated_dir / "include"
228        self.generated_src_dir = self.generated_dir / "src"
229
230        self.author_dir = self.target_path / "author"
231        self.author_include_dir = self.author_dir / "include"
232        self.author_src_dir = self.author_dir / "src"
233
234        self.user_dir = self.target_path / "user"
235        self.user_include_dir = self.user_dir / "include"
236        self.user_src_dir = self.user_dir / "src"
237
238        self.runtime_includes = [self.config.locator.get(ResourceType.RUNTIME_HEADER)]
239        if self.user == UserType.STS:
240            self.runtime_includes.append(self.config.panda_include_dir)
241        self.generated_includes = [*self.runtime_includes, self.generated_include_dir]
242        self.author_includes = [*self.generated_includes, self.author_include_dir]
243        self.user_includes = [*self.generated_includes, self.user_include_dir]
244
245        # Build sub-directories
246        self.build_generated_src_dir = self.build_dir / "generated" / "src"
247        self.build_author_src_dir = self.build_dir / "author" / "src"
248        self.build_runtime_src_dir = self.build_dir / "runtime" / "src"
249        self.build_generated_dir = self.build_dir / "generated"
250        self.build_user_dir = self.build_dir / "user"
251
252        # Output files
253        self.lib_name = self.target_path.absolute().name
254        self.so_target = self.build_dir / f"lib{self.lib_name}.so"
255        self.abc_target = self.build_dir / "main.abc"
256        self.exe_target = self.build_dir / "main"
257        self.arktsconfig_file = self.build_dir / "arktsconfig.json"
258
259    def create(self) -> None:
260        """Create a simple example project."""
261        self.create_idl()
262        self.create_author_cpp()
263        if self.user == UserType.STS:
264            self.create_user_ets()
265        if self.user == UserType.CPP:
266            self.create_user_cpp()
267
268    def create_idl(self) -> None:
269        """Create a simple example IDL file."""
270        self.create_directory(self.idl_dir)
271        with open(self.idl_dir / "hello.taihe", "w") as f:
272            f.write(f"function sayHello(): void;\n")
273
274    def create_author_cpp(self) -> None:
275        """Create a simple example author source file."""
276        self.create_directory(self.author_src_dir)
277        with open(self.author_dir / "compile_flags.txt", "w") as f:
278            for author_include_dir in self.author_includes:
279                f.write(f"-I{author_include_dir}\n")
280        with open(self.author_src_dir / "hello.impl.cpp", "w") as f:
281            f.write(
282                f'#include "hello.proj.hpp"\n'
283                f'#include "hello.impl.hpp"\n'
284                f"\n"
285                f"#include <iostream>\n"
286                f"\n"
287                f"void sayHello() {{\n"
288                f'    std::cout << "Hello, World!" << std::endl;\n'
289                f"    return;\n"
290                f"}}\n"
291                f"\n"
292                f"TH_EXPORT_CPP_API_sayHello(sayHello);\n"
293            )
294
295    def create_user_ets(self) -> None:
296        """Create a simple example user ETS file."""
297        self.create_directory(self.user_dir)
298        with open(self.user_dir / "main.ets", "w") as f:
299            f.write(
300                f'import * as hello from "hello";\n'
301                f"\n"
302                f'loadLibrary("{self.lib_name}");\n'
303                f"\n"
304                f"function main() {{\n"
305                f"    hello.sayHello();\n"
306                f"}}\n"
307            )
308
309    def create_user_cpp(self) -> None:
310        """Create a simple example user source file."""
311        self.create_directory(self.user_src_dir)
312        with open(self.user_dir / "compile_flags.txt", "w") as f:
313            for user_include_dir in self.user_includes:
314                f.write(f"-I{user_include_dir}\n")
315        with open(self.user_src_dir / "main.cpp", "w") as f:
316            f.write(
317                f'#include "hello.user.hpp"\n'
318                f"\n"
319                f"int main() {{\n"
320                f"    hello::sayHello();\n"
321                f"    return 0;\n"
322                f"}}\n"
323            )
324
325    def generate(self, sts_keep_name: bool, cmake: bool) -> None:
326        """Generate code from IDL files."""
327        if not self.idl_dir.is_dir():
328            raise FileNotFoundError(f"IDL directory not found: '{self.idl_dir}'")
329
330        self.clean_directory(self.generated_dir)
331
332        self.logger.info("Generating author and ani codes...")
333
334        registry = BackendRegistry()
335        registry.register_all()
336        backend_names = ["cpp-author"]
337        if self.user == UserType.STS:
338            backend_names.append("ani-bridge")
339        if self.user == UserType.CPP:
340            backend_names.append("cpp-user")
341        if self.should_run_pretty_print:
342            backend_names.append("pretty-print")
343        backends = registry.collect_required_backends(backend_names)
344        resolved_backends = [b() for b in backends]
345
346        if cmake:
347            output_config = CMakeOutputConfig(
348                dst_dir=Path(self.generated_dir),
349                runtime_include_dir=self.config.locator.get(
350                    ResourceType.RUNTIME_HEADER
351                ),
352                runtime_src_dir=self.config.locator.get(ResourceType.RUNTIME_SOURCE),
353            )
354        else:
355            output_config = OutputConfig(
356                dst_dir=Path(self.generated_dir),
357            )
358
359        invocation = CompilerInvocation(
360            src_files=[
361                src_file
362                for src_dir in [
363                    self.idl_dir,
364                    self.config.locator.get(ResourceType.STDLIB),
365                ]
366                for src_file in src_dir.glob("*.taihe")
367            ],
368            output_config=output_config,
369            backends=resolved_backends,
370            sts_keep_name=sts_keep_name,
371        )
372
373        instance = CompilerInstance(invocation)
374        if not instance.run():
375            raise RuntimeError(f"Code generation failed")
376
377    def build(self, opt_level: str) -> None:
378        """Run the complete build process."""
379        self.logger.info("Starting ANI compilation...")
380
381        self.setup_build_directories()
382
383        if self.user == UserType.STS:
384            # Set up paths for Panda VM
385            self.prepare_panda_vm()
386
387            # Compile the shared library
388            self.compile_shared_library(opt_level=opt_level)
389
390            # Compile and link ABC files
391            self.compile_and_link_ani()
392
393            # Run with Ark runtime
394            self.run_ani()
395        elif self.user == UserType.CPP:
396            # Compile the shared library
397            self.compile_shared_library(opt_level=opt_level)
398
399            # Compile the executable
400            self.compile_and_link_exe(opt_level=opt_level)
401
402            # Run the executable
403            self.run_exe()
404
405        self.logger.info("Build and execution completed successfully")
406
407    def compile_shared_library(self, opt_level: str):
408        """Compile the shared library."""
409        self.logger.info("Compiling shared library...")
410
411        runtime_src_dir = self.config.locator.get(ResourceType.RUNTIME_SOURCE)
412        runtime_sources = [
413            runtime_src_dir / "string.cpp",
414            runtime_src_dir / "object.cpp",
415        ]
416        if self.user == UserType.STS:
417            runtime_sources.append(runtime_src_dir / "runtime.cpp")
418
419        # Compile each component
420        runtime_objects = self.compile(
421            self.build_runtime_src_dir,
422            runtime_sources,
423            self.runtime_includes,
424            compile_flags=[f"-O{opt_level}"],
425        )
426        generated_objects = self.compile(
427            self.build_generated_src_dir,
428            self.generated_src_dir.glob("*.[cC]*"),
429            self.generated_includes,
430            compile_flags=[f"-O{opt_level}"],
431        )
432        author_objects = self.compile(
433            self.build_author_src_dir,
434            self.author_src_dir.glob("*.[cC]*"),
435            self.author_includes,
436            compile_flags=[f"-O{opt_level}"],
437        )
438
439        # Link all objects
440        if all_objects := runtime_objects + generated_objects + author_objects:
441            self.link(
442                self.so_target,
443                all_objects,
444                shared=True,
445                link_options=["-Wl,--no-undefined"],
446            )
447            self.logger.info("Shared library compiled: %s", self.so_target)
448        else:
449            self.logger.warning(
450                "No object files to link, skipping shared library compilation"
451            )
452
453    def compile_and_link_exe(self, opt_level: str) -> None:
454        """Compile and link the executable."""
455        self.logger.info("Compiling and linking executable...")
456
457        # Compile the user source files
458        user_objects = self.compile(
459            self.build_user_dir,
460            self.user_src_dir.glob("*.[cC]*"),
461            self.user_includes,
462            compile_flags=[f"-O{opt_level}"],
463        )
464
465        # Link the executable
466        if user_objects:
467            self.link(
468                self.exe_target,
469                [self.so_target, *user_objects],
470            )
471            self.logger.info("Executable compiled: %s", self.so_target)
472        else:
473            self.logger.warning(
474                "No object files to link, skipping executable compilation"
475            )
476
477    def run_exe(self) -> None:
478        """Run the compiled executable."""
479        self.logger.info("Running executable...")
480
481        elapsed_time = self.run(
482            self.exe_target,
483            self.so_target.parent,
484        )
485
486        self.logger.info("Done, time = %f s", elapsed_time)
487
488    def compile_and_link_ani(self):
489        """Compile and link ABC files."""
490        self.logger.info("Compiling and linking ABC files...")
491
492        paths: dict[str, Path] = {}
493        for path in self.generated_dir.glob("*.ets"):
494            paths[path.stem] = path
495        for path in self.user_dir.glob("*.ets"):
496            paths[path.stem] = path
497
498        self.create_arktsconfig(self.arktsconfig_file, paths)
499
500        # Compile ETS files in each directory
501        generated_abc = self.compile_abc(
502            self.build_generated_dir,
503            self.generated_dir.glob("*.ets"),
504            self.arktsconfig_file,
505        )
506        user_abc = self.compile_abc(
507            self.build_user_dir,
508            self.user_dir.glob("*.ets"),
509            self.arktsconfig_file,
510        )
511
512        # Link all ABC files
513        if all_abc_files := generated_abc + user_abc:
514            self.link_abc(
515                self.abc_target,
516                all_abc_files,
517            )
518            self.logger.info("ABC files linked: %s", self.abc_target)
519        else:
520            self.logger.warning("No ABC files to link, skipping ABC compilation")
521
522    def run_ani(self) -> None:
523        """Run the compiled ABC file with the Ark runtime."""
524        self.logger.info("Running ABC file with Ark runtime...")
525
526        elapsed_time = self.run_abc(
527            self.abc_target,
528            self.so_target.parent,
529            entry="main.ETSGLOBAL::main",
530        )
531
532        self.logger.info("Done, time = %f s", elapsed_time)
533
534    def setup_build_directories(self) -> None:
535        """Set up necessary build directories."""
536        # Clean and create directories
537        self.clean_directory(self.build_dir)
538
539        self.create_directory(self.build_dir)
540        self.create_directory(self.build_runtime_src_dir)
541        self.create_directory(self.build_generated_src_dir)
542        self.create_directory(self.build_author_src_dir)
543        self.create_directory(self.build_generated_dir)
544        self.create_directory(self.build_user_dir)
545
546    def prepare_panda_vm(self):
547        """Download and extract Panda VM."""
548        self.create_directory(self.config.panda_extract_dir)
549
550        url = self.config.panda_url
551        filename = url.split("/")[-1]
552        target_file = self.config.panda_extract_dir / filename
553        version = Path(filename).stem  # Use the filename without extension as version
554
555        if not self.check_local_version(version):
556            self.clean_directory(self.config.panda_package_dir)
557            self.logger.info("Downloading panda VM version: %s", version)
558            self.download_file(target_file, url, self.config.panda_userinfo)
559            self.extract_file(target_file, self.config.panda_extract_dir)
560            self.write_local_version(version)
561            self.logger.info("Completed download and extraction.")
562
563    def check_local_version(self, version: str) -> bool:
564        """Check if the local version matches the desired version."""
565        if not self.config.panda_version_file.exists():
566            return False
567        try:
568            with open(self.config.panda_version_file) as vf:
569                local_version = vf.read().strip()
570                return local_version == version
571        except OSError as e:
572            self.logger.warning("Failed to read version file: %s", e)
573            return False
574
575    def write_local_version(self, version: str) -> None:
576        """Write the local version to the version file."""
577        try:
578            with open(self.config.panda_version_file, "w") as vf:
579                vf.write(version)
580        except OSError as e:
581            self.logger.warning("Failed to write version file: %s", e)
582
583    def compile(
584        self,
585        output_dir: Path,
586        input_files: Iterable[Path],
587        include_dirs: Sequence[Path] = (),
588        compile_flags: Sequence[str] = (),
589    ) -> list[Path]:
590        """Compile source files."""
591        output_files: list[Path] = []
592
593        for input_file in input_files:
594            name = input_file.name
595            output_file = output_dir / f"{name}.o"
596
597            if name.endswith(".c"):
598                compiler = self.config.cc
599                std = "gnu11"
600            else:
601                compiler = self.config.cxx
602                std = "gnu++17"
603
604            command = [
605                compiler,
606                "-c",
607                "-fvisibility=hidden",
608                "-fPIC",
609                # "-Wall",
610                # "-Wextra",
611                f"-std={std}",
612                "-o",
613                output_file,
614                input_file,
615                *compile_flags,
616            ]
617
618            for include_dir in include_dirs:
619                if include_dir.exists():  # Only include directories that exist
620                    command.append(f"-I{include_dir}")
621
622            self.run_command(command)
623
624            output_files.append(output_file)
625
626        return output_files
627
628    def link(
629        self,
630        output_file: Path,
631        input_files: Sequence[Path],
632        shared: bool = False,
633        link_options: Sequence[str] = (),
634    ) -> None:
635        """Link object files."""
636        if len(input_files) == 0:
637            self.logger.warning("No input files to link")
638            return
639
640        command = [
641            self.config.cxx,
642            "-fPIC",
643            "-o",
644            output_file,
645            *input_files,
646            *link_options,
647        ]
648
649        if shared:
650            command.append("-shared")
651
652        self.run_command(command)
653
654    def run(
655        self,
656        target: Path,
657        ld_lib_path: Path,
658        args: Sequence[str] = (),
659    ) -> float:
660        """Run the compiled target."""
661        command = [
662            target,
663            *args,
664        ]
665
666        return self.run_command(
667            command,
668            env={"LD_LIBRARY_PATH": ld_lib_path},
669        )
670
671    def create_arktsconfig(
672        self,
673        arktsconfig_file: Path,
674        app_paths: Mapping[str, Path] | None = None,
675    ) -> None:
676        """Create ArkTS configuration file."""
677        paths = {
678            "std": self.config.panda_ets_dir / "stdlib/std",
679            "escompat": self.config.panda_ets_dir / "stdlib/escompat",
680        }
681
682        if app_paths is not None:
683            paths.update(app_paths)
684
685        config_content = {
686            "compilerOptions": {
687                "baseUrl": str(self.config.panda_tool_dir),
688                "paths": {key: [str(value)] for key, value in paths.items()},
689            }
690        }
691
692        with open(arktsconfig_file, "w") as json_file:
693            json.dump(config_content, json_file, indent=2)
694
695        self.logger.debug("Created configuration file at: %s", arktsconfig_file)
696
697    def compile_abc(
698        self,
699        output_dir: Path,
700        input_files: Iterable[Path],
701        arktsconfig_file: Path,
702    ) -> list[Path]:
703        """Compile ETS files to ABC format."""
704        output_files: list[Path] = []
705
706        for input_file in input_files:
707            name = input_file.name
708            output_file = output_dir / f"{name}.abc"
709            output_dump = output_dir / f"{name}.abc.dump"
710
711            es2panda_path = self.config.panda_tool_dir / "bin/es2panda"
712
713            gen_abc_command = [
714                es2panda_path,
715                input_file,
716                "--output",
717                output_file,
718                "--extension",
719                "ets",
720                "--arktsconfig",
721                arktsconfig_file,
722            ]
723
724            self.run_command(gen_abc_command)
725
726            output_files.append(output_file)
727
728            ark_disasm_path = self.config.panda_tool_dir / "bin/ark_disasm"
729            if not ark_disasm_path.exists():
730                self.logger.warning(
731                    "ark_disasm not found at %s, skipping disassembly", ark_disasm_path
732                )
733                continue
734
735            gen_abc_dump_command = [
736                ark_disasm_path,
737                output_file,
738                output_dump,
739            ]
740
741            self.run_command(gen_abc_dump_command)
742
743        return output_files
744
745    def link_abc(
746        self,
747        target: Path,
748        input_files: Sequence[Path],
749    ) -> None:
750        """Link ABC files."""
751        if len(input_files) == 0:
752            self.logger.warning("No input files to link")
753            return
754
755        ark_link_path = self.config.panda_tool_dir / "bin/ark_link"
756
757        command = [
758            ark_link_path,
759            "--output",
760            target,
761            "--",
762            *input_files,
763        ]
764
765        self.run_command(command)
766
767    def run_abc(
768        self,
769        abc_target: Path,
770        ld_lib_path: Path,
771        entry: str,
772    ) -> float:
773        """Run the compiled ABC file with the Ark runtime."""
774        ark_path = self.config.panda_tool_dir / "bin/ark"
775
776        etsstdlib_path = self.config.panda_ets_dir / "etsstdlib.abc"
777
778        command = [
779            ark_path,
780            f"--boot-panda-files={etsstdlib_path}",
781            f"--load-runtimes=ets",
782            abc_target,
783            entry,
784        ]
785
786        return self.run_command(
787            command,
788            env={"LD_LIBRARY_PATH": ld_lib_path},
789        )
790
791
792class RepositoryUpgrader(BuildUtils):
793    """Upgrade the code from a specified URL."""
794
795    def __init__(
796        self,
797        repo_url: str,
798        config: BuildConfig,
799        verbosity: int = logging.INFO,
800    ):
801        super().__init__(verbosity)
802        self.config = config
803
804        self.repo_url = repo_url
805
806    def fetch_and_upgrade(self):
807        filename = self.repo_url.split("/")[-1]
808        version = self.repo_url.split("/")[-2]
809
810        extract_dir = self.config.locator.root_dir / "../tmp"
811        target_file = extract_dir / filename
812        self.create_directory(extract_dir)
813        self.download_file(target_file, self.repo_url)
814        self.extract_file(target_file, extract_dir)
815
816        tmp_taihe_pkg_dir = extract_dir / "taihe"
817        self.clean_directory(self.config.locator.root_dir)
818        self.move_directory(tmp_taihe_pkg_dir, self.config.locator.root_dir)
819        self.clean_directory(extract_dir)
820
821        self.logger.info("Successfully upgraded code to version %s", version)
822
823
824class TaiheTryitParser(argparse.ArgumentParser):
825    """Parser for the Taihe Tryit CLI."""
826
827    def register_common_configs(self) -> None:
828        self.add_argument(
829            "-v",
830            "--verbose",
831            action="count",
832            default=0,
833            help="Increase verbosity (can be used multiple times)",
834        )
835
836    def register_project_configs(self) -> None:
837        self.add_argument(
838            "target_directory",
839            type=str,
840            help="The target directory containing source files for the project",
841        )
842        self.add_argument(
843            "-u",
844            "--user",
845            type=UserType,
846            choices=list(UserType),
847            required=True,
848            help="User type for the build system (ani/cpp)",
849        )
850
851    def register_build_configs(self) -> None:
852        self.add_argument(
853            "-O",
854            "--optimization",
855            type=str,
856            nargs="?",
857            default="0",
858            const="0",
859            help="Optimization level for compilation (0-3)",
860        )
861
862    def register_generate_configs(self) -> None:
863        self.add_argument(
864            "--sts-keep-name",
865            action="store_true",
866            help="Keep original function and interface method names",
867        )
868        self.add_argument(
869            "--cmake",
870            action="store_true",
871            help="Generate CMake files for the project",
872        )
873
874    def register_update_configs(self) -> None:
875        self.add_argument(
876            "URL",
877            type=str,
878            help="The URL to fetch the code from",
879        )
880
881
882def main(config: BuildConfig | None = None):
883    parser = TaiheTryitParser(
884        prog="taihe-tryit",
885        description="Build and run project from a target directory",
886    )
887    parser.register_common_configs()
888
889    subparsers = parser.add_subparsers(dest="command", required=True)
890
891    parser_create = subparsers.add_parser(
892        "create",
893        help="Create a simple example",
894    )
895    parser_create.register_common_configs()
896    parser_create.register_project_configs()
897
898    parser_generate = subparsers.add_parser(
899        "generate",
900        help="Generate code from the target directory",
901    )
902    parser_generate.register_common_configs()
903    parser_generate.register_project_configs()
904    parser_generate.register_generate_configs()
905
906    parser_build = subparsers.add_parser(
907        "build",
908        help="Build the project from the target directory",
909    )
910    parser_build.register_common_configs()
911    parser_build.register_project_configs()
912    parser_build.register_build_configs()
913
914    parser_test = subparsers.add_parser(
915        "test",
916        help="Generate and build the project from the target directory",
917    )
918    parser_test.register_common_configs()
919    parser_test.register_project_configs()
920    parser_test.register_build_configs()
921    parser_test.register_generate_configs()
922
923    parser_upgrade = subparsers.add_parser(
924        "upgrade",
925        help="Upgrade using the specified URL",
926    )
927    parser_upgrade.register_common_configs()
928    parser_upgrade.register_update_configs()
929
930    args = parser.parse_args()
931
932    match args.verbose:
933        case 0:
934            verbosity = logging.INFO
935        case 1:
936            verbosity = logging.DEBUG
937        case 2:
938            verbosity = TRACE_CONCISE
939        case _:
940            verbosity = TRACE_VERBOSE
941
942    if config is None:
943        config = BuildConfig()
944
945    try:
946        if args.command == "upgrade":
947            upgrader = RepositoryUpgrader(
948                args.URL,
949                config=config,
950                verbosity=verbosity,
951            )
952            upgrader.fetch_and_upgrade()
953        else:
954            build_system = BuildSystem(
955                args.target_directory,
956                args.user,
957                config=config,
958                verbosity=verbosity,
959            )
960            if args.command == "create":
961                build_system.create()
962            if args.command in ("generate", "test"):
963                build_system.generate(
964                    sts_keep_name=args.sts_keep_name,
965                    cmake=args.cmake,
966                )
967            if args.command in ("build", "test"):
968                build_system.build(
969                    opt_level=args.optimization,
970                )
971    except KeyboardInterrupt:
972        print("\nInterrupted. Exiting build process.", file=sys.stderr)
973        sys.exit(1)
974    except Exception as e:
975        print(f"Error: {e}", file=sys.stderr)
976        sys.exit(1)
977
978
979if __name__ == "__main__":
980    main()