13_apollo_tools_install子模块软件架构分析

4 阅读16分钟

13_apollo_tools_install子模块软件架构分析

1. 概述

  apollo_tools_install子模块是Apollo构建系统的安装工具,负责将编译好的二进制文件、库文件、头文件和配置文件安装到指定目录。该模块通过Bazel规则和Python脚本实现了灵活的安装机制,支持文件过滤、路径重命名、RPATH修复、符号剥离等功能,为Apollo系统提供了完整的部署支持。

2. 软件架构图

graph TB
    subgraph "Bazel构建系统"
        B1[Bazel工作空间]
        B2[规则引擎]
        B3[模板引擎]
    end

    subgraph "Install模块"
        I1[install.bzl]
        I2[install.py.in]
        I3[install_source.py.in]
        I4[InstallInfo提供者]
        I5[安装规则]
    end

    subgraph "安装脚本生成"
        S1[规则解析]
        S2[动作生成]
        S3[脚本展开]
        S4[可执行脚本]
    end

    subgraph "安装执行"
        E1[文件复制]
        E2[RPATH修复]
        E3[符号剥离]
        E4[元数据生成]
    end

    subgraph "目标系统"
        T1[bin目录]
        T2[lib目录]
        T3[include目录]
        T4[share目录]
    end

    B1 --> B2
    B2 --> I1
    I1 --> I4
    I1 --> I5
    I5 --> S1
    S1 --> S2
    S2 --> S3
    S3 --> I2
    S3 --> I3
    I2 --> S4
    I3 --> S4
    S4 --> E1
    E1 --> E2
    E2 --> E3
    E3 --> E4
    E4 --> T1
    E4 --> T2
    E4 --> T3
    E4 --> T4

    style I1 fill:#e1f5fe
    style I2 fill:#f3e5f5
    style I3 fill:#f3e5f5
    style I4 fill:#fff3e0

3. 调用流程图

sequenceDiagram
    participant User as 用户
    participant Bazel as Bazel构建系统
    participant Install as install.bzl
    participant Template as 模板引擎
    participant Script as install.py.in
    participant Executor as 安装执行器
    participant Target as 目标系统

    User->>Bazel: 执行bazel install命令
    Bazel->>Install: 加载install.bzl
    Install->>Install: 解析install规则
    Install->>Install: 收集安装动作
    Install->>Install: 生成安装代码
    Install->>Template: 传递模板和动作
    Template->>Script: 加载install.py.in模板
    Template->>Template: 替换<<actions>>占位符
    Template->>Bazel: 生成可执行脚本
    Bazel->>Executor: 执行安装脚本
    Executor->>Executor: 解析命令行参数
    Executor->>Executor: 执行安装动作
    Executor->>Target: 复制文件到目标目录
    Executor->>Executor: 修复RPATH
    Executor->>Executor: 剥离符号
    Executor->>Target: 生成元数据
    Executor-->>User: 安装完成

4. UML类图

4.1 核心规则类图

classDiagram
    class InstallRule {
        +name: string
        +deps: list
        +targets: list
        +docs: list
        +data: list
        +runtime_dest: string
        +library_dest: string
        +data_dest: string
        +doc_dest: string
        +strip_prefixes: list
        +rename: map
        +validate(): bool
        +generateActions(): list
    }

    class InstallInfo {
        +install_actions: list
        +rename: map
        +installed_files: map
        +addAction(action: InstallAction)
        +getActions(): list
    }

    class InstallAction {
        +src: File
        +dst: string
        +action_type: string
        +package_path: string
        +target_name: string
        +execute(): bool
    }

    class CcInstallAction {
        +src: File
        +dst: string
        +library_type: LibraryType
        +execute(): bool
        +fixRpath(): bool
    }

    class PyInstallAction {
        +src: File
        +dst: string
        +py_path: string
        +execute(): bool
        +setExecutable(): bool
    }

    class PluginInstallAction {
        +plugin: File
        +description: File
        +data: list
        +execute(): bool
        +generateIndex(): bool
    }

    InstallRule --> InstallInfo
    InstallInfo --> InstallAction
    InstallAction <|-- CcInstallAction
    InstallAction <|-- PyInstallAction
    InstallAction <|-- PluginInstallAction

4.2 脚本生成类图

classDiagram
    class ScriptGenerator {
        +template: File
        +actions: list
        +generate(): File
        +expandTemplate(): string
        -substitutions: map
    }

    class TemplateEngine {
        +load(template: File): string
        +expand(content: string, substitutions: map): string
        +save(content: string, output: File)
        -variables: list
    }

    class ActionCodeGenerator {
        +generate(action: InstallAction): string
        +generateInstallCode(action: InstallAction): string
        +generateInstallSrcCode(action: InstallAction): string
        +generatePluginCode(action: InstallAction): string
    }

    class InstallScript {
        +prefix: string
        +actions: list
        +execute(args: list)
        +install(src: string, dst: string)
        +fixRpaths(): bool
        +createPackageMeta(): bool
    }

    class SourceInstallScript {
        +prefix: string
        +actions: list
        +execute(args: list)
        +installSrc(src: string, dst: string, filter: string)
        +fixConfigs(): bool
    }

    ScriptGenerator --> TemplateEngine
    ScriptGenerator --> ActionCodeGenerator
    TemplateEngine --> InstallScript
    TemplateEngine --> SourceInstallScript

4.3 文件管理类图

classDiagram
    class FileManager {
        +copy(src: string, dst: string)
        +link(src: string, dst: string)
        +remove(path: string)
        +exists(path: string): bool
        +isLink(path: string): bool
    }

    class PathResolver {
        +resolve(path: string): string
        +makeRelative(base: string, target: string): string
        +normalize(path: string): string
        +stripPrefix(path: string, prefix: string): string
    }

    class RPathFixer {
        +fix(path: string, rpath: string)
        +getRpath(path: string): string
        +setRpath(path: string, rpath: string)
        +resolveInstallRpath(path: string, rpath: string): string
    }

    class SymbolStripper {
        +strip(path: string)
        +stripDebug(path: string)
        +keepSymbols(path: string)
    }

    class CacheManager {
        +createCache(src: string)
        +compare(src: string, cache: string): bool
        +needsInstall(src: string, dst: string): bool
        -cachePrefix: string
    }

    FileManager --> PathResolver
    FileManager --> RPathFixer
    FileManager --> SymbolStripper
    FileManager --> CacheManager

5. 状态机分析

5.1 安装规则解析状态机

stateDiagram-v2
    [*] --> IDLE: Bazel启动
    IDLE --> LOADING_RULES: 加载install.bzl
    LOADING_RULES --> PARSING_DEPS: 规则加载完成
    PARSING_DEPS --> COLLECTING_ACTIONS: 依赖解析完成
    COLLECTING_ACTIONS --> GENERATING_CODE: 动作收集完成
    GENERATING_CODE --> EXPANDING_TEMPLATE: 代码生成完成
    EXPANDING_TEMPLATE --> READY: 模板展开完成
    READY --> EXECUTING: 开始执行
    EXECUTING --> COMPLETED: 执行成功
    EXECUTING --> ERROR: 执行失败
    ERROR --> [*]: 错误终止
    COMPLETED --> [*]: 安装完成

    note right of PARSING_DEPS : 解析deps属性
    note right of COLLECTING_ACTIONS : 收集所有安装动作
    note right of GENERATING_CODE : 生成Python安装代码
    note right of EXPANDING_TEMPLATE : 替换模板占位符

5.2 文件安装状态机

stateDiagram-v2
    [*] --> IDLE: 安装开始
    IDLE --> CHECKING_CACHE: 检查缓存
    CHECKING_CACHE --> COPYING_FILE: 需要安装
    CHECKING_CACHE --> SKIPPING: 文件已最新
    SKIPPING --> [*]: 跳过安装
    COPYING_FILE --> CREATING_DIR: 创建目录
    CREATING_DIR --> COPYING: 目录创建完成
    COPYING --> FIXING_PERMISSIONS: 文件复制完成
    FIXING_PERMISSIONS --> FIXING_RPATH: 权限设置完成
    FIXING_RPATH --> STRIPPING_SYMBOLS: RPATH修复完成
    STRIPPING_SYMBOLS --> COMPLETED: 符号剥离完成
    COMPLETED --> [*]: 安装完成

    note right of CHECKING_CACHE : 比较文件哈希值
    note right of COPYING_FILE : 复制或链接文件
    note right of FIXING_PERMISSIONS : 设置可执行权限
    note right of FIXING_RPATH : 修复运行时路径
    note right of STRIPPING_SYMBOLS : 剥离调试符号

5.3 RPATH修复状态机

stateDiagram-v2
    [*] --> IDLE: 修复开始
    IDLE --> READING_RPATH: 读取当前RPATH
    READING_RPATH --> RESOLVING_PATHS: RPATH读取完成
    RESOLVING_PATHS --> FILTERING_PATHS: 路径解析完成
    FILTERING_PATHS --> BUILDING_NEW_RPATH: 路径过滤完成
    BUILDING_NEW_RPATH --> SETTING_RPATH: 新RPATH构建完成
    SETTING_RPATH --> COMPLETED: RPATH设置完成
    COMPLETED --> [*]: 修复完成

    note right of READING_RPATH : 使用patchelf读取
    note right of RESOLVING_PATHS : 解析$ORIGIN变量
    note right of FILTERING_PATHS : 过滤无效路径
    note right of BUILDING_NEW_RPATH : 构建新的RPATH列表
    note right of SETTING_RPATH : 使用patchelf设置

5.4 包元数据生成状态机

stateDiagram-v2
    [*] --> IDLE: 生成开始
    IDLE --> PARSING_CYBERFILE: 解析cyberfile.xml
    PARSING_CYBERFILE --> COLLECTING_FILES: cyberfile解析完成
    COLLECTING_FILES --> GENERATING_META: 文件收集完成
    GENERATING_META --> WRITING_PACK_JSON: 元数据生成完成
    WRITING_PACK_JSON --> WRITING_BUILD_FILE: pack.json写入完成
    WRITING_BUILD_FILE --> COMPLETED: BUILD文件写入完成
    COMPLETED --> [*]: 生成完成

    note right of PARSING_CYBERFILE : 读取包名和依赖
    note right of COLLECTING_FILES : 收集安装的文件列表
    note right of GENERATING_META : 生成包元数据
    note right of WRITING_PACK_JSON : 写入pack.json
    note right of WRITING_BUILD_FILE : 生成.BUILD文件

6. 源码分析

6.1 install.bzl文件分析

6.1.1 文件结构概述

  install.bzl文件是Apollo安装系统的核心规则定义文件,使用Starlark语言编写。该文件定义了install、install_files、install_src_files和install_plugin等规则,以及InstallInfo提供者和相关的辅助函数。

# -*- python -*-
# Adapted from RobotLocomotion/drake:tools/install/install.bzl
load("//tools:common.bzl", "dirname", "join_paths", "output_path", "remove_prefix")

InstallInfo = provider()
6.1.2 InstallInfo提供者

  InstallInfo是一个Bazel提供者(Provider),用于在规则之间传递安装信息:

InstallInfo = provider()

   提供者是Bazel中用于在规则之间传递数据的机制。InstallInfo包含以下信息: - install_actions:安装动作列表 - rename:文件重命名映射 - installed_files:已安装文件映射

6.1.3 辅助函数分析
6.1.3.1 _workspace函数
def _workspace(ctx):
    """Compute name of current workspace."""
    if hasattr(ctx.attr, "workspace"):
        if len(ctx.attr.workspace):
            return ctx.attr.workspace
    workspace = ctx.label.workspace_root.split("/")[-1]
    if len(workspace):
        return workspace
    return ctx.workspace_name

  该函数计算当前工作空间的名称,支持通过属性覆盖默认值。首先检查是否有workspace属性覆盖,然后从label中提取工作空间名称,最后使用ctx.workspace_name作为默认值。

6.1.3.2 _output_path函数
def _output_path(ctx, input_file, strip_prefix = [], warn_foreign = True):
    """Compute output path (without destination prefix) for install action."""
    path = output_path(ctx, input_file, strip_prefix)
    if path != None:
        return path
    owner = input_file.owner
    if owner.workspace_name != "" and owner.workspace_name != "apollo_src":
        dest = join_paths("third_party", owner.workspace_name, owner.package, input_file.basename)
    else:
        if owner.workspace_name == "apollo_src":
            return input_file.basename
        dest = join_paths(owner.package, input_file.basename)
    for p in strip_prefix:
        dest = remove_prefix(dest, p)
        if dest != None:
            return dest
    return dest

  该函数计算文件的输出路径,处理当前包和外部包的不同情况。对于外部包,将文件放在third_party目录下;对于当前工作空间,使用相对路径。

6.1.3.3 _install_action函数
def _install_action(
        ctx,
        artifact,
        dests,
        strip_prefixes = [],
        rename = {},
        warn_foreign = True,
        py_bin = False,
        py_path = None,
        plugin = False):
    """Compute install action for a single file."""
    if type(dests) == "dict":
        dest = dests.get(artifact.extension, dests[None])
    else:
        dest = dests

    dest_replacements = (
        ("@WORKSPACE@", _workspace(ctx)),
        ("@PACKAGE@", ctx.label.package.replace("/", "-")),
        ("@PACKAGE_PATH@", ctx.label.package),
    )
    for old, new in dest_replacements:
        if old in dest:
            dest = dest.replace(old, new)

    if type(strip_prefixes) == "dict":
        strip_prefix = strip_prefixes.get(
            artifact.extension,
            strip_prefixes[None],
        )
    else:
        strip_prefix = strip_prefixes
    if py_bin:
        file_dest = join_paths(dest, py_path)
    elif "@" not in dest:
        file_dest = join_paths(dest, _output_path(ctx, artifact, strip_prefix, warn_foreign))
    else:
        file_dest = dest
    file_dest = _rename(file_dest, rename)

    target_name = None
    if hasattr(ctx.attr, "tags") and len(ctx.attr.tags) >= 2 and "export_library" in ctx.attr.tags:
        for i in ctx.attr.tags:
            if i == "__CC_RULES_MIGRATION_DO_NOT_USE_WILL_BREAK__" or i == "export_library" or i == "exclude":
                continue
            else:
                target_name = i
    package_path = "None"
    if hasattr(ctx.attr, "package_path") and ctx.attr.package_path != "NONE":
        package_path = ctx.attr.package_path
    if hasattr(ctx.attr, "type") and ctx.attr.type != "NONE":
        install_type = ctx.attr.type
    else:
        install_type = "NONE"
    if plugin:
        install_type = "neo"
    return struct(
        src = artifact,
        dst = file_dest,
        target_name = target_name,
        package_path = package_path,
        type = install_type,
    )

  该函数计算单个文件的安装动作,支持多种配置选项:

  • dests:目标路径,可以是字符串或字典(按文件扩展名映射)
  • strip_prefixes:要剥离的前缀列表
  • rename:文件重命名映射
  • py_bin:是否为Python二进制文件
  • plugin:是否为插件

  函数还处理了特殊标记,如export_library,用于导出库信息。

6.1.4 install规则分析
def _install_impl(ctx):
    actions = []
    rename = dict(ctx.attr.rename)

    for d in ctx.attr.deps:
        actions += d[InstallInfo].install_actions
        rename.update(d[InstallInfo].rename)

    actions += _install_actions(
        ctx,
        ctx.attr.docs,
        ctx.attr.doc_dest,
        strip_prefixes = ctx.attr.doc_strip_prefix,
        rename = rename,
    )

    actions += _install_actions(
        ctx,
        ctx.attr.data,
        ctx.attr.data_dest,
        strip_prefixes = ctx.attr.data_strip_prefix,
        rename = rename,
    )

    for t in ctx.attr.targets:
        if CcInfo in t:
            actions += _install_cc_actions(ctx, t)
        elif PyInfo in t:
            actions += _install_py_actions(ctx, t)
        elif hasattr(t, "files_to_run") and t.files_to_run.executable:
            actions += _install_runtime_actions(ctx, t)

    files = ctx.runfiles(files = [a.src for a in actions])
    installed_files = _generate_install_script_action(actions, ctx)

    return [
        InstallInfo(
            install_actions = actions,
            rename = rename,
            installed_files = installed_files,
        ),
        DefaultInfo(runfiles = files),
    ]

  该函数实现了install规则的实现逻辑:

  1. 收集依赖的安装动作
  2. 添加文档文件的安装动作
  3. 添加数据文件的安装动作
  4. 根据目标类型添加相应的安装动作
  5. 生成安装脚本
  6. 返回InstallInfo和DefaultInfo
6.1.5 _install_cc_actions函数
def _install_cc_actions(ctx, target):
    dests = {
        "so": ctx.attr.library_dest,
        None: ctx.attr.runtime_dest,
    }
    strip_prefixes = {
        "so": ctx.attr.library_strip_prefix,
        None: ctx.attr.runtime_strip_prefix,
    }
    actions = _install_actions(
        ctx,
        [target],
        dests,
        strip_prefixes,
        rename = ctx.attr.rename,
    )

    mangled_solibs = [
        f
        for f in _depset_to_list(target.default_runfiles.files)
        if not f.is_source and target.label != f.owner
    ]

    if ctx.attr.guess_data != "NONE":
        data = [
            f
            for f in _depset_to_list(target.data_runfiles.files)
            if f.is_source
        ]
        data = _guess_files(target, data, ctx.attr.guess_data, "guess_data")
        actions += _install_actions(
            ctx,
            [struct(files = data)],
            ctx.attr.data_dest,
            ctx.attr.data_strip_prefix,
            ctx.attr.guess_data_exclude,
            rename = ctx.attr.rename,
        )

    return actions

  该函数处理C/C++目标的安装动作:

  1. 为共享库(.so)和可执行文件设置不同的目标路径
  2. 处理mangled的共享库
  3. 根据guess_data设置自动猜测数据文件

6.2 install.py.in文件分析

6.2.1 文件结构概述

  install.py.in是安装脚本的模板文件,由Bazel的模板引擎展开后生成可执行的安装脚本。该脚本使用Python编写,实现了文件安装、RPATH修复、符号剥离等功能。

#!/usr/bin/env python3
"""
Installation script generated from a Bazel `install` target.
"""
import argparse
import collections
import filecmp
import hashlib
import os
import re
import json
import shutil
import stat
import sys
import subprocess
import xml.etree.ElementTree as ET
from pathlib import Path
from multiprocessing import Pool, Lock
6.2.2 全局变量定义
subdirs = set()
color = False
prefix = None
strip = True
strip_tool = None
install_lib = True
fix_rpath = True

deprecated_package_prefix = "packages"

libraries_to_fix_rpath = {}
binaries_to_fix_rpath = []
potential_binaries_to_fix_rpath = []
list_only = False
dylib_match = re.compile(r"(.*\.so)(\.\d+)*$")

workspace = os.getenv("PWD", "/apollo_workspace").split("/.cache/")[0]
lib_cache_prefix = os.path.join(workspace, "dev", "install", "apollo")

meta = []
export_library_dict = {}
packages = {}
plugins = {}
plugins_meta = {}
installed_ele = set()
meta_prefix = "share/packages"

  这些全局变量控制安装脚本的行为:

  • subdirs:已创建的子目录集合
  • prefix:安装前缀
  • libraries_to_fix_rpath:需要修复RPATH的库文件映射
  • binaries_to_fix_rpath:需要修复RPATH的二进制文件列表
  • packages:包信息映射
6.2.3 needs_install函数
def needs_install(src, dst):
    global lib_cache_prefix
    co_dev = os.getenv("CO_DEV", 0)

    if co_dev:
        return True

    if os.path.basename(dst) == "cyberfile.xml":
        return True

    dst_full = os.path.join(prefix, dst)

    if not os.path.exists(dst_full):
        create_cache(src)
        installed_ele.add(src)
        return True

    cache = os.path.join(lib_cache_prefix, src)
    if not os.path.exists(cache):
        create_cache(src)
        installed_ele.add(src)
        return True

    if cachecmp(src, cache):
        if src in installed_ele:
            return True
        return False
    else:
        installed_ele.add(src)
        return True

  该函数判断文件是否需要安装,通过比较文件哈希值来避免不必要的安装:

  1. 在CO_DEV模式下总是安装
  2. cyberfile.xml总是安装
  3. 检查目标文件是否存在
  4. 比较缓存中的哈希值
6.2.4 install函数
def install(src, dst, action_type=None, package_path=None,
        shared_library_export=None, target_name=None):
    global subdirs
    global meta

    deprecated_flag = True

    if action_type is not None and package_path is not None:
        global packages
        global export_library_dict

        package_in_cache = False
        deprecated_flag = False

        true_pkg_path = None

        for k in packages:
            if package_path.startswith(k):
                if package_path == k or package_path[len(k)] == "/":
                    package_in_cache = True
                    packages[k].append("{}:{}".format(src, dst))
                    true_pkg_path = k
                    break

        if not package_in_cache:
            src_list = src.split("/")
            for i in range(0, len(src_list)):
                temp_path = "/".join(src_list[0: i])
                temp_cyberfile_path = os.path.join(temp_path, "cyberfile.xml")
                if os.path.exists(temp_cyberfile_path):
                    if temp_path in packages:
                        packages[temp_path].append("{}:{}".format(src, dst))
                    else:
                        packages[temp_path] = ["{}:{}".format(src, dst)]
                    true_pkg_path = temp_path
                    break

        if true_pkg_path is None:
            print("\033[31m[ERROR]\033[0m orphan install file: {} -> {}".format(
                    src, dst), file=sys.stderr)
            exit(-1)

        if shared_library_export is not None and target_name is not None:
            if true_pkg_path not in export_library_dict:
                export_library_dict[true_pkg_path] = [{"target": target_name, "dst": dst}]
            else:
                export_library_dict[true_pkg_path].append({"target": target_name, "dst": dst})
    else:
        dst = rename_package_name(dst)

    if legacy:
        if not dst.startswith("lib/"):
            dst = src

    if not deprecated_flag and src.endswith("cyberfile.xml"):
        module_src_path = src.replace("/cyberfile.xml", "")
        cyberfile_parser = ET.parse(src)
        root = cyberfile_parser.getroot()

        package_name = root.find("name").text
        dst = "{}/{}/cyberfile.xml".format(meta_prefix, package_name)
    # ... 更多处理逻辑

  该函数实现了文件安装的核心逻辑:

  1. 处理包路径和包信息
  2. 处理cyberfile.xml的特殊安装
  3. 创建目标目录
  4. 复制或链接文件
  5. 设置文件权限
  6. 收集需要修复RPATH的文件
6.2.5 fix_rpaths_and_strip函数
def fix_rpaths_and_strip(compatible=False):
    find_binary_executables()
    lib_fix_items = []
    bin_fix_items = []
    for k in libraries_to_fix_rpath:
        lib_fix_items += libraries_to_fix_rpath[k]

    for i in range(len(binaries_to_fix_rpath)):
        bin_fix_items += binaries_to_fix_rpath[i][1]

    for dst_full in lib_fix_items:
        if os.path.islink(dst_full):
            continue
        os.chmod(dst_full, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
                | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
        py_bin_path = os.path.join(prefix, "bin")
        if dst_full.startswith(py_bin_path) and ".runfiles" in dst_full:
            continue
        linux_fix_rpaths(dst_full, binary=False, compatible=compatible)

    for dst_full in bin_fix_items:
        if os.path.islink(dst_full):
            continue
        os.chmod(dst_full, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
                | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
        py_bin_path = os.path.join(prefix, "bin")
        if dst_full.startswith(py_bin_path) and ".runfiles" in dst_full:
            continue
        linux_fix_rpaths(dst_full, binary=True, compatible=compatible)

  该函数修复库文件和可执行文件的RPATH:

  1. 查找二进制可执行文件
  2. 收集需要修复的库文件和二进制文件
  3. 为每个文件设置写权限
  4. 调用linux_fix_rpaths修复RPATH
6.2.6 linux_fix_rpaths函数
def linux_fix_rpaths(path, binary=False, compatible=False):
    old_rpath = check_output(['patchelf', '--print-rpath', path])
    old_rpath_list = old_rpath.decode('utf-8').strip().split(':')
    if binary:
        prefix = "/opt/apollo/neo/bin"
    else:
        prefix = '/opt/apollo/neo/lib'
    new_rpath_list = list(filter(
        lambda x: x is not None and x != '',
        map(lambda x: resolve_install_rpath(path, x, prefix), old_rpath_list)))
    new_rpath = ':'.join(new_rpath_list)
    compatible_rpath_list = []
    if compatible:
        for i in new_rpath_list:
            if i.startswith("/opt/apollo/neo/lib"):
                compatible_rpath_list.append(
                    os.path.abspath(i).replace("/opt/apollo/neo/lib", "/apollo/bazel-bin"))
            elif i.startswith("$ORIGIN"):
                if "../lib/" in i:
                    suffix = i.split("../lib/")[-1]
                else:
                    suffix = i.split("../")[-1]
                compatible_rpath_list.append(os.path.join("/apollo/bazel-bin", suffix))

        compatible_rpath_list = list(set(compatible_rpath_list))

    if len(compatible_rpath_list) > 0:
        new_rpath = "{}:{}".format(new_rpath, ":".join(compatible_rpath_list))

    check_call(['patchelf', '--force-rpath', '--set-rpath', new_rpath, path])

  该函数使用patchelf工具修复RPATH:

  1. 读取当前的RPATH
  2. 解析每个RPATH条目
  3. 构建新的RPATH列表
  4. 如果需要兼容源码环境,添加兼容路径
  5. 使用patchelf设置新的RPATH

6.3 install_source.py.in文件分析

6.3.1 文件结构概述

  install_source.py.in是源代码安装脚本的模板文件,用于安装源代码文件。该脚本支持文件过滤、进度显示等功能。

#!/usr/bin/env python3
"""
Installation script generated from a Bazel `install` target.
"""
import argparse
import time
import os
import re
import shutil
import stat
import sys
from subprocess import check_output, check_call, Popen, PIPE
import xml.etree.ElementTree as ET

prefix = None
pkg_name = None
list_only = False
deprecated_package_prefix = "packages"
src_install_dirs_dict = {}
remove_path = []
6.3.2 progressbar函数
def progressbar(it, length, prefix="", out=sys.stdout):
    count = length
    start = time.time()
    try:
        size = int(os.get_terminal_size().columns / 4)
    except:
        size = 5
    def show(j):
        if count == 0:
            return
        x = int(size * j / count)
        remaining = ((time.time - start) / j) * (count - j)

        mins, sec = divmod(remaining, 60)
        time_str = f"{int(mins):02}:{sec:05.2f}"

        print(f"  {prefix}[{'#'*x}{('.'*(size-x))}] {j}/{count} Est wait {time_str}",
            end='\r', file=out, flush=True)

    for i, item in enumerate(it):
        yield item
        show(i + 1)
    try:
        print(" " * os.get_terminal_size().columns, end="\r")
    except:
        pass

  该函数显示安装进度条,包括:

  • 进度条可视化
  • 当前进度/总数
  • 预计剩余时间
6.3.3 install_src函数
def install_src(src, dst, file_filter, action_type=None):
    if list_only:
        return
    deprecated_flag = False
    if action_type is None:
        deprecated_flag = True
        dst = rename_package_name(dst)

    if not os.path.exists(src) and "__DO_NOT_INSTALL__" in src:
        remove_path.append(os.path.join(prefix, dst))
        return

    if not os.path.isdir(src):
        sys.stderr.write("install_src only support dir, {} is not dir.".format(dst))
        sys.exit(-1)

    if deprecated_flag:
        dst_full = os.path.join(prefix, deprecated_package_prefix, dst)
        if not os.path.exists(dst_full):
            os.makedirs(dst_full)
        shell_cmd("cd {} && find . -name '{}'|xargs -i -I@@ cp -rvfPL --parents @@ {}/ > /dev/null"
            .format(src, file_filter, dst_full))
    else:
        global src_install_dirs_dict
        dst_full = os.path.join(prefix, dst)
        if file_filter not in src_install_dirs_dict:
            src_install_dirs_dict[file_filter] = {}
        src_install_dirs_dict[file_filter][src] = {"dst": dst_full, "pwd": os.getcwd()}

  该函数安装源代码文件:

  1. 处理废弃的包路径
  2. 检查源目录是否存在
  3. 使用find和cp命令复制文件
  4. 收集安装信息用于后续处理
6.3.4 install_src_file函数
def install_src_file():
    global src_install_dirs_dict

    for file_filter in src_install_dirs_dict:
        common_prefix = os.path.commonprefix([src_dir for src_dir in src_install_dirs_dict[file_filter]])
        pwd = os.getcwd()
        for i in progressbar(src_install_dirs_dict[file_filter],
                len(src_install_dirs_dict[file_filter]), "Install Package Source Code:"):
            dst_full = src_install_dirs_dict[file_filter][i]["dst"]
            if not os.path.exists(dst_full):
                os.makedirs(dst_full)
            if file_filter == "*.h":
                shell_cmd("rsync -aLr --whole-file --include='{}' --include='*/' --exclude='*' {}/ {}/"
                    .format(file_filter, is_relative_link(os.path.join(pwd, i)), dst_full))
            else:
                shell_cmd("rsync -aLr --delete --whole-file --include='{}' --include='*/' --exclude='*' {}/ {}/"
                    .format(file_filter, is_relative_link(os.path.join(pwd, i)), dst_full))

    for i in remove_path:
        for root, _, files in os.walk(i):
            if root != i and os.path.exists(os.path.join(root, "BUILD")):
                continue
            for f in files:
                f = os.path.join(root, f)
                shell_cmd(f"rm -rf {f}")

  该函数执行源代码文件的实际安装:

  1. 计算公共前缀以优化复制
  2. 使用rsync命令复制文件
  3. 显示进度条
  4. 删除标记为不安装的文件

7. 设计模式

7.1 模板方法模式(Template Method Pattern)

  install.py.in和install_source.py.in使用模板方法模式,通过Bazel的模板引擎生成最终的安装脚本:

# 模板文件
ctx.actions.expand_template(
    template = ctx.executable.install_script_template,
    output = ctx.outputs.executable,
    substitutions = {"<<actions>>": "\n    ".join(script_actions)},
)

  这种设计将安装逻辑的框架固定在模板中,具体的安装动作通过替换占位符注入。

7.2 策略模式(Strategy Pattern)

  install.bzl中的不同安装规则实现了策略模式,根据目标类型选择不同的安装策略:

for t in ctx.attr.targets:
    if CcInfo in t:
        actions += _install_cc_actions(ctx, t)
    elif PyInfo in t:
        actions += _install_py_actions(ctx, t)
    elif hasattr(t, "files_to_run") and t.files_to_run.executable:
        actions += _install_runtime_actions(ctx, t)

7.3 建造者模式(Builder Pattern)

  InstallAction的构建过程体现了建造者模式:

return struct(
    src = artifact,
    dst = file_dest,
    target_name = target_name,
    package_path = package_path,
    type = install_type,
)

  通过逐步设置属性来构建复杂的安装动作对象。

7.4 观察者模式(Observer Pattern)

  缓存机制体现了观察者模式的思想,通过观察文件变化来决定是否需要安装:

def needs_install(src, dst):
    if cachecmp(src, cache):
        return False
    else:
        return True

7.5 责任链模式(Chain of Responsibility Pattern)

  RPATH解析过程体现了责任链模式:

def resolve_install_rpath(path, rpath, prefix):
    if not rpath.startswith('$ORIGIN'):
        return rpath
    # 多层解析逻辑
    if prefix == lib_prefix:
        # 处理库路径
    else:
        # 处理二进制路径

7.6 工厂模式(Factory Pattern)

  不同类型的安装动作通过工厂模式创建:

def _install_action(..., py_bin=False, plugin=False):
    if plugin:
        install_type = "neo"
    # 根据参数创建不同类型的动作
    return struct(...)

  工厂模式的核心思想是定义一个创建对象的接口,让子类决定实例化哪个类。在安装系统中,工厂模式用于创建不同类型的安装动作,如C/C++库安装动作、Python脚本安装动作、插件安装动作等。这种设计使得安装动作的创建更加灵活和可扩展。

7.7 适配器模式(Adapter Pattern)

  RPATH修复器使用适配器模式,适配不同平台的RPATH格式:

def resolve_install_rpath(path, rpath, prefix):
    if not rpath.startswith('$ORIGIN'):
        return rpath
    # 适配不同平台的RPATH格式
    if prefix == lib_prefix:
        return adapt_library_rpath(rpath)
    else:
        return adapt_binary_rpath(rpath)

  适配器模式的核心思想是将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。在安装系统中,适配器模式用于适配不同平台的RPATH格式,确保RPATH修复在不同平台上都能正常工作。

7.8 装饰器模式(Decorator Pattern)

  安装动作可以通过装饰器模式增强功能:

def install_with_logging(src, dst):
    print(f"Installing {src} to {dst}")
    result = install(src, dst)
    print(f"Installation completed: {result}")
    return result

  装饰器模式的核心思想是动态地给一个对象添加一些额外的职责,就增加功能来说,装饰器模式相比生成子类更为灵活。在安装系统中,装饰器模式用于为安装动作添加额外的功能,如日志记录、性能监控、错误处理等。

7.9 组合模式(Composite Pattern)

  安装动作通过组合模式组织成树形结构:

class CompositeInstallAction:
    def __init__(self):
        self.actions = []
    
    def add_action(self, action):
        self.actions.append(action)
    
    def execute(self):
        for action in self.actions:
            action.execute()

  组合模式的核心思想是将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性。在安装系统中,组合模式用于组织安装动作为树形结构,便于批量执行和依赖管理。

7.10 命令模式(Command Pattern)

  安装操作通过命令模式封装,支持撤销和重做:

class InstallCommand:
    def __init__(self, src, dst):
        self.src = src
        self.dst = dst
        self.backup = None
    
    def execute(self):
        self.backup = self.create_backup()
        install(self.src, self.dst)
    
    def undo(self):
        if self.backup:
            restore(self.backup)

  命令模式的核心思想是将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。在安装系统中,命令模式用于封装安装操作,支持撤销和重做,提高安装的可靠性。

8. 最佳实践分析

8.1 安装配置最佳实践

8.1.1 路径管理

  合理管理安装路径,避免路径冲突:

# 使用标准目录结构
runtime_dest = "bin"
library_dest = "lib"
data_dest = "share"
doc_dest = "share/doc"

  路径管理是安装配置的重要方面。通过使用标准目录结构,可以避免路径冲突,提高系统的可维护性。最佳实践是遵循FHS(Filesystem Hierarchy Standard)标准,使用bin、lib、share等标准目录。

8.1.2 文件过滤

  使用文件过滤避免安装不必要的文件:

# 使用strip_prefix过滤路径
doc_strip_prefix = ["docs/"]
data_strip_prefix = ["data/"]

  文件过滤是安装配置的另一个重要方面。通过使用strip_prefix过滤路径,可以避免安装不必要的文件,减少安装时间和磁盘空间占用。最佳实践是只安装必要的文件,避免安装测试文件、临时文件等。

8.1.3 权限管理

  合理设置文件权限,确保安全性:

# 设置可执行权限
os.chmod(dst_full, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
        | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)

  权限管理是安装配置的另一个重要方面。通过合理设置文件权限,可以确保系统的安全性。最佳实践是为可执行文件设置执行权限,为库文件设置读写权限,为配置文件设置适当的权限。

8.2 性能优化最佳实践

8.2.1 增量安装优化

  使用缓存机制实现增量安装:

def needs_install(src, dst):
    if cachecmp(src, cache):
        return False
    else:
        return True

  增量安装优化是性能优化的重要技术。通过使用缓存机制,可以避免重复安装未修改的文件,显著提高安装速度。缓存策略包括文件哈希比较、时间戳比较等。

8.2.2 并行安装优化

  使用多进程加速安装:

from multiprocessing import Pool, Lock

def install_parallel(files):
    with Pool() as pool:
        pool.map(install_single, files)

  并行安装优化是性能优化的另一个重要技术。通过使用多进程,可以同时安装多个文件,显著提高安装速度。并行安装的关键是正确处理共享资源和同步问题。

8.2.3 符号链接优化

  使用符号链接减少磁盘空间占用:

def install_with_symlink(src, dst):
    if can_use_symlink(src, dst):
        os.symlink(src, dst)
    else:
        shutil.copy2(src, dst)

  符号链接优化是性能优化的另一个重要技术。通过使用符号链接,可以减少磁盘空间占用,提高安装速度。符号链接的关键是正确处理相对路径和绝对路径。

8.3 安全最佳实践

8.3.1 完整性验证

  验证文件的完整性,防止篡改:

def verify_integrity(src, expected_hash):
    actual_hash = calculate_hash(src)
    if actual_hash != expected_hash:
        raise IntegrityError(f"File {src} has been tampered with")

  完整性验证是安全性的重要方面。通过验证文件的完整性,可以防止文件被篡改,确保系统的安全性。完整性验证包括哈希验证、数字签名验证等。

8.3.2 权限检查

  检查文件权限,防止权限提升:

def check_permissions(path):
    mode = os.stat(path).st_mode
    if mode & 0o777 != 0o755:
        raise PermissionError(f"Invalid permissions for {path}")

  权限检查是安全性的另一个重要方面。通过检查文件权限,可以防止权限提升,确保系统的安全性。权限检查包括检查文件的所有者、组、权限等。

8.3.3 路径安全

  防止路径遍历攻击:

def safe_path(path):
    real_path = os.path.realpath(path)
    if not real_path.startswith(prefix):
        raise PathTraversalError(f"Path traversal attempt: {path}")
    return real_path

  路径安全是安全性的另一个重要方面。通过防止路径遍历攻击,可以确保系统的安全性。路径安全包括检查路径是否在允许的范围内、防止使用../等特殊字符等。

8.4 维护最佳实践

8.4.1 日志记录

  记录安装过程,便于问题诊断:

def install_with_logging(src, dst):
    logger.info(f"Installing {src} to {dst}")
    try:
        install(src, dst)
        logger.info(f"Installation completed: {dst}")
    except Exception as e:
        logger.error(f"Installation failed: {e}")
        raise

  日志记录是可维护性的重要方面。通过记录安装过程,可以便于问题诊断和审计。日志记录应该包括安装的文件、安装的时间、安装的结果等信息。

8.4.2 错误处理

  提供清晰的错误信息,便于问题解决:

def install_with_error_handling(src, dst):
    try:
        install(src, dst)
    except FileNotFoundError as e:
        raise InstallError(f"Source file not found: {src}") from e
    except PermissionError as e:
        raise InstallError(f"Permission denied: {dst}") from e

  错误处理是可维护性的另一个重要方面。通过提供清晰的错误信息,可以便于问题解决。错误处理应该包括错误类型、错误位置、建议的解决方案等信息。

8.4.3 版本管理

  使用版本控制系统管理安装脚本:

# 使用Git管理安装脚本
# 每次修改都提交到版本控制系统
# 使用分支管理不同版本的配置

  版本管理是可维护性的另一个重要方面。通过使用版本控制系统管理安装脚本,可以跟踪变更历史、支持回滚、便于协作开发。版本管理应该包括提交规范、分支策略、代码审查等。

8.5 集成最佳实践

8.5.1 与Bazel的集成

  与Bazel构建系统紧密集成:

def _install_impl(ctx):
    # 使用Bazel的actions API
    ctx.actions.expand_template(...)
    ctx.actions.declare_file(...)

  与Bazel的集成是确保安装系统正常工作的关键。最佳实践是使用Bazel的actions API、遵循Bazel的规则定义规范、使用Bazel的提供者机制等。

8.5.2 与Cyber的集成

  与Apollo的Cyber框架集成:

# 解析cyberfile.xml
cyberfile_parser = ET.parse(src)
root = cyberfile_parser.getroot()
package_name = root.find("name").text

  与Cyber的集成是确保安装系统在Apollo环境中正常工作的关键。最佳实践是解析cyberfile.xml、使用Cyber的包管理机制、遵循Cyber的目录结构等。

8.5.3 持续集成

  在持续集成中验证安装:

# CI配置
jobs:
  install:
    steps:
      - name: Build
        run: bazel build //...
      - name: Install
        run: bazel install //...
      - name: Verify
        run: ./scripts/verify_install.sh

  持续集成是确保安装系统质量的重要手段。最佳实践是在每次提交时运行构建和安装、使用自动化测试、监控安装状态等。

9. 总结

  apollo_tools_install子模块通过Bazel规则和Python脚本实现灵活高效的安装机制。模块采用模板化设计,通过install.bzl定义规则,install.py.in和install_source.py.in生成脚本,支持文件过滤、路径重命名、RPATH修复、符号剥离等功能。使用InstallInfo提供者传递信息,支持依赖传递和增量安装,为Apollo项目部署提供完整解决方案。