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# 17 18import logging 19import os 20from pathlib import Path 21import shutil 22import subprocess 23from typing import List, Mapping 24import binascii 25import glob 26import tarfile 27 28 29# 配置日志记录 30logging.basicConfig( 31 level=logging.INFO, 32 format='%(asctime)s - %(levelname)s - %(message)s', 33 handlers=[ 34 logging.StreamHandler() 35 ] 36) 37 38 39def run_command(cmd, cwd=None, env=None): 40 logger = logging.getLogger(__name__) 41 try: 42 logger.info(f"Command: {' '.join(cmd)}") 43 result = subprocess.run(cmd, cwd=cwd, env=env, capture_output=True, text=True, check=True) 44 if result.stdout: 45 logger.info(f"Command output: {result.stdout.strip()}") 46 if result.stderr: 47 logger.warning(f"Command error output: {result.stderr.strip()}") 48 except subprocess.CalledProcessError as e: 49 logger.error(f"Command failed: {' '.join(cmd)}. Error: {e.stderr.strip()}") 50 raise 51 52class BuildConfig: 53 def __init__(self, args): 54 self.REPOROOT_DIR = args.repo_root 55 self.OUT_PATH = args.out_path 56 self.LLDB_PY_VERSION = args.lldb_py_version 57 self.LLDB_PY_DETAILED_VERSION = args.lldb_py_detailed_version 58 self.MINGW_TRIPLE = args.mingw_triple 59 60 61class PythonBuilder: 62 target_platform = "" 63 patches = [] 64 65 66 def __init__(self, build_config) -> None: 67 self.build_config = build_config 68 self.repo_root = Path(build_config.REPOROOT_DIR).resolve() 69 self._out_dir = Path(build_config.OUT_PATH).resolve() 70 self._lldb_py_version = build_config.LLDB_PY_VERSION 71 self._version = build_config.LLDB_PY_DETAILED_VERSION 72 version_parts = self._version.split('.') 73 self._major_version = version_parts[0] 74 self._source_dir = self.repo_root / 'third_party' / 'python' 75 self._patch_dir = self._source_dir / '.cid' / 'patches' 76 self._prebuilts_path = os.path.join(self.repo_root, 'prebuilts') 77 self._prebuilts_python_path = os.path.join(self._prebuilts_path, 'python', 'linux-x86', self._lldb_py_version, 'bin', 78 f'python{self._major_version}') 79 self._install_dir = "" 80 self._clean_patches() 81 logging.getLogger(__name__).addHandler(logging.FileHandler(self._out_dir / 'build.log')) 82 83 @property 84 def _logger(self) -> logging.Logger: 85 return logging.getLogger(__name__) 86 87 @property 88 def _cc(self) -> Path: 89 return self._clang_toolchain_dir / 'bin' / 'clang' 90 91 @property 92 def _cflags(self) -> List[str]: 93 return [] 94 95 @property 96 def _ldflags(self) -> List[str]: 97 return [] 98 99 @property 100 def _cxx(self) -> Path: 101 return self._clang_toolchain_dir / 'bin' / 'clang++' 102 103 @property 104 def _strip(self) -> Path: 105 return self._clang_toolchain_dir / 'bin' / 'llvm-strip' 106 107 @property 108 def _cxxflags(self) -> List[str]: 109 return self._cflags.copy() 110 111 @property 112 def _rcflags(self) -> List[str]: 113 return [] 114 115 @property 116 def _env(self) -> Mapping[str, str]: 117 env = os.environ.copy() 118 clang_bin_dir = self._clang_toolchain_dir / 'bin' 119 120 env.update({ 121 'CC': str(self._cc), 122 'CXX': str(self._cxx), 123 'WINDRES': str(clang_bin_dir / 'llvm-windres'), 124 'AR': str(clang_bin_dir / 'llvm-ar'), 125 'READELF': str(clang_bin_dir / 'llvm-readelf'), 126 'LD': str(clang_bin_dir / 'ld.lld'), 127 'DLLTOOL': str(clang_bin_dir / 'llvm-dlltoo'), 128 'RANLIB': str(clang_bin_dir / 'llvm-ranlib'), 129 'STRIP': str(self._strip), 130 'CFLAGS': ' '.join(self._cflags), 131 'CXXFLAGS': ' '.join(self._cxxflags), 132 'LDFLAGS': ' '.join(self._ldflags), 133 'RCFLAGS': ' '.join(self._rcflags), 134 'CPPFLAGS': ' '.join(self._cflags), 135 'LIBS': '-lffi' 136 }) 137 return env 138 139 def _configure(self) -> None: 140 self._logger.info("Starting configuration...") 141 return 142 143 def _clean_patches(self) -> None: 144 self._logger.info("Cleaning patches...") 145 run_command(['git', 'reset', '--hard', 'HEAD'], cwd=self._source_dir) 146 run_command(['git', 'clean', '-df', '--exclude=.cid'], cwd=self._source_dir) 147 148 def _pre_build(self) -> None: 149 self._deps_build() 150 self._apply_patches() 151 152 def _apply_patches(self) -> None: 153 if hasattr(self, '_patch_ignore_file') and self._patch_ignore_file.is_file(): 154 self._logger.warning('Patches for Python have being applied, skip patching') 155 return 156 157 if not self._patch_dir.is_dir(): 158 self._logger.warning('Patches are not found, skip patching') 159 return 160 161 for patch in self._patch_dir.iterdir(): 162 if patch.is_file() and patch.name in self.patches: 163 cmd = ['git', 'apply', str(patch)] 164 self._logger.info(f"Applying patch: {patch.name}") 165 run_command(cmd, cwd=self._source_dir) 166 167 168 def _deps_build(self) -> None: 169 self._logger.info("Starting dependency build process...") 170 return 171 172 def build(self) -> None: 173 self._logger.info("Starting build process...") 174 self._pre_build() 175 if hasattr(self, '_build_dir') and self._build_dir.exists(): 176 self._logger.info(f"Removing existing build directory: {self._build_dir}") 177 shutil.rmtree(self._build_dir) 178 if isinstance(self._install_dir, Path) and self._install_dir.exists(): 179 self._logger.info(f"Removing existing install directory: {self._install_dir}") 180 shutil.rmtree(self._install_dir) 181 if hasattr(self, '_build_dir'): 182 self._build_dir.mkdir(parents=True) 183 if isinstance(self._install_dir, Path): 184 self._install_dir.mkdir(parents=True) 185 self._configure() 186 self._install() 187 188 def _install(self) -> None: 189 self._logger.info("Starting installation...") 190 num_jobs = os.cpu_count() or 8 191 cmd = ['make', f'-j{num_jobs}', 'install'] 192 run_command(cmd, cwd=self._build_dir) 193 194 def _strip_in_place(self, file: Path) -> None: 195 self._logger.info(f"Stripping file: {file}") 196 cmd = [ 197 str(self._strip), 198 str(file), 199 ] 200 run_command(cmd) 201 202 def _clean_bin_dir(self) -> None: 203 self._logger.info("Cleaning bin directory...") 204 python_bin_dir = self._install_dir / 'bin' 205 if not python_bin_dir.is_dir(): 206 return 207 208 windows_suffixes = ('.exe', '.dll') 209 for f in python_bin_dir.iterdir(): 210 if f.suffix not in windows_suffixes or f.is_symlink(): 211 self._logger.info(f"Removing file: {f}") 212 f.unlink() 213 continue 214 self._strip_in_place(f) 215 216 def _remove_dir(self, dir_path: Path) -> None: 217 if dir_path.is_dir(): 218 self._logger.info(f"Removing directory: {dir_path}") 219 shutil.rmtree(dir_path) 220 221 def _clean_share_dir(self) -> None: 222 self._logger.info("Cleaning share directory...") 223 share_dir = self._install_dir / 'share' 224 self._remove_dir(share_dir) 225 226 def _clean_lib_dir(self) -> None: 227 self._logger.info("Cleaning lib directory...") 228 python_lib_dir = self._install_dir / 'lib' 229 pkgconfig_dir = python_lib_dir / 'pkgconfig' 230 self._remove_dir(pkgconfig_dir) 231 232 def _remove_exclude(self) -> None: 233 self._logger.info("Removing excluded files and directories...") 234 exclude_dirs_tuple = ( 235 f'config-{self._major_version}', 236 '__pycache__', 237 'idlelib', 238 'tkinter', 'turtledemo', 239 'test', 'tests' 240 ) 241 exclude_files_tuple = ( 242 'bdist_wininst.py', 243 'turtle.py', 244 '.whl', 245 '.pyc', '.pickle' 246 ) 247 248 for root, dirs, files in os.walk(self._install_dir / 'lib'): 249 for item in dirs: 250 if item.startswith(exclude_dirs_tuple): 251 self._logger.info(f"Removing directory: {os.path.join(root, item)}") 252 shutil.rmtree(os.path.join(root, item)) 253 for item in files: 254 if item.endswith(exclude_files_tuple): 255 self._logger.info(f"Removing file: {os.path.join(root, item)}") 256 os.remove(os.path.join(root, item)) 257 258 def _copy_external_libs(self) -> None: 259 self._logger.info("Copying external libraries...") 260 # 定义源文件路径 261 _external_libs = [self._deps_dir / 'ffi' / 'bin' / 'libffi-8.dll', self._clang_toolchain_dir / self.build_config.MINGW_TRIPLE / 'bin' / 'libssp-0.dll'] 262 # 定义目标目录 263 target_dir = self._install_dir / 'lib' / 'python3.11' / 'lib-dynload' 264 # 创建目标目录(如果不存在) 265 target_dir.mkdir(parents=True, exist_ok=True) 266 267 try: 268 for lib in _external_libs : 269 # 调用提取的方法拷贝 libffi-8.dll 270 self._copy_file_if_exists(lib, target_dir) 271 except Exception as e: 272 self._logger.error(f"Error copying external libraries: {e}") 273 274 def _copy_file_if_exists(self, src_path: Path, dest_dir: Path) -> None: 275 """ 276 若源文件存在,则将其拷贝到目标目录,并记录相应日志;若不存在,则记录警告日志。 277 278 :param src_path: 源文件的路径 279 :param dest_dir: 目标目录的路径 280 """ 281 if src_path.exists(): 282 shutil.copy2(src_path, dest_dir) 283 self._logger.info(f"Copied {src_path} to {dest_dir}") 284 else: 285 self._logger.warning(f"{src_path} does not exist. Skipping.") 286 287 def _is_elf_file(self, file_path: Path) -> bool: 288 with open(file_path, 'rb') as f: 289 magic_numbers = f.read(4) 290 hex_magic_number = binascii.hexlify(magic_numbers).decode('utf-8') 291 return hex_magic_number == '7f454c46' 292 293 @property 294 def install_dir(self) -> str: 295 return str(self._install_dir) 296 297 298class MinGWPythonBuilder(PythonBuilder): 299 def __init__(self, build_config) -> None: 300 super().__init__(build_config) 301 302 self.target_platform = "x86_64-w64-mingw32" 303 self.patches = [f'cpython_mingw_v{self._version}.patch'] 304 self._clang_toolchain_dir = Path( 305 os.path.join(self._prebuilts_path, 'mingw-w64', 'ohos', 'linux-x86_64', 'clang-mingw')).resolve() 306 self._mingw_install_dir = self._clang_toolchain_dir / build_config.MINGW_TRIPLE 307 self._build_dir = self._out_dir / 'python-windows-build' 308 self._install_dir = self._out_dir / 'python-windows-install' 309 self._deps_dir = self._out_dir / 'python-windows-deps' 310 # This file is used to detect whether patches are applied 311 self._patch_ignore_file = self._source_dir / 'mingw_ignorefile.txt' 312 313 for directory in (self._mingw_install_dir, self._source_dir): 314 if not directory.is_dir(): 315 raise ValueError(f'No such directory "{directory}"') 316 317 def _extract_libffi(self): 318 """ 319 定位 libffi-*.tar.gz 文件,清理输出目录后,将其直接解压到 out/libffi 目录。 320 321 Returns: 322 Path: 解压后的 libffi 内部目录的路径。 323 324 Raises: 325 FileNotFoundError: 若未找到 libffi-*.tar.gz 文件。 326 Exception: 若无法获取 libffi 压缩包的内部目录。 327 """ 328 # 找到 libffi-*.tar.gz 包 329 libffi_tar_gz_files = glob.glob(str(self.repo_root / 'third_party' / 'libffi' / 'libffi-*.tar.gz')) 330 if not libffi_tar_gz_files: 331 self._logger.error("No libffi-*.tar.gz file found in third_party/libffi directory.") 332 raise FileNotFoundError("No libffi-*.tar.gz file found.") 333 libffi_tar_gz = libffi_tar_gz_files[0] 334 335 # 清理 out/libffi 目录 336 libffi_extract_dir = self._out_dir / 'libffi' 337 if libffi_extract_dir.exists(): 338 self._logger.info(f"Cleaning existing libffi directory: {libffi_extract_dir}") 339 shutil.rmtree(libffi_extract_dir) 340 libffi_extract_dir.mkdir(parents=True) 341 342 # 直接解压 libffi-*.tar.gz 到 out/libffi 目录 343 with tarfile.open(libffi_tar_gz, 'r:gz') as tar: 344 tar.extractall(path=libffi_extract_dir) 345 # 获取解压后的目录名 346 members = tar.getmembers() 347 if members: 348 libffi_inner_dir = libffi_extract_dir / members[0].name 349 else: 350 self._logger.error("Failed to get inner directory of libffi tarball.") 351 raise Exception("Failed to get inner directory of libffi tarball.") 352 return libffi_inner_dir 353 354 @property 355 def _cflags(self) -> List[str]: 356 cflags = [ 357 f'-target {self.target_platform}', 358 f'--sysroot={self._mingw_install_dir}', 359 f'-fstack-protector-strong', 360 f'-I{str(self._deps_dir / "ffi" / "include")}', 361 f'-nostdinc', 362 f'-I{str(self._mingw_install_dir / "include")}', 363 f'-I{str(self._clang_toolchain_dir / "lib" / "clang" / "15.0.4" / "include")}' 364 ] 365 return cflags 366 367 @property 368 def _ldflags(self) -> List[str]: 369 ldflags = [ 370 f'--sysroot={self._mingw_install_dir}', 371 f'-rtlib=compiler-rt', 372 f'-target {self.target_platform}', 373 f'-lucrt', 374 f'-lucrtbase', 375 f'-fuse-ld=lld', 376 f'-L{str(self._deps_dir / "ffi" / "lib")}', 377 ] 378 return ldflags 379 380 @property 381 def _rcflags(self) -> List[str]: 382 return [f'-I{self._mingw_install_dir}/include'] 383 384 def _deps_build(self) -> None: 385 self._logger.info("Starting MinGW dependency build process...") 386 # 调用提取的方法 387 libffi_inner_dir = self._extract_libffi() 388 389 env = os.environ.copy() 390 env.update({ 391 'CC': "/bin/x86_64-w64-mingw32-gcc", 392 'CXX': "/bin/x86_64-w64-mingw32-g++", 393 'WINDRES': "/bin/x86_64-w64-mingw32-windres", 394 'AR': "/bin/x86_64-w64-mingw32-ar", 395 'READELF': "/bin/x86_64-w64-mingw32-readelf", 396 'LD': "/bin/x86_64-w64-mingw32-ld", 397 'DLLTOOL': "/bin/x86_64-w64-mingw32-dlltool", 398 'RANLIB': "/bin/x86_64-w64-mingw32-gcc-ranlib", 399 'STRIP': "/bin/x86_64-w64-mingw32-strip", 400 'CFLAGS': "--sysroot=/usr/x86_64-w64-mingw32 -fstack-protector-strong", 401 'CXXFLAGS': "--sysroot=/usr/x86_64-w64-mingw32 -fstack-protector-strong", 402 'LDFLAGS': "--sysroot=/usr/x86_64-w64-mingw32", 403 'RCFLAGS': "-I/usr/x86_64-w64-mingw32/include", 404 'CPPFLAGS': "--sysroot=/usr/x86_64-w64-mingw32 -fstack-protector-strong" 405 }) 406 407 configure_cmd = [ 408 "./configure", 409 f"--prefix={self._deps_dir / 'ffi'}", 410 "--enable-shared", 411 "--build=x86_64-pc-linux-gnu", 412 "--host=x86_64-w64-mingw32", 413 "--disable-symvers", 414 "--disable-docs" 415 ] 416 run_command(configure_cmd, env=env, cwd=libffi_inner_dir) 417 418 # 执行 make -j16 419 make_cmd = ['make', '-j16'] 420 run_command(make_cmd, env=env, cwd=libffi_inner_dir) 421 422 # 执行 make install 423 make_install_cmd = ['make', 'install'] 424 run_command(make_install_cmd, env=env, cwd=libffi_inner_dir) 425 426 def _configure(self) -> None: 427 self._logger.info("Starting MinGW configuration...") 428 run_command(['autoreconf', '-vfi'], cwd=self._source_dir) 429 build_platform = subprocess.check_output( 430 ['./config.guess'], cwd=self._source_dir).decode().strip() 431 config_flags = [ 432 f'--prefix={self._install_dir}', 433 f'--build={build_platform}', 434 f'--host={self.target_platform}', 435 f'--with-build-python={self._prebuilts_python_path}', 436 '--enable-shared', 437 '--without-ensurepip', 438 '--enable-loadable-sqlite-extensions', 439 '--disable-ipv6', 440 '--with-pydebug', 441 '--with-system-ffi' 442 ] 443 cmd = [str(self._source_dir / 'configure')] + config_flags 444 run_command(cmd, env=self._env, cwd=self._build_dir) 445 446 def prepare_for_package(self) -> None: 447 self._logger.info("Preparing MinGW build for packaging...") 448 self._clean_bin_dir() 449 self._clean_share_dir() 450 self._clean_lib_dir() 451 self._remove_exclude() 452 self._copy_external_libs() 453 454 def package(self) -> None: 455 self._logger.info("Packaging MinGW build...") 456 archive = self._out_dir / f'python-mingw-x86-{self._version}.tar.gz' 457 if archive.exists(): 458 self._logger.info(f"Removing existing archive: {archive}") 459 archive.unlink() 460 cmd = [ 461 'tar', 462 '-czf', 463 str(archive), 464 '--exclude=__pycache__', 465 '--transform', 466 f's,^,python/windows-x86/{self._lldb_py_version}/,', 467 ] + [f.name for f in self._install_dir.iterdir()] 468 run_command(cmd, cwd=self._install_dir) 469