OpenHarmony 编译系统:架构、隔离与实战

基于 build/build/templates/cxx/cxx.gnibuild/hb/services/preloader.pybuild/ohos.gni 源码

目录

术语

缩写 说明
GN Generate Ninja,Google 元构建系统
Ninja 轻量构建执行引擎
hb HarmonyOS Build,顶层 Python CLI
declare_args GN 中声明可覆盖变量的语法
inner_kits 部件向外部暴露的接口白名单
preloader 前置处理:解析产品配置
forward_variables_from GN 模板中将调用者变量传递到子目标的语法
public_deps 传递性依赖(依赖方的消费者自动获得该依赖)

1. 架构设计

1.1 为什么是 GN + Ninja 而不是 Make 或 Bazel

OHOS 的构建框架来自 Google Chromium 项目的 GN + Ninja 组合。

选项 为什么不用
GNU Make 隐式规则混乱,增量编译慢(扫描全目录),并行不可靠。一个 include 错了就全体重编
CMake 交叉编译配置繁琐,需要写 toolchain file,且 CMake 本身生成 Makefile,绕不开 Make 的缺点
Soong (Blueprint) Android 专用,深度绑定 Android build environment。OHOS 不能直接用
Bazel 功能最全但部署最重——需要独立 daemon、远程缓存、严格的 hermetic build。OHOS 的嵌入式场景没那么复杂

GN 的哲学很纯粹:不编译,只生成。 它把 BUILD.gn 解析成 build.ninja 后就不管了,Ninja 只负责执行编译命令。这种分离让每层都能独立优化。

1.2 三层引擎的分工

hb (Python CLI)          ← 产品配置、参数解析、依赖预校验
  ↓
GN (元构建)               ← BUILD.gn → build.ninja
  ↓
Ninja (执行引擎)          ← 编译、安装、镜像制作

hbbuild/hb/main.py)做配置解析和编排,不参与编译决策。核心代码在 preloader.py——解析 JSON 配置、检测依赖环、聚合 features(preloader.py:87-112)。

GN 做模板展开。每个 ohos_executable 被展开为多个内部目标(cxx.gni:38)。主要目标包括:

  • __check —— 编译时校验 external_deps 是否在目标部件的 inner_kits 中
  • __collect —— 收集模块元数据(供 install 使用)
  • __link —— 真正的 shared_library / executable 编译规则
  • 辅助目标:__notice(开源协议收集)、_info(模块信息)

另外还有 ohos_source_set 模板——它不产生 .so.a 文件,只是把源码组织为编译单元,供其他目标引用。适合用作内部模块分组。

Ninja 只做一件事:调度执行编译命令。它不知道 C++ 是什么,只运行 build.ninja 中的命令行。

1.3 四层配置链

① subsystem_config.json     → 子系统↔路径映射
② config.json              → 产品选型 + 部件列表 + 特性开关
③ bundle.json              → 部件定义 + inner_kits + features + deps.components
④ BUILD.gn                 → 模块编译规则 + deps + external_deps

Preloader 逐层向下解析,输出 out/preloader/{product}/ 下的中间文件。GN 通过 exec_script 加载这些文件——JSON(Preloader 产出)是 Python 和 GN 之间的协议格式。

1.4 Features 机制

Features 让产品配置直接影响代码编译,却不需要产品团队写一行 GN:

bundle.json → { "features": { "enable_ble": true } }   ← 部件定义其可选特性
    ↓ 产品可覆盖
config.json → { "features": ["enable_ble=true"] }       ← 产品选择开/关
    ↓ Preloader 聚合
build_gnargs.prop → "enable_ble=true"                   ← 关键值格式
    ↓ GN declare_args() 接收
BUILD.gn 中 if (defined(enable_ble)) { sources += [ "ble.c" ] }

这里用 defined() 而不是 if (enable_ble)defined() 在 GN 中检查"是否被赋值过"(包括被命令行或产品配置设置),而 if (enable_ble) 只检查布尔值是否为真。前者区分"产品显式选了"和"用了默认值"。

2. GN 语法速查与常见编译单元

2.1 GN 基础语法

OHOS 的 BUILD.gn 使用 GN 语法。以下是阅读源码时最常见的 6 个语法结构:

import():导入 .gni 头文件

import("//build/ohos.gni")          # 导入 OHOS 模板(所有 BUILD.gn 几乎都以此开头)
import("//build/config/ohos/config.gni")  # 导入 OHOS 平台配置

declare_args():声明可覆盖变量

declare_args() {
  enable_feature_x = false           # 默认值,可被产品配置或命令行覆盖
  my_string = "default"
}

declare_args() 可以多次调用。后调用的覆盖先调用的同名变量。

if/else:条件编译

if (is_standard_system) {
  sources += [ "tcp_channel.c" ]
} else if (is_small_system) {
  sources += [ "br_channel.c" ]
}

if (defined(enable_ble)) {           # ← 注意:用 defined() 检查是否被设置过
  sources += [ "ble_manager.c" ]
}

④ 标签引用://path:target

# // 开头表示源码根目录
# : 后面是目标名
deps = [
  "//foundation/communication/dsoftbus/core/common:softbus_common",
  ":local_sub_target",                # 当前 BUILD.gn 中的目标
]

forward_variables_from():模板中的变量透传

template("ohos_my_template") {
  forward_variables_from(invoker, "*", [ "part_name", "install_images" ])
  # 将调用者传入的所有变量转发给子目标(排除 part_name 和 install_images)
}

configs / public_configs:编译器标志管理

config("my_cflags") {
  cflags = [ "-Wall", "-O2" ]
  defines = [ "MY_DEFINE" ]
}

ohos_shared_library("foo") {
  configs += [ ":my_cflags" ]         # 仅本目标生效
  public_configs = [ ":my_api" ]      # 依赖本目标的目标也会继承
}

2.2 常用编译单元(模板)

以下为 OHOS 源码中出现频率最高的编译模板(基于 foundation/ + applications/ 下 BUILD.gn 的统计):

模板 出现次数 用途 是否安装
ohos_shared_library 1618 动态库(.so),核心编译产物 默认安装到 system/lib64/
ohos_prebuilt_etc 695 预置配置文件(JSON/XML/conf),直接复制到镜像 安装
ohos_source_set 391 编译单元(只编译不链接,不产生产物) 不安装
ohos_static_library 296 静态库(.a),链接到动态库或可执行文件 不直接安装
ohos_executable 170 可执行文件(bin) 默认不安装
ohos_hap 80 系统应用包(HAP) 安装到 system/app/
ohos_fuzztest Fuzz 测试用例(测试专用) 不安装

ohos_shared_library(最常用)

ohos_shared_library("softbus") {
  sources = [ "bus_center.c", "discovery.c" ]
  include_dirs = [ "include", "//foundation/communication/dsoftbus/interfaces" ]
  deps = [ ":common" ]
  external_deps = [ "hilog:libhilog" ]
  part_name = "dsoftbus"
  relative_install_dir = "module/dsoftbus"   # 安装到 system/lib64/module/dsoftbus/
}

ohos_prebuilt_etc(配置文件)

ohos_prebuilt_etc("launcher_config") {
  source = "launcher_config.json"            # 源文件路径
  module_install_dir = "etc/launcher"         # 安装到 system/etc/launcher/
  part_name = "launcher"
}

全系统有 695 个这样的配置描述文件——从 SELinux 策略到 init 启动脚本,都在用这个模板部署。

ohos_source_set(编译分组)

ohos_source_set("common_utils") {
  sources = [ "string_util.c", "buffer_util.c" ]
  deps = [ "//utils/common:utils" ]
  part_name = "dsoftbus"
}

ohos_source_set 不产生产物,只把源码组织为编译单元。它会被 ohos_shared_libraryohos_executable 通过 deps 引用,引用的源码会被编译到最终产物中。

ohos_executable(可执行文件)

ohos_executable("init") {
  sources = [ "init.cpp" ]
  deps = [ "//base/startup/init:init_utils" ]
  part_name = "init"
  install_enable = true                      # ← 重要:默认不安装,需要显式声明
  install_images = [ "system" ]
  relative_install_dir = "bin"
}

2.3 构建系统目录结构

build/                           # 编译构建子系统根目录
├── hb/                          # hb 工具(Python 源码)
│   ├── main.py                  # CLI 入口
│   ├── services/preloader.py    # Preloader:配置解析 + 依赖校验 + features 聚合
│   └── modules/                 # 命令模块(build/set/clean/env)
├── config/                      # GN 配置
│   ├── BUILDCONFIG.gn           # GN 全局配置(1195 行,核心文件)
│   ├── ohos/config.gni          # OHOS 平台配置(CPU/ABI/工具链路径)
│   ├── c++/                     # C++ 编译选项
│   ├── compiler/                # 编译器选项(优化等级、sanitizer)
│   └── clang/                   # Clang 工具链配置
├── templates/                   # GN 模板定义
│   └── cxx/cxx.gni             # ohos_executable / ohos_shared_library 模板(2186 行)
├── ohos/                        # OHOS 专属构建逻辑
│   ├── app/app.gni             # ohos_hap 模板(855 行)
│   ├── images/build_image.py   # 镜像制作入口
│   ├── packages/modules_install.py  # install 阶段脚本
│   └── sdk/                    # SDK 编译配置
├── ohos.gni                     # 模板汇总入口(import 了所有模板)
├── ohos_var.gni                 # GN 全局变量声明(declare_args,536 行)
└── common.gni                   # 通用 default 变量

2.4 标签命名惯例

模式 示例 说明
//subsystem:target //utils/common:utils 子系统根目录下的目标
//subsystem/part:target //foundation/communication/dsoftbus:softbus 部件根目标
:target :common_utils 当前 BUILD.gn 内的目标
//third_party/lib:a //third_party/zlib:zlib 三方库
part_name:output dsoftbus:softbus external_deps 格式

3. 编译维度与选项

OHOS 的编译配置分布在 5 个维度:

① 系统类型(mini / small / standard)
   影响:工具链、内核选择、模块粒度
   设置方式:config.json 的 type 字段

② 构建变体(user / userdebug / eng)
   影响:调试符号、日志等级、root 权限
   设置方式:--build-variant 参数

③ 产品配置(config.json)
   影响:部件列表、CPU 架构、特性开关
   设置方式:vendor/{厂商}/{产品}/config.json

④ 部件特性(bundle.json features)
   影响:单个部件的条件编译
   设置方式:部件 bundle.json 定义默认值,config.json 覆盖
   ↑③和④的关系:部件声明默认值 / 产品决定最终值

⑤ GN 参数(--gn-args → gn gen --args=)
   影响:任意 declare_args()
   优先级:命令行直接传参 > hb --gn-args > Preloader 产出 > GNI 默认值

注意第⑤项的优先级链:gn gen --args="key=val"(在 hb 之外直接调用 gn)的优先级高于 hb build --gn-args "key=val",因为后者是通过 hb 间接传递给 gn 的。

4. 不同级别举例

4.1 系统类型

系统类型通过 config.jsontype 字段设定,转化为 GN 的三个互斥变量:

# BUILDCONFIG.gn:257-265
if (is_mini_system) { os_level = "mini" }
if (is_small_system) { os_level = "small" }
if (is_standard_system) { os_level = "standard" }

同一个模块可以用 if (is_standard_system) 条件编译不同实现:

# 以 dsoftbus 为例
ohos_shared_library("softbus") {
  sources = [ "bus_center.c", "discovery.c" ]
  if (is_standard_system) {
    sources += [
      "transmission/tcp_channel.c",   # 标准系统用 TCP
      "transmission/udp_negotiation.c",
    ]
  } else {
    sources += [
      "transmission/br_channel.c",    # 小型系统用蓝牙
    ]
  }
}

典型配置:

// 手机产品
{ "product_name": "ohos",   "type": "standard", "target_cpu": "arm64" }

// 手表产品
{ "product_name": "watch",  "type": "small",    "target_cpu": "arm" }

// 传感器
{ "product_name": "sensor", "type": "mini",     "target_cpu": "arm" }

4.2 构建变体

OHOS 只有 2 个有效构建变体buildargs.json:456-458),与 Android 的 user/userdebug/eng 完全不同:

"build_variant": { "argDefault": "root", "arg_attribute": { "optional": ["user", "root"] } }

它在 GN 层面的默认值是 rootbuild/common.gni:15),作用于编译和打包两个阶段

编译阶段的影响(GN 条件编译):

  • Rust 标准库root 变体额外编译 Rust libtest.dylib.so,用于单元测试(build/rust/BUILD.gn:18
  • seccomp 策略root 变体使用宽松的 seccomp 过滤规则,允许更多系统调用,方便调试(build/.../seccomp_policy_fixer.gni:135
# build/rust/BUILD.gn:18
if (build_variant == "root") {
  deps += [ ":libtest.dylib.so" ]   # root 变体才编译测试库
}

镜像打包阶段的影响:

build_variant 被传递到 build_image.py(通过 images/BUILD.gn:331),决定是否生成 eng_system 分区:

# build/ohos/images/build_image.py:92-97
def _prepare_eng_ststem(eng_system_path: str, build_variant: str):
    if build_variant == "user":
        return  # user 变体跳过 eng_system 分区
    # root 变体:复制 debug init、selinux 宽松 policy、调试工具

eng_system 是一个独立分区,包含调试工具、宽松的 SELinux 策略、非 release 的 init 参数。user 变体直接跳过它。

变体 编译 Rust libtest seccomp 策略 eng_system 分区 适用场景
root (默认) 编译 宽松 包含 开发板、调试
user 不编译 严格 跳过 量产固件
# 默认 root
hb build --product-name ohos

# 发布版本
hb build --product-name ohos --build-variant user

4.3 产品配置——以 dsoftbus 为例

dsoftbus 部件在 bundle.json 中定义可选特性:

{
  "component": {
    "name": "dsoftbus",
    "features": {
      "enable_ble": true,
      "enable_wifi_direct": false,
      "enable_tcp": true
    }
  }
}

手机产品打开全部,手表产品只开蓝牙:

// 手机 config.json
{ "component": "dsoftbus", "features": [
    "enable_ble=true",
    "enable_wifi_direct=true",
    "enable_tcp=true"
]}

// 手表 config.json
{ "component": "dsoftbus", "features": [
    "enable_ble=true",
    "enable_wifi_direct=false",
    "enable_tcp=false"
]}

4.4 GN 参数命令行覆盖

# 覆盖硬件加速开关
hb build --gn-args "enable_gpu=true enable_mesa3d=true"

# 查看所有 GN 变量的当前值
gn args out/ohos --list

# 只做 GN 解析(调试用)
hb build --build-only-gn

4.5 编译目标范围

# 只编一个模块(秒级)
hb build -T //foundation/communication/dsoftbus:softbus

# 只编一个部件(分钟级)
hb build -T dsoftbus

# 全量编译(小时级)
hb build

系统类型、构建变体、产品配置、GN 参数四者叠加作用——同一个 dsoftbus 模块,在 standard+userdebug 下编译 TCP 支持 + 调试符号,在 small+user 下只编译蓝牙支持 + 全优化。

5. 不同部件之间的依赖

5.1 两种依赖形式

┌──────────────────────────────────┐
│  部件 A                          │
│  ┌────────┐  ┌────────┐         │
│  │ mod_A1 ├──┤ mod_A2 │         │
│  └───┬────┘  └────────┘         │
│      │ deps                      │
│      ├──────────────────┐        │
│      ▼                  │        │
│  ┌────────┐             │        │
│  │ mod_A3 │             │        │
│  └────────┘             │        │
└─────────────────────────┼────────┘
                          │ external_deps
                          ▼
┌──────────────────────────────────┐
│  部件 B                          │
│  ┌──────────────┐               │
│  │ libhilog     │  ← inner_kits │
│  └──────────────┘               │
│  ┌──────────────────┐           │
│  │ hilog_internal.c │  ← 不可见 │
│  └──────────────────┘           │
└──────────────────────────────────┘
deps external_deps public_deps
访问范围 同部件任意模块 跨部件(限 inner_kits) 同部件/跨部件
传递性 不传递 不传递 传递:如果 A public_dep B,A 的消费者自动获得 B
举例 "//common:utils" "hilog:libhilog" "//common:api_headers"

public_deps 的传递性在 cxx.gni:87-94 中被捕获:

if (defined(invoker.public_deps)) {
  module_deps += invoker.public_deps      # 也加入校验
}

如果 A 依赖 B,B 依赖 C,且 B 的依赖声明为 public_deps,则 A 也能直接引用 C 的头文件。这用于"接口库"场景——A 只需要知道 B,不需要知道 B 内部使用了 C。

5.2 inner_kits——编译时接口契约

以 bundle_framework 为例:

{
  "component": {
    "name": "bundle_framework",
    "build": {
      "inner_kits": [
        { "name": "//foundation/bundlemanager/...:libappexecfwk" }
      ],
      "sub_component": [
        "//foundation/bundlemanager/bundle_framework:target",
        "//foundation/bundlemanager/...:libappexecfwk_icon"
      ]
    }
  }
}

launcher 只能引用 libappexecfwk(在 inner_kits 中),不能引用 libappexecfwk_icon(不在白名单中)。

两个校验阶段(以下报错为示例格式,信息与实际编译输出类似):

Preloader(运行在 GN 前):

[OHOS ERROR] part "launcher" uses external_dep
"bundle_framework:libappexecfwk_icon",
but this module is NOT declared in bundle_framework's inner_kits.
Available inner_kits from bundle_framework:
  - bundle_framework:libappexecfwk

GN check_target(展开 BUILD.gn 时):

ERROR at //launcher/BUILD.gn:12
Target "launcher_service" depends on 
"bundle_framework:libappexecfwk_icon"
which is not in inner_kits of bundle_framework.
  See //foundation/bundlemanager/.../bundle.json

Preloader 报错在 GN 之前,更快发现。GN 报错精确到文件行号。

5.3 产品遗漏依赖的情况

如果 config.json 选了部件 A 但没有选 B(A 依赖 B),Preloader 输出:

[OHOS ERROR] component "launcher" depends on "bundle_framework",
but "bundle_framework" is NOT included in product "ohos".
Please check: vendor/xxx/ohos/config.json

依赖从 bundle.json 的 deps.components 推导(部件级),而非从 BUILD.gn 的 external_deps 推导(模块级)。前者在产品配置层面校验,后者在编译层面校验。

6. 产品(系统应用)与镜像编译是否分离

6.1 独立编译与全量编译的区别

全量编译(不分离):

hb build --product-name ohos
# 全部部件 → install → system.img / vendor.img

独立编译(分离):

# 仅编译 launcher 部件
hb build -T Launcher --product-name ohos
# 产物:out/ohos/indep/Launcher.hap(含 native so,但不进镜像)

独立编译的产物不在 packages/phone/ 下,不打包到镜像中。

半分离(只更新镜像,不重新编译):

ninja -C out/ohos install        # 重新收集产物
ninja -C out/ohos build_image    # 只打包镜像

6.2 系统应用的 HAP 打包

以 Launcher 为例,ohos_hap 模板(build/ohos/app/app.gni:580)的处理阶段:

阶段 操作 产物
1 compile_js_assets ets → .abc 字节码
2 compile_resources 资源压缩
3 打包 ZIP Launcher.hap
4 install system/app/com.ohos.launcher/
5 build_image system.img 对应目录

ohos_hap 规则实例:

ohos_hap("Launcher") {
  hap_profile = "./src/main/module.json"
  js_assets = [ "./src/main/ets/" ]
  resources = [ "./src/main/resources/" ]
  part_name = "launcher"
  module_install_dir = "app/com.ohos.launcher"
  deps = [ ":Launcher_binary" ]
}

7. OHOS 编译架构的优缺点

优点

① 编译隔离是编译时强制执行的。 其他系统的模块隔离靠 code review 和团队口头约定。OHOS 用 external_deps / inner_kits 白名单机制,在编译阶段就阻止跨部件引用未公开接口。在多团队协作时——bundle_framework 团队改内部实现,launcher 团队不需要改任何代码。

② 产品配置改 JSON,不需要碰 GN。 产品团队只需要在 config.json 中列部件名、开特性。不涉及 GN 语法。编译系统团队和产品团队之间有清晰的契约边界。

③ 模板展开可追踪。 每个 ohos_executable 展开为可预期的内部目标集合。可以通过 gn gen --ide=json 导出完整的展开结构。

④ 编译与打包分离。 独立编译只产生 .so,全量编译才生成镜像。修改一个 ArkTS 文件不需要重新打包镜像——hdc push 即可验证。

缺点及缓解

① declare_args 的覆盖顺序不直观。 GN 约定"后定义优先",但开发者容易在 ohos_var.gni 中改默认值却没生效(被 Preloader 产出覆盖)。

缓解: 改 GNI 不生效时,先查 out/preloader/{product}/build_gnargs.prop 确认变量来源。在 config.json 的 features 中改,或用 --gn-args 直接覆盖。

② Preloader 的 features 聚合用了 dict.update() 不同部件同名 feature,后遍历者覆盖前者——隐式命名空间冲突。

缓解: feature 命名加部件前缀,如 dsoftbus_enable_ble 而非 enable_ble

③ GN 的 exec_script 是同步的且每次 gn gen 都执行。 但 GN 对相同脚本+相同参数有缓存机制,不会对同一脚本重复执行。对于大产品(几十个部件),exec_script 的执行总时间仍然会累加。

缓解: 该成本是 GN 架构决定的,无法避免。在合理规模下(< 100 个部件)影响可接受。

④ 双层校验(Preloader + GN check_target)有代码重复。 依赖校验在 Python 层和 GN 层各做一次。

缓解: 两者报错时机不同(Preloader 更快,GN 更精确),设计上互补而非完全冗余。出现不一致时优先信任 GN 的报错(因为它有完整标签信息)。

8. 编译注意事项

8.1 ohos_var.gni 的改动无效

最常见的问题。原因在 GN 的 declare_args 覆盖顺序:

# ohos_var.gni(较早加载)
declare_args() { build_ark = true }

# BUILDCONFIG.gn(更晚加载,通过 exec_script 加载了 Preloader 产出)
# → build_ark = false(覆盖了 GNI 的默认值)

解决:out/preloader/{product}/build_gnargs.prop → 有该变量则在 config.json 的 features 中改 → 或用 --gn-args 命令行覆盖。

8.2 同名的 feature 定义冲突

# preloader.py:92
for vals in self._all_parts.items():
    all_features.update(vals.get('features', {}))  # 后覆盖前

解决: feature 命名加部件前缀,如 dsoftbus_enable_ble

8.3 ohos_executable 默认不安装

ohos_executable 缺省 install_enable = falsecxx.gni 中定义):

ohos_executable("test_tool") {
  install_enable = true          # 显式声明
  install_images = [ "system" ]
  relative_install_dir = "bin"
}

动态库(ohos_shared_library)的行为不同——默认安装到 system/lib/

8.4 独立编译产物不参与镜像(§5.1 已详述)

hb build -T 的产物在 out/{product}/indep/ 下,不在镜像中。推送开发板:

hdc shell mount -o rw,remount /
hdc push out/ohos/indep/Launcher.hap /system/app/com.ohos.launcher/

8.5 镜像重打包顺序

ninja -C out/ohos install        # 先确保产物最新
ninja -C out/ohos build_image    # 再打包(否则 packages/ 里是旧文件)

8.6 GN exec_script 调试

gn gen 卡住或用奇怪的错误时:

hb build --build-only-gn --verbose
# 或直接调用 gn
gn gen out/ohos --script-executable=python3 --verbose

9. 总结

9.1 核心链路

config.json → Preloader (聚合 features + 校验依赖)
           → build_gnargs.prop
           → GN exec_script → declare_args
           → BUILD.gn → ohos_executable (展开 check/collect/link)
           → build.ninja
           → Ninja (编译 + install + build_image)
           → system.img

9.2 关键设计

概念 目的
四层配置链 每层只解决自己问题域:子系统路径、产品选型、部件定义、模块规则
deps / external_deps / public_deps 同部件自由引用 / 跨部件白名单 / 传递性依赖
Features → defined() 产品配置直接驱动条件编译,不碰 GN 语法
Preloader + GN 双层校验 越快发现问题越好:Python 层毫秒级检出,GN 层精确到行号
独立编译 + image 分离 开发调试不断开镜像打包,hdc push 秒级验证
declare_args 后定义优先 命令行 > Preloader 产出 > GNI 默认值

9.3 注意点速查

场景 问题 解决
改了 GNI 但编译没变 declare_args 被 Preloader 覆盖 build_gnargs.prop,改 config.json 或用 --gn-args
feature 不生效 另一部件同名 feature 覆盖 加部件名前缀,如 dsoftbus_enable_ble
可执行文件没进镜像 ohos_executable 默认不安装 install_enable = true
独立编译产物重启消失 dm-verity 恢复 hdc shell mount -o rw,remount /
镜像没更新 改了 HAP 但没重新 install ninja installninja build_image
gn gen 卡住 exec_script 异常 --verbose 查看执行路径
posted @ 2026-06-03 17:59  getmoon  阅读(52)  评论(0)    收藏  举报