arduino-builder:命令行驱动的Arduino编译流水线与CI实践
简介:arduino-builder 是一款面向 Arduino 开发者的命令行编译工具,由 Go 语言实现,用于解析草图、自动生成函数原型、收集库路径并调用 gcc 完成编译。该仓库目前作为 arduino-cli 的包装器存在,官方已建议新项目迁移至 Arduino CLI,但本项目仍保留发布以支持旧用户过渡。压缩包共 12 个文件,以 Go 源码(.go)为主,辅以模块定义(go.mod/go.sum)、协议文件(.proto)及说明文档(README、md、txt),整体仅 53KB,适合快速查阅工具实现与过渡期内核逻辑。资源已有 680 人学习,读者可从中了解 Arduino 构建流水线、命令行参数设计(编译/预处理/导出偏好)以及工具迁移背后的架构调整思路,对研究 Arduino 工具链或维护旧版构建流程的开发者有直接参考价值。
1. 为什么编译 Arduino 草图的人最终都会换到命令行
先聊一个反直觉的结论:在 Arduino IDE 里点“上传”按钮越熟练,你离真正的编译链路就越远。IDE 把板卡参数、库路径和工具链调用全裹在一个黑匣子里,报错就弹一段红色英文,新手只能截图求助。而 arduino-builder 就是那个撕开黑匣子的命令行工具:它直接读 boards.txt、解析 platform.txt、调用 avr-gcc 或 xtensa 交叉编译器,把 .ino 草图编译成可以烧录的 hex 文件。它解决的是 IDE 在无人值守、批量编译、持续集成场景下的无力感。适合谁用?想让固件编译走进脚本和 CI 的嵌入式工程师、要维护多块板卡固件的人、以及被 IDE 启动等待和卡顿反复折磨的开发者。这篇笔记不打算空谈原理,直接告诉你最小命令怎么写、参数怎么调、坑在哪里。
2. 拆解编译链路:arduino-builder 在哪个环节干活
2.1 arduino-builder 不是编译器,它是编译流程的调度器
第一次打开 arduino-builder 的 -verbose 输出时,很多人会被吓一跳:一条简单的 Blink 编译,背后居然有几十条命令。这些命令依次在做四件事:把 .ino 预处理成 .cpp,扫描并拼接所有 .h 头文件路径,编译核心库和用户库的每个 .cpp,最后用链接器合并成 .elf 再转成 .hex。这四件事在 IDE 里是一个整体,在 arduino-builder 里则是串起来的流水线。
这里要建立第一个关键认知:arduino-builder 本身不是一个编译器,它更像一个构建调度器。真正的编译动作全部由外部的 avr-gcc、arm-none-eabi-gcc、xtensa-esp32-elf-gcc 完成,arduino-builder 只负责把所有参数按板卡描述文件拼好,然后按顺序执行。所以它不依赖于 IDE 的图形界面,只依赖硬件平台目录里是否放着对应的工具链。
板卡描述文件有三份,都放在硬件目录下:boards.txt 里有每块板子的名字、芯片型号、时钟频率、烧录协议;platform.txt 里有编译器路径模板和各个编译阶段的命令模板;programmers.txt 里记录烧录器参数。arduino-builder 拿到 fqbn 后,第一件事就是在 boards.txt 里找到对应板卡段,然后把 platform.txt 里的占位符全部替换成真实值,例如把{build.opt.flags}换成-Os -g之类。
排查问题的顺序可以固定下来:先确认 fqbn 在 boards.txt 里存在,再确认硬件目录存在,最后确认工具链路径存在。三个条件都满足,编译才能正常启动。很多人第一次跑失败,不是参数不对,而是某个路径根本不存在——命令行工具不会帮你检查路径合理性,它只会按你给的路径去找,找不到就报错。这也解释了为什么它的报错信息看起来那么“冷”。
2.2 从 IDE 的“上传”按钮到命令行的最小映射
我习惯把 IDE 里的操作当成一组命令行的“翻译练习”:IDE 的“工具 → 开发板 → 选择板卡”就是-fqbn;IDE 的“项目 → 加载库 → 添加 .zip 库”就是把库解压到一个目录,然后用-libraries指过去;IDE 在启动时自动加载的硬件核心目录,对应的是-hardware;IDE 底部日志里一闪而过的那行编译命令,拆开来看就是 arduino-builder 的各个参数。
下面这张映射表帮我省了很多沟通成本,也建议你存一份:
| IDE 操作 | arduino-builder 参数 | 作用 |
|---|---|---|
| 工具 → 开发板 → 选择板卡 | -fqbn/--fqbn | 指定板卡完全限定名 |
| 项目 → 加载库 → 添加 .zip 库 | -libraries/--libraries | 指定第三方库扫描根目录 |
| 文件 → 首选项 → 附加开发板管理器网址 | -hardware/--hardware | 指定硬件平台根目录 |
| 文件 → 首选项 → 显示详细编译输出 | -verbose/--verbose | 打印完整编译命令与输出 |
注意,IDE 里的“附加开发板管理器网址”本身不直接对应-hardware,但它在下载完第三方核心后,会把核心解压到一个硬件目录下,命令行里只要把这个硬件目录指给-hardware就行。有些第三方核心还自带工具链,这些工具链目录也要通过-tools传给 arduino-builder。
这里有一个当初让我困惑很久的细节:-hardware和-tools这两个参数看着差不多,实际分工完全不同。-hardware用来查找板卡描述,只影响“用哪块芯片、哪些编译宏”;-tools用来定位编译器、烧录器这类可执行程序,只影响“调用哪个二进制”。如果你把工具链目录写进了-hardware,arduino-builder 会发现板卡但找不到编译器;写反了则会直接说找不到板卡。
2.3 准备环境与第一条能跑通的最小命令
先准备环境。最简单可靠的做法是:从已安装好的 IDE 目录里,把核心和工具链复制到你自己的工作目录,然后按 arduino-builder 期望的目录结构放好。目录结构大致是这样:
~/arduino-env/ ├── hardware/ │ ├── arduino/ │ │ └── avr/ │ │ ├── boards.txt │ │ └── platform.txt │ └── esp32/ │ └── esp32/ │ ├── boards.txt │ └── platform.txt ├── tools/ │ ├── avr/ │ │ └── bin/ │ │ └── avr-gcc │ └── esp32/ │ └── tools/ └── libraries/ ├── Servo/ ├── LiquidCrystal/ └── ...这一步看着啰嗦,但值得做:IDE 的安装包会把硬件和工具链拆得很散,甚至有多级嵌套,而 arduino-builder 需要的只是一个干净的根目录。你自己维护这个目录之后,出问题的概率会大幅下降,换机器时也只需要打包这一个目录。
最小编译命令如下:
arduino-builder \ -fqbn arduino:avr:uno \ -hardware ~/arduino-env/hardware \ -tools ~/arduino-env/tools \ -libraries ~/arduino-env/libraries \ -build-path /tmp/blink-build \ -verbose \ ~/projects/blink/blink.ino逐行解释:-fqbn arduino:avr:uno指定板卡为 Arduino AVR 平台的 Uno;-hardware指向包含 boards.txt 的硬件根目录;-tools指向工具链所在根目录;-libraries是库扫描根目录;-build-path指定本次构建输出目录;-verbose打开详细输出,第一次跑务必打开。最后一个参数是 .ino 草图文件的完整路径。
跑成功后,到/tmp/blink-build目录里会看到三个关键产物:blink.ino.hex(烧录文件)、blink.ino.elf(带调试信息的可执行文件)以及一堆.o和.d中间文件。拿到 hex 还不够,建议顺手做一步确认:用file命令看 hex 格式,或者用 avrdude 干跑验证。第一步跑通后,要做的就是围绕这条命令加参数、套脚本、接 CI。
3. 核心参数逐个拆解:fqbn、硬件目录、库目录与缓存
3.1 -fqbn:板卡选择从玄学到可查
fqbn 全称 Fully Qualified Board Name,格式是“厂商:架构:板卡名”,中间用英文冒号隔开,例如arduino:avr:uno、arduino:samd:nano_33_iot、esp32:esp32:esp32dev。它看着像玄学,实际上就是一把查字典的 key:boards.txt 里每块板卡的定义都以这个名字开头,后面跟一堆等号配置。
最容易翻车的是带菜单选项的板卡。比如 ESP32 开发板,基础 fqbn 是esp32:esp32:esp32,但你在 IDE 里可能选了“Flash Size 4MB”或者“Partition Scheme”,这些选项在命令行里要拼在 fqbn 后面。写法是菜单项之间用英文逗号连接,菜单名与值之间用等号:
arduino-builder \ -fqbn esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default_4MB \ -hardware ./arduino-env/hardware \ -tools ./arduino-env/tools \ -libraries ./arduino-env/libraries \ -build-path ./build/esp32dev \ ./esp32_mqtt/esp32_mqtt.ino这里FlashSize和PartitionScheme是 boards.txt 里menu.FlashSize与menu.PartitionScheme去掉menu.前缀后的名字。如果你不确定有哪些菜单项,去硬件目录下搜索menu.开头的行就能看到完整列表。fqbn 拼错时,arduino-builder 的报错有点含糊,通常给一句“board not found”或者直接列出一堆可用板卡,但不会指出是哪一段错了。我的排查习惯是先用不带任何菜单项的板卡跑通,比如arduino:avr:uno,再去给 ESP32 加菜单项,一次只加一个,能快速定位是板卡名错还是菜单项错。
3.2 -hardware 与 -tools:第三方平台包的摆放逻辑
-hardware参数指定的是硬件根目录,这个目录下按“厂商/架构”两级组织。比如 Arduino AVR 的目录是arduino/avr,ESP32 的是esp32/esp32,里面放着 boards.txt 和 platform.txt。一个容易被忽略的能力是-hardware可以重复传:一个目录放官方核心,另一个目录放你从网上下载的第三方核心,arduino-builder 会自动合并索引。
arduino-builder \ -fqbn esp32:esp32:esp32 \ -hardware ~/.arduino15/packages \ -hardware ~/my_third_party_cores \ -tools ~/.arduino15/packages \ ...-tools参数则指向工具链所在路径。这里有个坑:当多个硬件目录都包含同名工具链时,arduino-builder 按优先级匹配,但不会主动告诉你哪个目录生效了。我一般会在-verbose输出里搜索编译器路径,确认到底用的是哪套工具链。同一台机器上装过多个 ESP32 核心版本时,这一步尤其重要,否则可能出现“新核心配老编译器”的错位。
还有一个常见简化思路:如果你用的是 IDE 下载的完整包,-hardware和-tools可以直接指向 IDE 自带目录。但要注意 IDE 2.x 和 1.8.x 的目录结构并不一样,有些版本把工具链放在 packages 目录而不是 hardware/tools 目录下,照旧教程写路径就会踩坑。最稳的方案是第二章里说的,自己建一个干净的目录,把核心与工具链放进去,再通过参数指过去,这样版本控制完全掌握在自己手里。
3.3 -libraries:库目录指定对的三件事
-libraries是第三方库的根目录,三点经验值得记:第一,arduino-builder 会递归扫描这个目录下的所有子目录,每个子目录被认为是一个库,所以不要在库目录里堆零散文件;第二,同名库冲突时,解析顺序是核心库优先、用户库其次,但如果你同时指定了多个-libraries目录,目录之间的优先级按参数顺序排列;第三,库依赖可以自动解析,但不代表它能理清任意复杂的依赖图。
编译报No such file or directory #include <xxx.h>时,从三个方向排查:头文件名大小写是否正确、库目录是否真的包含该头文件、以及这个库是否依赖另一个库而你没有把另一个库的目录传进来。库依赖是 arduino-builder 最让新人崩溃的地方,因为它不像 IDE 那样有“库管理器索引”,命令行下这个索引不存在,全靠你自己指路径。
比如你用了 Servo 库,而 Servo 内部依赖了 Arduino 核心的头文件,那只要指定了正确的硬件目录就行。如果你用了某些第三方传感器库,它又依赖别的库,你就得把所有库的根目录都加进-libraries,一次加齐,不要挤牙膏。我习惯在项目里维护一个libs.txt文件,列出当前项目需要的所有库目录,脚本里读出来循环传给-libraries,既清晰又可复现。
3.4 -build-path 与缓存:Windows 编译慢的解决思路
-build-path指定编译输出目录。这个参数不仅决定文件放哪,还决定了增量编译能否生效。arduino-builder 会在构建目录里做文件时间戳对比,改过的.cpp才重新编译,没改的不动。所以如果你每次构建都换一个新的-build-path,那就等于天天全量编译,ESP32 这种大工程每次要一两分钟,Windows 上更明显。
针对 Windows 编译 esp32 速度慢这个老问题,我有一套固定打法:固定同一个-build-path目录,配合一个核心预编译缓存目录,避免每次把 ESP32 核心库重新编译一遍:
arduino-builder \ -fqbn esp32:esp32:esp32 \ -hardware ./arduino-env/hardware \ -tools ./arduino-env/tools \ -libraries ./arduino-env/libraries \ -build-path ./build/esp32dev \ -build-cache ./cache/esp32core \ ./esp32_mqtt/esp32_mqtt.ino-build-cache用来存放核心的预编译产物。第一次编译还会花时间,第二次开始,核心库的.o文件直接命中缓存,编译时间能明显降下来。很多人 Windows 上编译慢,一半原因是杀毒软件实时扫描中间文件,另一半是缓存没配。前者去杀毒软件里把构建目录加入白名单,后者就用-build-cache。
另外提醒一句:-build-path目录如果残留了之前不同板卡或不同 fqbn 的构建产物,有时会干扰编译。最省心的做法是按板卡分目录,比如build/uno和build/esp32dev分开。遇到改了代码但编译结果没变这种“灵异事件”,先清空对应板卡的 build 目录再编译,多半药到病除。
4. 把 arduino-builder 接进自己的构建流程:脚本与持续集成
4.1 用 bash 脚本封装编译、烧录与日志输出
命令行工具的强项在于可脚本化。我一般会写一个build.sh把参数集中管理,避免每次敲一长串命令。一个最简单的封装如下:
#!/bin/bash set -euo pipefail FQBN="${1:-arduino:avr:uno}" SKETCH_DIR="${2:-./firmware}" BUILD_ROOT="./build" mkdir -p "$BUILD_ROOT" arduino-builder \ -fqbn "$FQBN" \ -hardware ./arduino-env/hardware \ -tools ./arduino-env/tools \ -libraries ./arduino-env/libraries \ -build-path "$BUILD_ROOT/$(echo "$FQBN" | tr ':,' '__')" \ -build-cache "$BUILD_ROOT/cache" \ -verbose \ "$SKETCH_DIR" echo "build ok"set -euo pipefail是三个 bash 严格模式开关:-e遇到第一个错误立即退出,-u变量未定义直接报错,-o pipefail让管道命令中任何一个失败都视为整体失败。fqbn 传入后经过tr处理当作构建目录名,这样不同板卡的构建产物天然隔离,不会互相污染。
脚本化之后还可以把烧录也接进来。arduino-builder 本身不做烧录,烧录要用 avrdude 或 esptool,但你可以根据 fqbn 自动选择烧录工具,实现编译完成后自动烧录的单命令流程。比如 AVR 板卡用 avrdude 加-c arduino -p m328p,ESP32 用 esptool.py 的write_flash。这样一块板卡从源码到烧录只需要一行命令,团队新人也能快速上手。日志方面,建议把-verbose输出同时写到构建目录下的build.log里,出问题时直接翻文件,不用人守在那看终端。
4.2 在 GitHub Actions 里跑 arduino-builder 的常见配置
仓库托管在 GitHub 上时,用 Actions 做自动编译验证是很自然的需求。常见做法是每次推送只编译一次,确认改动没有破坏构建。需要特别注意:Actions 运行环境是干净的,你本机的硬件目录和工具链不会自动带上去,所以第一步必须先把依赖装好。
一个典型的 workflow 配置如下:
name: build-firmware on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install AVR core run: | mkdir -p hardware/arduino/avr curl -L <core-download-url> | tar xj -C hardware/arduino/avr --strip-components=1 - name: Build sketch run: | arduino-builder \ -fqbn arduino:avr:uno \ -hardware ./hardware \ -tools ./tools \ -libraries ./libraries \ -build-path ./build这里没有写死具体的下载链接,因为核心包版本更新频繁,我在实际配置里一般会手动锁定某个固定版本,或者用 IDE 安装包解压出核心目录。关键是理解这个结构:先把硬件目录准备好,再调用 arduino-builder。如果 Actions 上报找不到 arduino-builder,说明需要在 run 之前先安装它,或者使用一个预置了工具链的基础镜像。
有人会问:直接装完整 IDE 不就行了吗?理论上可行,但 Actions 里装完整 IDE 不仅慢,还容易踩坑,比如 IDE 自动更新占用的时间和网络资源。把构建流程拆到最小依赖上,是更稳的路。另外,artifact 上传这一步别省:把构建目录里的 hex 文件上传为 CI 产物,这样每次提交都会留下一份可下载的固件,后续烧录测试直接拉取,非常方便。
4.3 交叉编译多块板卡:循环遍历 fqbn 的写法
很多项目不只维护一块板卡,比如同一套逻辑要跑在 Uno、Nano 和 Mega 上。循环遍历 fqbn 是脚本化的常见需求,代价是每次循环都会触发一次完整编译,因为 fqbn 不同,编译目标和宏定义都不同。
FQBNS=( "arduino:avr:uno" "arduino:avr:nano" "arduino:avr:mega2560" ) for fqbn in "${FQBNS[@]}"; do echo "=== Building $fqbn ===" arduino-builder \ -fqbn "$fqbn" \ -hardware ./arduino-env/hardware \ -tools ./arduino-env/tools \ -libraries ./arduino-env/libraries \ -build-path "./build/$(echo "$fqbn" | tr ':,' '__')" \ -build-cache "./cache/$(echo "$fqbn" | tr ':,' '__')" \ "$SKETCH_DIR" done-build-path和-build-cache都用了tr转换后的 fqbn 作为子目录名,如arduino__avr__uno,避免文件系统不认冒号。缓存目录也按板卡分开,避免不同板卡的核心预编译产物互相覆盖。循环里建议加一个失败统计:任何一块板卡编译失败时记录到error.log,但整个循环继续跑完,这样一次提交能暴露所有板卡的问题,而不是卡在第一个失败处。
遍历多板卡时最常被坑的是库兼容性。比如某库依赖 AVR 独有的avr/pgmspace.h,编 Uno 没问题,编其他 AVR 板卡大概率也没问题,但一旦列表里加了 ESP32,同样的库可能直接编译失败。原因是不同板卡实质上是不同的编译器与不同的预定义宏,库头文件里的条件编译没有处理好就会炸。循环编译的价值正是尽早暴露这类兼容问题。
5. 避坑与排查:arduino-builder 编译失败先看这五个地方
5.1 fqbn 拼写不合法,报错却不说清楚是哪里错了
现象:arduino-builder 报board not found,或者直接把所有可用板卡列出来,但就是不告诉你哪一段拼错了。 原因:fqbn 格式不匹配 boards.txt 里的定义,常见的是厂商名写成了 IDE 显示名,架构名写错,或者菜单项拼写与 boards.txt 里menu.后面的字段不一致。 解决:先跑一次最简板卡验证环境,比如arduino:avr:uno;再用 grep 从 boards.txt 里搜板卡实际名字。加菜单项时先只写板卡名不带菜单,跑通后再逐个加上,一次只加一个。这个方法浪费不了几分钟,但能直接把问题定位到具体菜单项上。
5.2 库找不到但 IDE 能编译
现象:同一个工程,IDE 里点编译一切正常,轮到命令行就报一堆#include <xxx.h>找不到。 原因:IDE 的库管理器维护着一个全局库索引,会自动把已安装库全部传给编译流程;命令行下没有这层自动索引,你必须在-libraries里把库目录显式指给 arduino-builder。 解决:从报错的第一个头文件入手,找到它所在库的目录,把父目录加到-libraries。注意如果库目录结构是./libraries/XYZ/src/XYZ.h,-libraries要指向./libraries,而不是./libraries/XYZ/src。如果这个库还依赖别的库,把依赖库的目录也一起加上,不要等报错才补,一次加齐能省下好几轮无意义的失败。
5.3 中文路径与空格路径导致头文件找不到
现象:把 .ino 放在带空格或中文字符的目录下,编译时有时通过有时失败,报错里的头文件路径看起来却是正常的。 原因:编译命令在 shell 拼接时,路径里的空格把参数切成了两段,或者非 ASCII 字符在编译器的 include 路径处理中出现了编码问题。arduino-builder 在内部拼接 include 路径时,对特殊字符的处理远没有 IDE 那么宽容。 解决:把草图、库目录、硬件目录都放在纯英文且无空格的路径下。我在这件事上有过血泪经验:项目叫“智能小车”,在 Windows 上建了个中文文件夹,结果 Arduino 核心库有一半概率编不过去,后来统一改成projects/smart-car,问题再没出现过。如果你用的是 Windows,还要注意盘符路径里的反斜杠尽量换成正斜杠,减少转义带来的意外。
5.4 缓存未命中导致反复全量编译
现象:每次编译都比预期慢很多,ESP32 项目尤其明显,甚至要一分钟以上。 原因:-build-cache目录每次都在变,或者压根没设置,核心库每次都要重新编译;Windows 上还有杀毒软件实时扫描中间文件,进一步拖慢速度。 解决:固定-build-cache目录,同一套核心共用同一个缓存目录;Windows 下把构建目录和缓存目录都加入杀毒软件白名单。另外检查是不是每次用了不同的-build-path,路径只要变一次,时间戳对比就失效,增量编译也就形同虚设。记住一个判断标准:第二次编译同一份代码,耗时应远低于第一次,如果没缩短,缓存链路大概率断了。
5.5 -build-path 残留物导致“改了代码不生效”
现象:明明改了源码,重新编译后 hex 文件的修改时间也在变,但烧进板子里行为还是老样子。 原因:构建目录里残留了旧的对象文件,或者多个板卡共用了同一个 build 目录,旧目标文件被时间戳逻辑错误地判定为“不用更新”。 解决:清空对应板卡的 build 目录后重新编译一次。如果还有疑惑,对比两次-verbose日志中某个.cpp是否真的被编译器调用了,被跳过的话大概率是残留对象文件在作祟。为了避免再次发生,我在前面那句build.sh里加了一个clean函数,一键清空指定板卡的构建目录,操作成本降到几乎为零。
这些坑有个共同规律:绝大多数不是 arduino-builder 本身的问题,而是环境、路径和缓存状态的问题。命令行工具只是把细节暴露出来了,反而给了你排查的抓手。每个报错背后都对应一条可以验证的命令,这比 IDE 的黑匣子好处理多了。
6. 验证编译结果与进阶用法:构建信息提取与缓存预热
6.1 从 build 目录提取固件大小与内存占用
编译完成不等于可以烧录。每次编译后,我都习惯立刻确认固件大小是否在芯片容量之内。AVR 板卡可以用 avr-size 直接读 elf 文件:
avr-size -C --mcu=atmega328p ./build/arduino__avr__uno/blink.ino.elf输出里 program 和 data 两列分别对应该固件的 Flash 与 RAM 占用,对照芯片手册上的容量就能判断余量是否足够。ESP32 项目则可以用 esptool 的 image_info 子命令查看分区与镜像结构。这个验证步骤虽然简单,但能避免烧录后才发现容量超标的尴尬,关键时刻算是一颗后悔药。
6.2 缓存预热:让 CI 和本地编译都快一点
进阶用法是把预编译缓存用到极限。同一颗芯片、同一套 fqbn,在不同项目之间共用同一个-build-cache目录,可以节省大量重复编译核心的时间。尤其当你的项目有多个变体(不同的编译宏、不同的菜单项)时,为每个变体建一个独立缓存目录,分别预热,CI 总耗时能显著下降。
我在 Makefile 里加了一个prewarm目标:第一次全量编译后保留核心缓存目录,后续构建只重新编译业务代码。实测对一个中等规模的 ESP32 工程,缓存预热后编译时间能缩短一半以上。配合-verbose输出观察哪一步还在耗时,就能知道自己有没有把缓存配到位。
6.3 一个收尾习惯
最后分享一个让我少熬夜的习惯:每次改完构建脚本,第一件事不是急着编译,而是用-dump-prefs或者等价的信息导出参数,把这次构建用的所有变量、路径、板卡描述导出来存档。这样下次别人——包括两周后的我自己——拿到构建结果,能快速还原出“这个 hex 是用哪套核心、哪个编译选项做出来的”。命令行编译工具的好处在于一切都是显式的:参数可以查,日志可以翻,目录可以清。熬过最开始的环境配置阶段,arduino-builder 会比 IDE 更让你省心,希望帮到你。
本文还有配套的精品资源,点击获取