Files
enginex-ascend-910-vllm/csrc/scripts/package/common/py/pkg_parser.py
Sun Ruoxi 7f8a1b1f7a init v0.23.0
Signed-off-by: Sun Ruoxi <sunruoxi@4paradigm.com>
2026-08-27 15:11:51 +08:00

1059 lines
36 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
# -----------------------------------------------------------------------------------------------------------
# Copyright (c) 2025 Huawei Technologies Co., Ltd.
# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
# CANN Open Software License Agreement Version 2.0 (the "License").
# Please refer to the License for details. You may not use this file except in compliance with the License.
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
# See LICENSE in the root of the software repository for the full text of the License.
# -----------------------------------------------------------------------------------------------------------
import copy
import glob
import hashlib
import itertools
import os
import sys
import xml.etree.ElementTree as ET
from argparse import Namespace
from collections.abc import Callable, Iterable, Iterator
from functools import partial
from io import StringIO
from itertools import chain
from operator import attrgetter, itemgetter, methodcaller
from typing import Any, NamedTuple
import regex as re
from .filelist import FileItem, FileList, fill_is_common_path
from .utils import pkg_utils
from .utils.comm_log import CommLog
from .utils.funcbase import constant, dispatch, invoke, pipe, star_apply
from .utils.pkg_utils import (
BLOCK_CONFIG_PATH,
FAIL,
BlockConfigError,
ContainAsteriskError,
EnvNotSupported,
IllegalVersionDir,
PackageError,
ParseOsArchError,
config_feature_to_set,
flatten,
merge_dict,
star_pipe,
yield_if,
)
from .version_info import VersionFormatNotMatch, VersionInfo, VersionXml, is_multi_version
# 环境变量字典
EnvDict = dict[str, str]
# 文件信息
FileInfo = dict[str, str]
# 包属性
PackageAttr = dict[str, str | bool]
# 生成信息
GenerateInfo = dict[str, str]
class ParseOption(NamedTuple):
"""解析参数。"""
os_arch: str | None
pkg_version: str | None
build_type: str | None
package_check: bool
ext_name: str = ""
def parse_os_arch(os_arch: str) -> tuple[str, str, str]:
"""解析系统和架构。"""
match = re.match("^([a-z]+)(\\d+(\\.\\d+)*)?[.-]?(\\S*)", os_arch)
if match:
os_name = match.group(1)
os_ver = match.group(2)
if match.group(4):
arch = match.group(4)
else:
# 如果os_arch中没有配置ARCHARCH默认值为aarch64
arch = "aarch64"
return os_name, os_ver, arch
raise ParseOsArchError()
def replace_env(env_dict: EnvDict, in_str: str):
"""替换环境变量为实际值。"""
env_list = re.findall(".*?\\$\\((.*?)\\).*?", in_str)
for env in env_list:
if env == "FILE":
continue
if env in env_dict:
if env_dict[env] is not None:
in_str = in_str.replace(f"$({env})", env_dict[env])
else:
in_str = in_str.replace(f"$({env})", "")
else:
raise EnvNotSupported(f"Error: {env} not supported.")
return in_str
class ParseEnv(NamedTuple):
"""解析上下文环境。"""
env_dict: EnvDict
parse_option: ParseOption
delivery_dir: str
top_dir: str
class BlockElement(NamedTuple):
"""块配置。"""
name: str
block_conf_path: str
dst_path: str
chips: set[str]
features: set[str]
attrs: dict[str, str]
# BlockElement直接透传给LoadedBlockElement的参数列表
BLOCK_ELEMENT_PASS_THROUGH_ARGS = ["dst_path", "chips", "features", "attrs"]
class LoadedBlockElement(NamedTuple):
"""加载后的块配置。"""
root_ele: ET.Element
use_move: bool
dst_path: str
chips: set[str]
features: set[str]
attrs: dict[str, str]
class FileInfoParsedResult(NamedTuple):
"""file_info元素解析结果。"""
file_info: FileInfo
move_infos: list[FileInfo]
dir_infos: list[dict[str, str]]
expand_infos: list[dict[str, str]]
class BlockConfig(NamedTuple):
"""块配置。"""
dir_install_list: list[dict]
move_files: list[FileInfo]
expand_content_list: list[dict]
package_content_list: list[dict]
generate_infos: list[GenerateInfo]
class PackerConfig(NamedTuple):
"""安装相关配置。"""
fill_is_common_path: Callable[[FileList], Iterator[FileItem]]
class XmlConfig(NamedTuple):
"""安装xml配置。"""
default_config: dict[str, str]
package_attr: PackageAttr
version_info: VersionInfo
blocks: list[BlockConfig]
version: str
version_xml: VersionXml | None
packer_config: PackerConfig
def _collect_list(self, list_name):
result = []
for block in self.blocks:
result.extend(getattr(block, list_name))
return result
@property
def dir_install_list(self):
return self._collect_list("dir_install_list")
@property
def move_content_list(self):
return self._collect_list("move_files")
@property
def expand_content_list(self):
return self._collect_list("expand_content_list")
@property
def package_content_list(self):
return self._collect_list("package_content_list")
@property
def generate_infos(self) -> list[GenerateInfo]:
return self._collect_list("generate_infos")
# 默认包属性
DEFAULT_PACKAGE_ATTR = {
"gen_version_info": True,
}
def parse_package_info(package_info_ele: ET.Element | None) -> dict:
"""解析package_info元素。"""
def get_package_info_attrs(ele: ET.Element) -> Iterator[tuple[str, str | bool]]:
# expand_asterisk: 展开配置中星号
# parallel: 并行复制文件
# parallel_limit: 限制并发数
# package_check: 检查filelist.csv中配置目录是否完整
# check_features: 检查filelist.csv中所有feature是否符合package_check
# gen_version_info: 是否生成version.info文件
bool_attrs = (
"expand_asterisk",
"parallel",
"parallel_limit",
"package_check",
"check_features",
"use_move",
"gen_version_info",
)
bool_values = ("t", "true", "y", "yes")
if ele.tag in bool_attrs:
if ele.text.lower() in bool_values:
yield ele.tag, True
else:
yield ele.tag, False
else:
yield ele.tag, ele.text
if not package_info_ele:
return {}
attr = dict(chain.from_iterable(map(get_package_info_attrs, list(package_info_ele))))
return attr
def parse_package_attr_by_args(args: Namespace) -> dict:
"""通过命令行参数解析"""
def pairs():
if hasattr(args, "chip_name") and args.chip_name:
yield "chip_name", args.chip_name
if hasattr(args, "suffix") and args.suffix:
yield "suffix", args.suffix
if hasattr(args, "func_name") and args.func_name:
yield "func_name", args.func_name
return dict(pairs())
def parse_package_attr(root_ele: ET.Element, args: Namespace) -> dict:
"""通过根元素解析package_info元素。"""
package_info_ele = root_ele.find("package_info")
return merge_dict(
DEFAULT_PACKAGE_ATTR,
parse_package_info(package_info_ele),
parse_package_attr_by_args(args),
)
def render_cann_version(
a_ver: int, b_ver: int, c_ver: int | None, d_ver: int | None, e_ver: int | None, f_ver: int | None
) -> str:
"""渲染CANN版本号。"""
buffer = StringIO()
buffer.write("(")
buffer.write(f"({a_ver + 1} * 100000000) + ({b_ver + 1} * 1000000)")
if c_ver is not None:
buffer.write(f" + ({c_ver + 1} * 10000)")
if d_ver is not None:
buffer.write(f" + (({d_ver + 1} * 100) + 5000)")
if e_ver is not None:
buffer.write(f" + ({e_ver + 1} * 100)")
if f_ver is not None:
buffer.write(f" + {f_ver}")
buffer.write(")")
return buffer.getvalue()
def render_semver(package_name: str, version: str) -> Iterator[tuple[str, str]]:
"""
将语义化版本号转换为可比较的整数表达式严格遵循SemVer规范
排序规则:
1. 正式版本 > 所有对应预发布版本(如 8.0.5 > 8.0.5-rc.1
2. 预发布类型优先级rc > beta > alpha > 其他类型(如 rc.1 > beta.100
3. 同类型预发布版本:序号越大优先级越高(如 alpha.10 > alpha.2
4. 多段序号比较:从左到右逐段比较(如 alpha.1.2 > alpha.1.1
"""
expr_buffer = StringIO()
expr_buffer.write("(")
# 移除构建元数据(+后面的内容不影响版本优先级)
version = version.split("+")[0]
# 分离正式版本和预发布版本
pre_release = None
if "-" in version:
release_part, pre_release = version.split("-", 1)
release_part = release_part.split(".")
else:
release_part = version.split(".")
if len(release_part) > 3:
pre_release = ".".join(release_part[3:])
release_part = release_part[:3]
# 解析正式版本号(主版本.次版本.修订号)
try:
major, minor, patch = map(int, release_part)
except (ValueError, TypeError) as ex:
raise IllegalVersionDir(f"无效的版本号格式: {version}") from ex
yield f"{package_name}_VERSION_STR", f'"{version}"'
yield f"{package_name}_MAJOR", str(major)
yield f"{package_name}_MINOR", str(minor)
yield f"{package_name}_PATCH", str(patch)
# 计算基础版本值(主版本*10^7 + 次版本*10^5 + 修订号*10^3
# 预留10^3空间用于预发布版本确保不同正式版本区间不重叠
expr_buffer.write(f"({major} * 10000000) + ({minor} * 100000) + ({patch} * 1000)")
# 处理正式版本(无预发布部分)
if not pre_release:
expr_buffer.write(")")
yield f"{package_name}_PRERELEASE", '""'
yield f"{package_name}_VERSION_NUM", expr_buffer.getvalue()
return
yield f"{package_name}_PRERELEASE", f'"{pre_release}"'
# 预发布类型权重(值越小优先级越高)
type_weights = {
"rc": 100, # rc优先级最高
"beta": 200, # beta次之
"alpha": 300, # alpha最低
}
def calc_pre_release() -> tuple[int, int]:
"""计算预发布版本。"""
if "." in pre_release:
pre_parts = pre_release.split(".")
pre_type = pre_parts[0] # 提取预发布类型rc/beta/alpha等
# 提取序号部分(支持多段序号,非数字部分忽略)
pre_nums = []
for part in pre_parts[1:]:
if part.isdigit():
pre_nums.append(int(part))
if not pre_nums: # 无序号时默认0
pre_nums = [0]
# 未知类型权重设为400优先级低于alpha
pre_type_weight = type_weights.get(pre_type, 400)
# 计算序号值(支持多段和多位数)
num_str = "".join(map(str, pre_nums))
# 转换为整数并返回
num_value = int(num_str)
return pre_type_weight, num_value
for pre_type in type_weights:
if pre_release.startswith(pre_type):
pre_type_weight = type_weights[pre_type]
num_value = int(pre_release[len(pre_type) :])
return pre_type_weight, num_value
return None, None
try:
pre_type_weight, num_value = calc_pre_release()
except (ValueError, TypeError) as ex:
raise IllegalVersionDir(f"无效的预发布版本: {pre_release}") from ex
if not pre_type_weight:
raise IllegalVersionDir(f"无效的预发布版本: {pre_release}")
# 预发布版本最终值 = 基础值 - 类型权重 + 序号值
# 确保:预发布值 < 基础值(正式版本)
expr_buffer.write(f" - {pre_type_weight} + {num_value}")
expr_buffer.write(")")
yield f"{package_name}_VERSION_NUM", expr_buffer.getvalue()
def get_cann_version_info(name: str, version: str) -> Iterator[tuple[str, str]]:
"""获取CANN版本号信息。"""
# 删除字符串中的_VERSION
package_name = name[:-8]
if not version:
yield f"{package_name}_VERSION_STR", '"0"'
return
yield from render_semver(package_name, version)
def get_default_env_items() -> Iterator[tuple[str, str]]:
"""获取默认环境字典条目。"""
yield "VERSION_DIR", ""
yield "HOME", os.environ.get("HOME")
def get_env_items_by_version(version: str | None) -> Iterator[tuple[str, str]]:
"""根据version获取环境字典条目。"""
if version:
yield "ASCEND_VER", version
version_parts = version.split(".")
for idx in range(1, len(version_parts) + 1):
yield f"CUR_VER[{idx}]", ".".join(version_parts[:idx])
yield "CUR_VER", version
yield "LOWER_CUR_VER", version.lower()
def get_env_items_by_version_dir(version_dir: str | None) -> Iterator[tuple[str, str]]:
"""根据version_dir获取环境字典条目。"""
if version_dir:
yield "VERSION_DIR", version_dir
def get_os_arch_default_env_items() -> Iterator[tuple[str, str]]:
"""获取系统相关默认环境字典条目。"""
yield "OS_NAME", "linux"
yield "OS_VER", ""
yield "ARM", "aarch64"
yield "TARGET_ENV", "$(TARGET_ENV)"
def get_env_items_by_os_arch(os_arch: str) -> Iterator[tuple[str, str]]:
"""根据os_arch获取环境字典条目。"""
if os_arch:
os_name, os_ver, arch = parse_os_arch(os_arch)
yield "OS_NAME", os_name
yield "OS_VER", os_ver
yield "ARCH", arch
yield "OS_ARCH", os_arch
if arch in ("arm", "sw_64"):
yield "ARM", arch
else:
yield "ARM", "aarch64"
yield "TARGET_ENV", f"{arch}-linux"
else:
yield from get_os_arch_default_env_items()
def get_env_items_by_timestamp(timestamp: str | None) -> Iterator[tuple[str, str]]:
"""根据timestamp获取环境字典条目。"""
if timestamp:
yield "TIMESTAMP", timestamp
yield "TIMESTAMP_NO", timestamp.replace("_", "")
else:
yield "TIMESTAMP", "0"
yield "TIMESTAMP_NO", "0"
def parse_env_dict(
os_arch: str, package_attr: PackageAttr, version: str | None, version_dir: str | None, timestamp: str | None
) -> EnvDict:
"""解析环境变量字典。"""
env_dict = dict(
chain(
get_default_env_items(),
yield_if(("ARCH", package_attr.get("default_arch")), itemgetter(1)),
get_env_items_by_os_arch(os_arch),
get_env_items_by_version(version),
get_env_items_by_version_dir(version_dir),
yield_if(("VERSION_DIR", version_dir), constant(version_dir)),
get_env_items_by_timestamp(timestamp),
)
)
return env_dict
def get_timestamp(args: Namespace) -> str | None:
"""获取触发时间戳。"""
if "tag" not in args:
return None
tag = args.tag
if tag:
timestamp_re = r"\d{8}_\d{9}"
timestamp_list = re.findall(timestamp_re, tag)
if not timestamp_list:
raise PackageError("The {} format is incorrect.".format(tag))
timestamp = timestamp_list[-1]
else:
timestamp = None
return timestamp
def extract_element_attrib(ele: ET.Element) -> dict:
"""提取元素属性。"""
return ele.attrib.copy()
def extract_generate_info_content(generate_info_ele: ET.Element, env_dict: EnvDict) -> dict:
"""提取生成信息内容。"""
file_content = {sub_item.tag: replace_env(env_dict, sub_item.text) for sub_item in list(generate_info_ele)}
return {"content": file_content}
def parse_generate_infos_by_loaded_block(
loaded_block: LoadedBlockElement, default_config: dict[str, str], env_dict: EnvDict
) -> list[dict]:
"""根据根元素解析生成信息列表。"""
return invoke(
pipe(
partial(
map,
pipe(
dispatch(
pipe(
extract_element_attrib,
partial(merge_dict, default_config),
partial(evaluate_info, loaded_block=loaded_block, env_dict=env_dict),
),
partial(extract_generate_info_content, env_dict=env_dict),
),
star_apply(merge_dict),
),
),
list,
),
loaded_block.root_ele.findall("generate_info"),
)
def join_pkg_inner_softlink(link_str_list: list[str]) -> str:
"""合并pkg_inner_softlink"""
path = "/".join(link_str_list)
return os.path.normpath(path)
def check_contain_asterisk(value: str) -> bool:
"""检查串是否包含星号。"""
return "*" in value
def check_value(value: str, package_check: bool, package_attr: PackageAttr):
"""检查元素value属性。"""
if package_check and package_attr.get("suffix") == "run":
if check_contain_asterisk(value):
raise ContainAsteriskError(value)
def get_dst_prefix(file_info: FileInfo, env: ParseEnv) -> str:
"""获取文件的前缀。"""
return os.path.join(env.delivery_dir, file_info["dst_path"])
def get_dst_target(file_info: FileInfo, env: ParseEnv) -> str:
"""获取文件的实际路径。"""
dst_prefix = get_dst_prefix(file_info, env)
return os.path.join(dst_prefix, os.path.basename(file_info.get("value")))
def make_hash(filepath: str) -> str:
"""计算文件的hash(sha256)值。"""
sha256_hash = hashlib.sha256()
with open(filepath, "rb") as file:
sha256_hash.update(file.read())
return sha256_hash.hexdigest()
def config_hash(parsed_result: FileInfoParsedResult, env: ParseEnv):
"""配置hash值。"""
file_info = parsed_result.file_info
# 如果配置了configurable需要计算文件的hash值
if file_info and file_info["configurable"] == "TRUE":
src_target = get_dst_target(file_info, env)
hash_value = make_hash(src_target)
file_info["hash"] = hash_value
return parsed_result
def apply_func(func: Callable[[str], str], value: list[str] | set[str] | str) -> list[str] | set[str] | str:
"""对一个字符串,或字符串序列,应用函数。"""
# 如pkg_softlink列表
if isinstance(value, list):
return list(map(func, value))
# 如feature集合
if isinstance(value, set):
return set(map(func, value))
return func(value)
REAL_PREFIX = "real:"
def join_dst_path(base: str, other: str) -> str:
"""联结dst_path。"""
if other.startswith("real:"):
other = other[len(REAL_PREFIX) :]
return other
return os.path.join(base, other)
def evaluate_info(info: dict[str, str], loaded_block: LoadedBlockElement, env_dict: EnvDict) -> dict[str, str]:
"""info元素求值。"""
dst_keys = ("dst_path",)
replace_env_func = partial(replace_env, env_dict)
add_dst_path_func = partial(join_dst_path, loaded_block.dst_path)
def upper_value(key: str, value: str) -> tuple[str, str]:
if key == "configurable":
return key, value.upper()
return key, value
def add_dst_path(key: str, value: str) -> tuple[str, str]:
if key in dst_keys:
return key, apply_func(add_dst_path_func, value)
return key, value
def replace_pkg_inner_softlink(key: str, value: str) -> tuple[str, str]:
if key == "pkg_inner_softlink":
return key, "NA"
return key, value
def merge_feature(key: str, value: str) -> tuple[str, str]:
if key in ("chip", "feature"):
config_features = config_feature_to_set(value, key)
return key, config_features | getattr(loaded_block, f"{key}s")
return key, value
def eval_value(_key: str, value: str) -> str:
if value is not None:
return apply_func(replace_env_func, value)
eval_value_func = star_pipe(
upper_value,
add_dst_path,
replace_pkg_inner_softlink,
merge_feature,
eval_value,
)
return {
key: eval_value_func(key, value)
for key, value in itertools.chain(
# 默认值配置
[
("dst_path", ""),
("configurable", "FALSE"),
("chip", None),
("feature", None),
("pkg_feature", None),
],
info.items(),
)
}
def parse_dir_info_elements(
loaded_block: LoadedBlockElement, default_config: dict[str, str], package_attr: PackageAttr, env: ParseEnv
) -> list[dict[str, str]]:
"""解析dir_info元素。"""
dir_info_elements: list[ET.Element] = loaded_block.root_ele.findall("dir_info")
dir_infos = []
for item in dir_info_elements:
dir_config = default_config.copy()
dir_config.update(item.attrib)
dir_config["module"] = dir_config.get("value")
for sub_item in list(item):
dir_info = dir_config.copy()
dir_info.update(sub_item.attrib)
dir_info = evaluate_info(dir_info, loaded_block, env.env_dict)
check_value(dir_info["value"], env.parse_option.package_check, package_attr)
dir_infos.append(dir_info)
return dir_infos
def expand_dir(file_info: FileInfo, get_dst_target_func: Callable[[FileInfo], str]):
"""
如果file_info中配置的路径是文件夹需要展开到文件
"""
file_info_list = []
dir_info_list = []
dst_target = get_dst_target_func(file_info)
value_list = file_info.get("value").split("/")
target_name = value_list[-1] if value_list[-1] else value_list[-2]
# 这里把当前目录也加入到dir_info_list中
dir_info_copy = file_info.copy()
dir_info_copy["module"] = file_info.get("value")
dir_info_copy["value"] = os.path.join(file_info.get("install_path", ""), target_name)
# 子目录的权限按照xml中subdir_mod配置如果没有配置subdir_mod按照install_mod配置
subdir_mod = file_info.get("subdir_mod", None)
if subdir_mod is not None:
dir_info_copy["install_mod"] = subdir_mod
# 被展开的当前目录不需要设置softlink
dir_info_copy["install_softlink"] = "NA"
dir_info_copy["pkg_inner_softlink"] = "NA"
dir_info_list.append(dir_info_copy)
for root, dirs, files in os.walk(dst_target, followlinks=True):
# 不同操作系统上os.walk遍历的结果顺序会略有不同这里按字母排序保证不同系统一致
dirs.sort()
files.sort()
dirs_to_remove = []
for name in dirs:
dirname = os.path.join(root, name)
# 如果是指向目录的软连接,则按照文件处理,无需在安装时创建目录,只需要卸载时删除就行
if os.path.islink(dirname) and not need_dereference(file_info):
copy_file_info = create_file_info(dirname, dst_target, file_info, name, target_name)
# 被展开的子文件不需要设置softlink
copy_file_info["install_softlink"] = "NA"
copy_file_info["pkg_inner_softlink"] = "NA"
file_info_list.append(copy_file_info)
dirs_to_remove.append(name)
continue
relative_dirname = os.path.relpath(dirname, dst_target)
dir_info_copy = file_info.copy()
dir_info_copy["module"] = file_info.get("value")
dir_info_copy["value"] = os.path.join(file_info.get("install_path", ""), target_name, relative_dirname)
# 被展开的子目录不需要设置softlink
dir_info_copy["install_softlink"] = "NA"
dir_info_copy["pkg_inner_softlink"] = "NA"
# 子目录的权限按照xml中subdir_mod配置如果没有配置subdir_mod按照install_mod配置
subdir_mod = file_info.get("subdir_mod", None)
if subdir_mod is not None:
dir_info_copy["install_mod"] = subdir_mod
dir_info_list.append(dir_info_copy)
for name in files:
filename = os.path.join(root, name)
copy_file_info = create_file_info(filename, dst_target, file_info, name, target_name)
file_info_list.append(copy_file_info)
for name in dirs_to_remove:
dirs.remove(name)
return file_info_list, dir_info_list
def create_file_info(dirname, dst_target, file_info, name, target_name):
relative_filename = os.path.relpath(dirname, dst_target)
relative_dir_name = os.path.split(relative_filename)[0]
copy_file_info = file_info.copy()
copy_file_info["value"] = name
copy_file_info["src_path"] = os.path.join(file_info["src_path"], file_info["value"], relative_dir_name)
copy_file_info["dst_path"] = os.path.join(file_info["dst_path"], target_name, relative_dir_name)
copy_file_info["install_path"] = os.path.join(file_info.get("install_path", ""), target_name, relative_dir_name)
return copy_file_info
def expand_file_info_asterisk(parsed_result: FileInfoParsedResult, env: ParseEnv) -> Iterator[FileInfoParsedResult]:
"""展开FileInfoParsedResult中的星号。"""
file_info = parsed_result.file_info
if check_contain_asterisk(file_info.get("value", "")):
dst_prefix = get_dst_prefix(file_info, env)
dst_targets = sorted(glob.glob(get_dst_target(file_info, env)))
if "exclude" in file_info:
exclude = list(map(methodcaller("strip"), file_info["exclude"].split(";")))
else:
exclude = []
for dst_target in dst_targets:
value = os.path.relpath(dst_target, dst_prefix)
if value in exclude:
continue
new_file_info = file_info.copy()
new_file_info["value"] = value
if "pkg_inner_softlink" in new_file_info:
# pkg_inner_softlink中的特殊变量$(FILE)替换为展开后的文件名
pkg_inner_softlink = new_file_info["pkg_inner_softlink"]
new_file_info["pkg_inner_softlink"] = pkg_inner_softlink.replace(
"$(FILE)", os.path.basename(dst_target)
)
yield parsed_result._replace(file_info=new_file_info)
else:
yield parsed_result
def trans_to_stream(item: Any) -> Iterator[Any]:
"""转换为流。"""
yield item
def need_dereference(file_info: FileInfo) -> bool:
"""是否需要解引用。"""
return "dereference" in file_info
def need_expand(file_info: FileInfo, get_dst_target_func: Callable[[FileInfo], str]) -> bool:
"""是否需要展开子目录。"""
if file_info.get("entity") == "true":
return False
dst_target = get_dst_target_func(file_info)
if os.path.isdir(dst_target):
if need_dereference(file_info):
return True
return not os.path.islink(dst_target)
return False
def expand_file_info(
parsed_result: FileInfoParsedResult, use_move: bool, get_dst_target_func: Callable[[FileInfo], str]
) -> FileInfoParsedResult:
"""展开FileInfoParsedResult中的目录。"""
file_info = parsed_result.file_info
if need_expand(file_info, get_dst_target_func):
# 如果当前是文件夹,需要展开计算
expand_infos, dir_infos = expand_dir(file_info, get_dst_target_func)
# 实测发现对于opp包整体目录cp的安装速度要快于目录中各文件mv
# 可能的原因是cp遍历目录的速度较快并且目录中的文件都比较小。mv依赖shell迭代目录中的所有文件。
return FileInfoParsedResult(merge_dict(file_info, {"is_dir": True}), [], dir_infos, expand_infos)
if use_move:
return FileInfoParsedResult({}, [file_info], parsed_result.dir_infos, parsed_result.expand_infos)
return parsed_result
def trans_file_info_to_result(file_info: FileInfo) -> FileInfoParsedResult:
"""file_info转换为FileInfoParsedResult。"""
return FileInfoParsedResult(file_info, [], [], [])
def parse_file_element(
file_ele: ET.Element,
file_config: dict[str, str],
loaded_block: LoadedBlockElement,
package_attr: PackageAttr,
env: ParseEnv,
) -> Iterator[FileInfoParsedResult]:
"""解析file元素。"""
file_info = merge_dict(file_config, file_ele.attrib)
file_info = evaluate_info(file_info, loaded_block, env.env_dict)
if package_attr.get("expand_asterisk", False):
expand_asterisk_func = partial(expand_file_info_asterisk, env=env)
else:
expand_asterisk_func = trans_to_stream
if "install_path" not in file_info:
file_info["install_path"] = ""
trans_file_info_func = pipe(
trans_file_info_to_result,
expand_asterisk_func,
partial(map, partial(config_hash, env=env)),
partial(
map,
partial(
expand_file_info, use_move=loaded_block.use_move, get_dst_target_func=partial(get_dst_target, env=env)
),
),
)
yield from trans_file_info_func(file_info)
def parse_file_info_elements(
loaded_block: LoadedBlockElement, default_config: dict[str, str], package_attr: PackageAttr, env: ParseEnv
) -> Iterator[FileInfoParsedResult]:
"""解析file_info元素。"""
file_info_elements: list[ET.Element] = loaded_block.root_ele.findall("file_info")
for file_info_ele in file_info_elements:
file_config = merge_dict(default_config, file_info_ele.attrib, {"module": file_info_ele.attrib.get("value")})
for sub_item in list(file_info_ele):
yield from parse_file_element(sub_item, file_config, loaded_block, package_attr, env)
def unique_infos(infos: Iterable) -> list[dict[str, str]]:
"""infos去重。"""
cache: set[str] = set()
new_infos = []
for info in infos:
if info["value"] in cache:
continue
cache.add(info["value"])
new_infos.append(info)
return new_infos
def parse_block_config(loaded_block: LoadedBlockElement, package_attr: PackageAttr, parse_env: ParseEnv):
"""解析块配置。"""
default_config = copy.copy(loaded_block.attrs)
default_config.update(loaded_block.root_ele.attrib)
dir_infos = parse_dir_info_elements(
loaded_block,
default_config,
package_attr,
parse_env,
)
file_info_results = list(
chain(
parse_file_info_elements(
loaded_block,
default_config,
package_attr,
parse_env,
)
)
)
generate_infos = parse_generate_infos_by_loaded_block(loaded_block, default_config, parse_env.env_dict)
return BlockConfig(
unique_infos(itertools.chain(dir_infos, flatten(result.dir_infos for result in file_info_results))),
list(flatten(map(attrgetter("move_infos"), file_info_results))),
list(flatten(map(attrgetter("expand_infos"), file_info_results))),
[result.file_info for result in file_info_results if result.file_info],
generate_infos,
)
def make_loaded_block_element(root_ele: ET.Element, dst_path: str = "") -> LoadedBlockElement:
"""创建加载后的块配置。"""
return LoadedBlockElement(root_ele, False, dst_path, set(), set(), {})
def parse_block_element(block_ele: ET.Element, block_info_attr: dict[str, str]) -> BlockElement:
"""解析单个块配置。"""
def filter_attrs(attrs: dict[str, str]) -> dict[str, str]:
# block属性中过滤掉dst_path与block_conf_path
# dst_path由单独的参数传递
# block中不需要block_conf_path
return {key: value for key, value in attrs.items() if key not in ("dst_path", "block_conf_path")}
def with_merged_attrs(attrs: dict[str, str]) -> BlockElement:
name = attrs.get("name")
block_conf_path = attrs.get("block_conf_path")
if not name:
raise BlockConfigError("block's name is not set!")
if not block_conf_path:
raise BlockConfigError("block's conf_path is not set!")
return BlockElement(
name=name,
block_conf_path=block_conf_path,
dst_path=attrs.get("dst_path", ""),
chips=config_feature_to_set(attrs.get("chip"), "chip"),
features=config_feature_to_set(attrs.get("feature"), "feature"),
attrs=filter_attrs(attrs),
)
return with_merged_attrs(merge_dict(block_info_attr, block_ele.attrib))
def parse_block_info(block_info: ET.Element) -> list[BlockElement]:
"""解析块配置。"""
def parse_block_elements(block_elements: list[ET.Element]) -> list[BlockElement]:
return [parse_block_element(block_ele, block_info.attrib) for block_ele in block_elements]
return parse_block_elements(list(block_info))
def get_block_filepath(block_element: BlockElement) -> str:
"""获取块配置路径。"""
return os.path.join(
pkg_utils.TOP_SOURCE_DIR, BLOCK_CONFIG_PATH, block_element.block_conf_path, f"{block_element.name}.xml"
)
def load_block_element(package_attr: PackageAttr, block_element: BlockElement) -> LoadedBlockElement:
"""加载块配置。"""
def with_filepath(block_xml: str):
if not os.path.exists(block_xml):
raise BlockConfigError(f"block's config xml {block_xml} does not exist!")
try:
return LoadedBlockElement(
root_ele=ET.parse(block_xml).getroot(),
use_move=package_attr.get("use_move", False),
**{name: getattr(block_element, name) for name in BLOCK_ELEMENT_PASS_THROUGH_ARGS},
)
except Exception:
raise BlockConfigError(f"dependent block configuration {block_xml} parse failed!")
return with_filepath(get_block_filepath(block_element))
def parse_blocks(root_ele: ET.Element, package_attr: PackageAttr, parse_env: ParseEnv) -> list[BlockConfig]:
"""解析块列表。"""
return [
parse_block_config(loaded_block, package_attr, parse_env)
for loaded_block in itertools.chain(
[make_loaded_block_element(root_ele)],
map(
partial(load_block_element, package_attr),
chain.from_iterable(map(parse_block_info, root_ele.findall("block_info"))),
),
)
]
def read_version_info() -> tuple[str, str]:
version_path = os.path.join(pkg_utils.TOP_DIR, "version.info")
with open(version_path) as file:
line1 = file.readline().strip()
line2 = file.readline().strip()
version = line1.split("=")[1]
version_dir = line2.split("=")[1]
m = re.match(r"[.a-zA-Z0-9]+$", version) or re.match(r"[-a-zA-Z.0-9]+$", version)
if not m:
raise VersionFormatNotMatch()
return version, version_dir
def parse_xml_config(filepath: str, delivery_dir: str, parse_option: ParseOption, args: Namespace) -> XmlConfig:
"""解析打包xml配置。"""
try:
tree = ET.parse(filepath)
xml_root = tree.getroot()
except ET.ParseError as ex:
CommLog.cilog_error("xml parse %s failed: %s!", filepath, ex)
sys.exit(FAIL)
default_config = xml_root.attrib.copy()
package_attr = parse_package_attr(xml_root, args)
if args.version_dir:
version = args.version_dir
version_dir = args.version_dir
else:
version, version_dir = read_version_info()
if args.disable_multi_version:
version_dir = None
timestamp = get_timestamp(args)
try:
env_dict = parse_env_dict(parse_option.os_arch, package_attr, version, version_dir, timestamp)
except ParseOsArchError:
CommLog.cilog_error("os_arch %s is not correctly configured: %s!", parse_option.os_arch, filepath)
sys.exit(FAIL)
parse_env = ParseEnv(env_dict, parse_option, delivery_dir, pkg_utils.TOP_SOURCE_DIR)
blocks = parse_blocks(xml_root, package_attr, parse_env)
if is_multi_version(version_dir):
fill_is_common_path_func = partial(fill_is_common_path, target_env=env_dict.get("TARGET_ENV"))
else:
fill_is_common_path_func = iter
return XmlConfig(default_config, package_attr, None, blocks, version, None, PackerConfig(fill_is_common_path_func))