免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

C++包管理器CPPAN:解决依赖管理与构建碎片化难题

C++包管理器CPPAN:解决依赖管理与构建碎片化难题 1. 项目概述为什么我们需要一个C的“中央仓库”如果你是一个C开发者无论是刚入门的新手还是摸爬滚打多年的老手我相信你都经历过类似的痛苦想用一个开源的JSON解析库比如nlohmann/json结果发现需要手动下载源码、配置CMake、解决依赖还可能因为编译器版本、构建选项不对而编译失败。或者你写了一个很棒的库想分享给别人却发现除了把代码扔到GitHub上还得写一长串复杂的构建说明文档别人用起来依然困难重重。这种“依赖地狱”和“构建碎片化”的问题长期以来一直是C生态中一个令人头疼的痛点。相比之下看看隔壁的Pythonpip、JavaScriptnpm、Rustcargo甚至JavaMaven它们都有一个中心化的包管理仓库和一套标准化的构建、发布、依赖管理流程。开发者只需要一行命令就能把世界各地的优秀代码“安装”到自己的项目中极大地提升了开发效率和协作的便利性。C社区对此的渴望由来已久也催生过Conan、vcpkg等优秀的解决方案。而今天我们要聊的C Archive Network (CPPAN)就是在这个背景下诞生的又一个积极探索者。简单来说CPPAN 的目标是成为一个C的包管理器和中央仓库。你可以把它想象成C的“npm”或“pip”。它的核心愿景是让C开发者能够像使用现代语言一样轻松地发布、发现、安装和管理代码库库、框架、工具等从而把大家从繁琐的依赖管理和环境配置中解放出来真正专注于业务逻辑的实现。对于初学者CPPAN 可以降低学习门槛让你能快速搭建起包含各种流行库的开发环境而不用在环境配置上浪费大量时间。对于有经验的开发者它能提升团队协作和项目复用的效率让库的分享和使用变得标准化。对于整个C生态一个成熟、易用的包管理器是推动社区繁荣、吸引新开发者、促进代码复用的重要基础设施。接下来我们就深入拆解CPPAN的设计思路、核心机制以及如何上手使用。2. CPPAN的核心设计理念与工作机制2.1 核心目标解决C生态的“最后一公里”问题CPPAN 的设计并非凭空而来它直指C包管理中的几个核心痛点构建系统碎片化Autotools、CMake、Meson、Bazel、手写Makefile……五花八门的构建系统让库的编译安装过程千差万别。依赖关系复杂库A依赖库B的特定版本库B又依赖系统上的某个动态库环环相扣手动解决极易出错。跨平台兼容性差在Windows上用MSVC编译的库在Linux上用GCC可能完全无法通过更不用说macOS、嵌入式平台了。发现与分发困难除了少数明星项目很多优秀的C库散落在GitHub、GitLab等各处缺乏一个统一的、易于搜索和评估的入口。CPPAN 试图通过一套统一的规范和工具链来解决这些问题。它的核心思想是标准化和自动化。2.2 核心组件与工作流程一个完整的CPPAN生态主要由三部分组成客户端 (CPPAN Client)这是开发者本地使用的命令行工具。它的主要功能包括cppan install package安装指定的包及其所有依赖。cppan search keyword在中央仓库中搜索包。cppan init为当前项目初始化CPPAN配置创建一个cppan.yml文件。cppan build根据cppan.yml配置自动下载依赖并构建当前项目。cppan publish将配置好的库发布到CPPAN中央仓库。包描述文件 (cppan.yml)这是CPPAN项目的“心脏”是一个YAML格式的配置文件。它定义了关于一个包的所有元数据类似于Node.js的package.json或Rust的Cargo.toml。一个典型的cppan.yml会包含以下关键信息# 包的唯一标识符通常遵循“作者/项目名”的格式 name: myorg/awesome-lib # 版本号遵循语义化版本控制 version: 1.0.0 # 项目的简短描述 description: A high-performance networking library written in modern C. # 许可证信息 license: MIT # 源代码的存放位置通常是Git仓库 source: git: https://github.com/myorg/awesome-lib.git tag: v1.0.0 # 声明本项目的依赖项 dependencies: - org/json-lib: ^3.0.0 # 依赖另一个CPPAN包并指定版本范围 - system: openssl 1.1.1 # 声明对系统库的依赖 # 构建配置这是关键部分告诉CPPAN如何编译你的代码 build: # 指定使用的构建系统CPPAN会调用对应的命令 system: cmake # 可以传递参数给构建系统 options: - -DBUILD_SHARED_LIBSON - -DCMAKE_BUILD_TYPERelease # 定义如何将编译好的文件安装到本地缓存中 install: # 指定哪些头文件需要暴露给其他项目使用 include: include/ # 指定编译生成的库文件路径 lib: build/libawesome.so中央仓库 (CPPAN Registry)这是一个在线的、存储所有已注册包元数据主要是cppan.yml文件和源代码索引的服务器。当用户执行cppan install时客户端会向仓库查询包的元数据然后根据source字段的指引如Git URL去拉取真正的源代码进行构建。仓库本身不存储编译后的二进制文件坚持“源码分发”原则这保证了跨平台的灵活性和安全性。工作流程简述作为使用者你在项目根目录创建cppan.yml声明依赖。运行cppan build客户端会从仓库获取依赖包的元数据下载源码在本地隔离的环境中依次构建所有依赖最后构建你的主项目并将依赖的头文件和库文件链接进来。作为发布者你为自己的库编写cppan.yml运行cppan publish将元数据上传到中央仓库。其他开发者就可以通过你的包名来引用它了。2.3 与Conan、vcpkg的异同CPPAN并非第一个吃螃蟹的人。理解它与其他方案的差异能更好地定位它的价值。Conan这是一个非常成熟、功能强大的去中心化C包管理器。它支持“二进制包”管理可以为不同的设置如编译器、架构、构建类型预编译并缓存二进制包速度极快。Conan的配置conanfile.py更为灵活和强大但学习曲线也相对陡峭。CPPAN 在理念上更接近“简单化”和“标准化”试图通过一个更简洁的YAML文件来降低使用门槛。vcpkg这是微软推出的C库管理器以其庞大的库收录和与Visual Studio的良好集成而闻名。vcpkg的特点是“端口ports”每个库都有一个描述如何获取和构建的端口文件。vcpkg通常是全局安装库而CPPAN和Conan更倾向于项目级依赖管理。vcpkg在Windows平台体验最佳而CPPAN立志于成为跨平台的首选。注意选择哪个工具往往取决于项目需求、团队习惯和目标平台。对于追求极致构建速度和企业级复杂依赖管理的场景Conan可能是更好的选择。对于深度绑定微软生态的Windows开发vcpkg非常方便。而CPPAN则试图在易用性、跨平台和标准化之间找到一个平衡点吸引那些希望快速上手、简化工作流的开发者。3. 从零开始使用CPPAN管理你的第一个C项目理论说了这么多我们来点实际的。假设我们要创建一个简单的控制台应用程序它依赖一个用于命令行参数解析的库比如cxxopts和一个用于单元测试的框架比如Catch2。我们将全程使用CPPAN来管理这些依赖。3.1 环境准备与CPPAN客户端安装首先你需要在你的开发机器上安装CPPAN客户端。由于CPPAN本身是一个C项目它通常也通过源码构建。最直接的方式是从其官方GitHub仓库获取。步骤一获取CPPAN源码并构建# 1. 克隆CPPAN仓库 git clone https://github.com/cppan/cppan-client.git cd cppan-client # 2. 使用CPPAN来构建它自己自举 # 通常项目会提供一个引导脚本 ./bootstrap.sh # 在Linux/macOS上 # 或者 bootstrap.bat 在Windows上 # 3. 构建并安装到系统路径 # 根据引导脚本的提示或README进行通常涉及CMake的配置、编译和安装 mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease cmake --build . --target install安装成功后你应该能在终端中运行cppan --version来验证安装。步骤二初始化你的项目为你的新项目创建一个目录并初始化CPPAN。mkdir my-cppan-demo cd my-cppan-demo cppan init这个命令会在当前目录生成一个初始的cppan.yml文件。让我们来编辑它。3.2 编写核心配置文件cppan.yml打开生成的cppan.yml我们将它修改为我们的项目配置。# my-cppan-demo/cppan.yml name: demo/my-first-app version: 0.1.0 description: A demo application using CPPAN to manage dependencies. # 指定项目使用的C标准 settings: cppstd: 17 # 声明我们的依赖项 # 假设cxxopts和Catch2都已经在CPPAN仓库中注册这里仅为示例实际包名需查询仓库 dependencies: - cxxopts/cxxopts: ^3.0.0 # 命令行解析库 - catchorg/Catch2: ^3.0.0 # 单元测试框架 # 定义如何构建我们的应用程序 build: system: cmake # 我们将在CMakeLists.txt中详细定义目标这里可以留空或设置全局选项 options: - -DCMAKE_BUILD_TYPERelease # 我们的项目没有库需要安装给他人所以install部分可以简单定义或省略 install: # 因为我们只是一个可执行文件项目通常不需要安装头文件或库。 # 但可以定义一个“运行时”组件将可执行文件安装到某个路径如果需要。 # 对于简单的本地开发这部分可以先不配置。关键点解析dependencies部分是我们关注的重点。格式是作者或组织/包名: 版本约束。^3.0.0表示兼容3.0.0及以上但低于4.0.0的版本遵循语义化版本控制。build.system指定为cmake意味着CPPAN在构建时会期望在项目根目录找到一个CMakeLists.txt文件并调用CMake来执行构建。settings.cppstd告诉CPPAN和构建系统本项目要求C17标准。3.3 编写项目源码与CMakeLists.txt接下来我们创建项目的主源文件和CMake构建脚本。1. 创建主程序src/main.cpp// my-cppan-demo/src/main.cpp #include iostream #include cxxopts.hpp // 依赖的头文件CPPAN会确保我们能找到它 #include my_math.h // 我们自己的头文件 int main(int argc, char** argv) { cxxopts::Options options(MyApp, A simple demo app with CPPAN); options.add_options() (n,number, Input number, cxxopts::valueint()-default_value(5)) (h,help, Print help); auto result options.parse(argc, argv); if (result.count(help)) { std::cout options.help() std::endl; return 0; } int num result[number].asint(); std::cout The square of num is square(num) std::endl; return 0; }2. 创建自定义头文件include/my_math.h// my-cppan-demo/include/my_math.h #pragma once int square(int x);3. 创建实现文件src/my_math.cpp// my-cppan-demo/src/my_math.cpp #include my_math.h int square(int x) { return x * x; }4. 创建核心构建脚本CMakeLists.txt这是连接CPPAN和CMake的关键。我们需要在CMake中“导入”CPPAN管理的依赖。# my-cppan-demo/CMakeLists.txt cmake_minimum_required(VERSION 3.15) project(MyCppanDemo LANGUAGES CXX) # 设置C标准 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 关键步骤包含CPPAN提供的CMake脚本。 # 当运行 cppan build 时CPPAN会设置一个环境变量或生成一个CMake文件 # 帮助我们找到依赖。具体方式可能因CPPAN版本而异。 # 一种常见模式是CPPAN会生成一个 cppan.cmake 文件。 if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/cppan.cmake) include(cppan.cmake) endif() # 另一种方式是CPPAN可能通过 find_package 提供目标。 # 我们假设依赖项以CMake包的形式提供。在实际中你需要查阅CPPAN文档来确认具体用法。 # 例如它可能自动执行了类似下面的操作伪代码 # find_package(cxxopts REQUIRED) # find_package(Catch2 REQUIRED) # 添加可执行文件 add_executable(my_app src/main.cpp src/my_math.cpp) # 包含自定义头文件目录 target_include_directories(my_app PRIVATE include) # 链接依赖库。这里假设cxxopts是仅头文件的库Catch2我们暂时不用链接。 # 对于cxxopts通常只需要包含它的头文件路径。 # 如果CPPAN将依赖项的目标target导出为 cppan::cxxopts我们可以这样链接 # target_link_libraries(my_app PRIVATE cppan::cxxopts) # 为了简化演示我们假设CPPAN已经将依赖库的头文件路径添加到了全局的包含路径中。 # 在实际复杂项目中更推荐使用CMake的target模式进行精确依赖管理。实操心得CPPAN与CMake或其他构建系统的集成方式是使用中的关键也是最容易出问题的地方。早期的包管理器可能只是简单地下载源码到本地目录然后期望你的CMakeLists.txt通过add_subdirectory来包含它。现代的方式更倾向于让包管理器生成CMake的find_package所需的配置文件。务必仔细阅读你所使用的CPPAN版本的文档了解它如何向CMake项目暴露依赖项。这可能涉及在cppan.yml中配置exports字段或者在CMakeLists.txt中包含特定的生成文件。3.4 构建与运行现在一切就绪我们可以进行构建了。# 在项目根目录my-cppan-demo/下执行 cppan build这个命令会执行以下操作解析cppan.yml发现依赖cxxopts/cxxopts和catchorg/Catch2。联系CPPAN中央仓库获取这两个包的元数据cppan.yml。根据元数据中的source.git信息克隆或下载这两个库的源代码到本地的CPPAN缓存目录通常位于~/.cppan或类似位置。按照每个依赖包自己的cppan.yml中的build配置在隔离的环境中分别构建它们。对于仅头文件库如cxxopts构建步骤可能只是复制头文件。所有依赖构建并“安装”到CPPAN的本地缓存后开始构建我们的主项目。CPPAN会配置好环境变量如CMAKE_PREFIX_PATH让CMake能够找到刚刚构建好的依赖。最终在我们的项目目录下生成build文件夹或你指定的其他输出目录并在其中生成可执行文件my_app。构建成功后运行程序# 进入构建输出目录通常就是项目根目录下的 build 或 _build 文件夹 cd build ./my_app --number 10 # 输出The square of 10 is 100 ./my_app --help # 输出帮助信息至此我们完成了一个使用CPPAN管理外部依赖的完整微型项目。整个过程的核心在于cppan.yml的编写和构建系统的集成。4. 进阶使用发布你自己的库到CPPAN使用CPPAN管理依赖已经很酷但更大的价值在于分享。让我们看看如何将一个自研的C库发布到CPPAN仓库供全世界的开发者使用。4.1 为你的库创建标准的cppan.yml假设我们有一个名为string-utils的字符串处理工具库代码结构如下string-utils/ ├── include/ │ └── strutils/ │ ├── join.hpp │ └── split.hpp ├── src/ │ ├── join.cpp │ └── split.cpp ├── tests/ │ └── test_basic.cpp ├── CMakeLists.txt └── README.md我们需要为其创建一个完整且规范的cppan.yml。# string-utils/cppan.yml name: yourgithubusername/string-utils # 建议使用GitHub用户名作为命名空间 version: 1.2.0 description: A collection of useful string manipulation utilities for modern C. homepage: https://github.com/yourgithubusername/string-utils license: MIT # 源代码信息 source: git: https://github.com/yourgithubusername/string-utils.git tag: v1.2.0 # 强烈建议与version对应使用Git标签 # 依赖声明这个库可能依赖标准库以外的其他库 dependencies: # 假设我们内部使用了某个特定的转换库 # - someorg/unicode-utils: ^2.0.0 # 构建配置 build: system: cmake # 可以定义一些构建选项比如是否构建测试 options: - -DSTRING_UTILS_BUILD_TESTSOFF # 默认不构建测试节省依赖下载时间 # 安装配置 - 这是告诉CPPAN“什么是我这个库的公共接口”的关键部分 install: # 1. 头文件指定哪些头文件需要暴露给使用者 include: - include/strutils/*.hpp # 将include/strutils下的所有.hpp文件作为公共头文件 # 2. 库文件指定构建后生成的库文件路径。 # CMake通常会将库输出到类似 ${CMAKE_BINARY_DIR}/lib 的目录。 # 我们可以使用通配符或让CPPAN从CMake安装目标中捕获。 # 更推荐的方式是让CMake负责安装CPPAN执行CMake的install步骤。 # 可以在build部分添加一个安装后钩子post-install hook或者依赖CMake的导出功能。 # 导出配置高级功能定义如何让下游的CMake项目通过find_package找到你。 # 这需要你的CMakeLists.txt正确编写了install(EXPORT ...)命令。 exports: cmake: # 假设你的CMake项目导出的目标名称为 strutils::strutils - target: strutils::strutils namespace: strutils # 可选的命名空间关键点解析与注意事项name字段这是包在CPPAN全球仓库中的唯一标识。采用平台用户名/项目名的格式可以很好地避免命名冲突。source.git.tag强烈建议为每个发布版本打上Git标签如v1.2.0。这确保了使用者获取的是确定性的、与版本号对应的源代码而不是某个不断变化的开发分支。install.include这定义了库的公共API边界。只有这里列出的头文件才会被CPPAN安装到使用者的依赖路径中。内部头文件不要放在这里。exports.cmake这是实现高质量集成的关键。它要求你的库的CMakeLists.txt必须按照现代CMake的最佳实践来编写使用install(TARGETS ... EXPORT ...)和install(EXPORT ...)命令来生成并安装一个CMake的包配置文件。这样下游用户就可以在他们的CMakeLists.txt中简单地使用find_package(string-utils REQUIRED)和target_link_libraries(myapp PRIVATE strutils::strutils)体验会非常流畅。4.2 编写支持CPPAN发布的CMakeLists.txt你的库的CMakeLists.txt需要做一些调整来支持被CPPAN很好地管理和被其他项目消费。# string-utils/CMakeLists.txt (部分关键内容) cmake_minimum_required(VERSION 3.15) project(string-utils LANGUAGES CXX VERSION 1.2.0) # ... 添加源码定义库目标等 ... add_library(strutils STATIC src/join.cpp src/split.cpp) # 或SHARED target_include_directories(strutils PUBLIC $BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include $INSTALL_INTERFACE:include # 安装后头文件位于 include/ 目录下 ) target_compile_features(strutils PUBLIC cxx_std_17) # 安装目标将头文件和库文件安装到标准位置 install(TARGETS strutils EXPORT strutils-targets ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION bin # 如果是Windows的DLL ) install(DIRECTORY include/strutils DESTINATION include ) # 生成并安装包的配置文件这是实现find_package的关键 install(EXPORT strutils-targets FILE strutils-config.cmake NAMESPACE strutils:: DESTINATION lib/cmake/strutils ) # 可选生成一个版本文件用于版本兼容性检查 include(CMakePackageConfigHelpers) write_basic_package_version_file( strutils-config-version.cmake VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion # 主版本号相同则兼容 ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/strutils-config-version.cmake DESTINATION lib/cmake/strutils )4.3 发布流程与版本管理当你的库代码稳定cppan.yml和CMakeLists.txt都准备就绪后就可以发布了。本地测试首先在本地彻底测试你的库能否被另一个项目通过CPPAN正确引用和构建。可以创建一个新的测试项目在其cppan.yml的dependencies中添加yourgithubusername/string-utils: 1.2.0或者使用本地路径进行测试然后运行cppan build看是否能成功。提交与打标签将所有的更改包括cppan.yml提交到Git仓库。然后为这个发布版本创建一个Git标签。git add . git commit -m Release version 1.2.0 with CPPAN support git tag -a v1.2.0 -m Version 1.2.0 git push origin main --tags发布到CPPAN仓库使用CPPAN客户端进行发布。cppan publish这个命令会读取当前目录下的cppan.yml验证其有效性并将包的元数据上传到CPPAN中央仓库。请注意你可能需要先拥有一个CPPAN账户并通过cppan login进行认证。版本更新当你修复bug或添加新功能后需要更新cppan.yml中的version字段例如改为1.2.1再次提交、打标签v1.2.1然后运行cppan publish。重要注意事项发布到公共仓库意味着你的代码将对所有人可见。请确保代码许可证license字段清晰明确。不要发布含有敏感信息如密钥、密码的代码。cppan.yml中的依赖声明要准确避免引入不必要的或过大的依赖增加使用者的负担。遵循语义化版本控制SemVer让使用者能安全地更新版本。5. 常见问题、排查技巧与生态现状思考在实际使用任何新的工具链时遇到问题在所难免。以下是一些使用CPPAN可能遇到的典型问题及其解决思路。5.1 依赖解析与构建失败问题现象运行cppan build时在下载或构建某个依赖包时失败提示找不到包、版本冲突或编译错误。排查思路检查包名和版本首先确认cppan.yml中依赖的包名和版本号是否正确。可以到CPPAN的官方网站或使用cppan search keyword命令搜索确认。检查网络与仓库状态确保网络通畅并且CPPAN中央仓库服务可用。有时可能是仓库临时故障或包作者删除了该版本。查看详细日志使用cppan build -v或--verbose选项获取更详细的输出错误信息通常会指出是下载失败、配置错误还是编译错误。编译错误分析如果是编译错误问题可能出在依赖包本身与你的环境不兼容如编译器版本过高/过低缺少系统库。这时需要检查该依赖包的文档看是否有特定的环境要求。尝试在cppan.yml中为该依赖指定不同的版本。在你的项目cppan.yml的settings中可以尝试指定更具体的编译器或构建选项但需谨慎因为这可能影响可移植性。依赖冲突两个不同的依赖包或间接依赖要求同一个包的不同版本。CPPAN的解析器需要解决这个冲突。如果自动解决失败你可能需要手动排除或强制指定某个版本。这通常在cppan.yml的dependencies部分通过更精确的版本约束来实现。5.2 CMake集成问题问题现象依赖包下载构建成功但在构建自己的项目时CMake报告找不到包Could NOT find package ...或者链接错误。排查思路确认CPPAN的集成方式这是最常见的问题根源。你需要清楚CPPAN是如何将依赖信息传递给CMake的。是生成了一个cppan.cmake文件让你去include还是通过设置CMAKE_PREFIX_PATH环境变量查阅CPPAN的最新文档至关重要。检查CMakeLists.txt确保你的CMakeLists.txt正确包含了CPPAN提供的脚本或正确使用了find_package。例如如果CPPAN生成了cppan.cmake你的CMakeLists.txt开头必须包含include(cppan.cmake)。检查依赖包的导出如果你依赖的库比如你自己发布的库没有正确配置exports.cmake和对应的CMake安装规则那么即使它被CPPAN构建和安装了CMake也无法通过find_package找到它。你需要联系该库的维护者或者考虑换用其他提供了良好CMake支持的库。手动检查安装路径可以到CPPAN的本地缓存目录如~/.cppan/packages下查看依赖包是否确实被安装头文件和库文件是否在预期的子目录里。这有助于判断是CPPAN安装的问题还是CMake查找路径的问题。5.3 关于CPPAN生态的现状与挑战CPPAN作为一个相对较新的项目其最大的挑战在于生态建设。包的数量与质量一个包管理器的价值取决于仓库里有多少高质量、维护良好的包。目前CPPAN的包数量远少于Conan和vcpkg。许多流行的C库可能还没有人为其制作CPPAN支持的cppan.yml文件。这就需要社区成员积极贡献。工具链的成熟度与Conan这样经过多年工业级应用打磨的工具相比CPPAN的客户端稳定性、错误处理、文档完整性、与各种IDE的集成等方面可能还存在差距。对于企业级关键项目这可能是需要考虑的风险。社区与支持遇到复杂问题时社区的大小和活跃度决定了你能多快找到答案。Reddit、GitHub Issues和Stack Overflow上关于CPPAN的讨论目前还比较少。给开发者的建议对于个人项目或小型团队如果你对尝试新技术充满热情并且项目依赖的库恰好有CPPAN支持或者你愿意自己为依赖库创建cppan.yml那么CPPAN是一个值得尝试的、简化依赖管理的工具。对于学习和实验通过参与CPPAN你可以深入理解C包管理、构建系统集成、跨平台编译等概念这是一个非常好的学习过程。对于大型或企业级项目在当前阶段可能更需要评估Conan或vcpkg这些更成熟、社区支持更完善的方案。但可以持续关注CPPAN的发展。CPPAN的愿景是美好的它试图为C带来一种更简单、更统一的包管理体验。它的成功与否最终取决于社区是否愿意接受并贡献其中。作为开发者我们可以从为自己的小项目制作一个CPPAN包开始体验其工作流程如果觉得好用就向社区分享一点一滴地共同构建这个生态。毕竟C生态的进步离不开每一个开发者的实践和贡献。
返回列表