资讯动态

U-Boot Kbuild构建机制深度解析:从Kconfig到镜像生成

发布时间:2026/10/9 1:15:19 来源:尧图企业网站定制
1. 为什么U-Boot移植第一步不是改板级代码而是读懂Kbuild的“呼吸节奏”刚接触U-Boot移植的人十有八九会直接打开board/rockchip/rv1106/目录对着rv1106_evb_defconfig和Kconfig文件一顿猛改结果编译报错“make: *** No targets specified and no makefile found.”——这句话不是警告是系统在用最直白的方式告诉你你连U-Boot构建系统的“呼吸节奏”都没摸清就急着给它做心肺复苏。我第一次在RV1106平台上移植U-Boot时就是这么栽的。当时以为只要把芯片手册里寄存器地址填进board_init_f()函数、把DDR初始化时序写对就能跑起来。结果make rv1106_evb_defconfig能过make -j4却卡在scripts/Makefile.build:44: *** missing separator. Stop.。查了三天才发现问题出在arch/arm/mach-rockchip/Kconfig里一行缩进用了空格而非Tab——而Kbuild对缩进的敏感程度堪比外科医生对手术刀无菌状态的要求。Kbuild不是Makefile的语法糖它是U-Boot构建体系的中枢神经。它不处理具体硬件逻辑但决定哪段逻辑该被编译、以什么顺序被链接、哪些头文件路径必须提前注入、哪些配置项会触发条件编译开关。它像一个精密的交通调度中心Kconfig定义路口信号灯CONFIG_XXX选项是否启用Makefile划定主干道与支路obj-y / obj-$(CONFIG_XXX)而Kbuild本身则是那套实时解析规则、动态生成依赖关系、并确保所有车辆目标文件按既定路线汇入最终镜像u-boot.bin的智能调度算法。这解释了为什么网络热搜里“make没有指明目标并且找不到makefile”高居榜首——新手常误以为U-Boot根目录下的Makefile是唯一入口却不知道Kbuild会在编译前根据.config文件内容递归扫描所有Kbuild文件注意不是Makefile动态拼接出完整的构建指令链。当你执行make时它实际运行的是scripts/Makefile.build这个“元构建脚本”而你看到的Makefile只是它的参数来源之一。更关键的是Kbuild与GNU Make的关系不是替代而是深度嵌套。U-Boot的Makefile里大量使用$(MAKE) -f scripts/Makefile.build objxxx这类调用这意味着每一次子目录编译都是GNU Make启动一个新进程加载scripts/Makefile.build再由它读取当前目录的Kbuild文件来决定编译行为。这种设计让U-Boot能支持数千种板卡配置却也让调试变得异常隐蔽——错误可能发生在第7层递归调用中而终端只显示顶层的“Stop”。所以U-Boot移植的真正起点从来不是写C代码而是理解Kbuild如何将Kconfig里的布尔开关翻译成Makefile中的obj-y列表再驱动GNU Make完成千行代码的精准编译。这一步没走稳后面所有硬件适配都像在流沙上盖楼。接下来我们就从RV1106这个典型平台切入一层层剥开Kbuild的外壳看它如何把一堆零散的.c、.S、.dts文件拧成一颗可烧录的u-boot-dtb.bin。2. Kbuild三件套解剖Kconfig、Kbuild、Makefile如何协同完成一次“精准投喂”U-Boot的构建系统常被笼统称为“Kbuild”但严格来说它是由三个核心文件协同工作的有机体Kconfig负责“决策”Kbuild负责“分发”Makefile负责“统筹”。它们共同构成一个闭环确保编译器只看到它该看到的代码链接器只链接它该链接的目标。下面以RV1106平台为例拆解这个闭环如何运转。2.1 Kconfig硬件能力的“宪法性文件”决定哪些模块有权被编译Kconfig不是配置文件而是一套声明式语言。它不存储具体值只定义配置项的类型、依赖关系和用户界面提示。在arch/arm/mach-rockchip/Kconfig中你会看到这样的结构config ROCKCHIP_RV1106 bool Rockchip RV1106 SoC select ARCH_ROCKCHIP select CPU_V7A help Enable support for Rockchip RV1106 SoC.这段代码的含义远不止字面bool表示这是一个开关型配置select ARCH_ROCKCHIP意味着一旦启用ROCKCHIP_RV1106ARCH_ROCKCHIP这个父配置项将自动被选中无需用户手动勾选help块则是在make menuconfig界面中显示的说明文字。最关键的是ROCKCHIP_RV1106这个符号将成为后续所有条件编译的基石。再看板级配置board/rockchip/rv1106/Kconfigconfig TARGET_RV1106_EVB bool RV1106 EVB board depends on ROCKCHIP_RV1106 select DM select DM_GPIO select DM_I2C help Support for Rockchip RV1106 Evaluation Board.这里depends on ROCKCHIP_RV1106建立了强依赖如果ROCKCHIP_RV1106未启用TARGET_RV1106_EVB根本不会出现在菜单里。而select DM_*则自动启用设备树驱动框架Driver Model及其GPIO、I2C子系统——这些select语句正是Kbuild实现“配置即依赖”的核心机制。当执行make rv1106_evb_defconfig时U-Boot的conf工具会扫描所有Kconfig文件根据defconfig中预设的CONFIG_TARGET_RV1106_EVBy递归解析其依赖链最终生成.config文件。这个文件里每一行CONFIG_XXXy或CONFIG_XXXm都是Kbuild后续工作的“圣旨”。提示.config文件里出现# CONFIG_XXX is not set不等于CONFIG_XXXn。Kbuild只认y、m、n三种状态#开头的注释行会被完全忽略。这是新手常踩的坑——以为注释掉某行就禁用了功能实则该功能仍可能被其他select语句激活。2.2 Kbuild构建规则的“执行法官”决定每个目录编译什么如果说Kconfig是立法者那么Kbuild就是执法者。它存在于每个需要参与构建的目录下如drivers/gpio/、arch/arm/cpu/armv7/文件名就是Kbuild注意不是Makefile。它的语法极其精简只有两条核心指令obj-y和obj-$(CONFIG_XXX)。以drivers/gpio/Kbuild为例obj-$(CONFIG_DM_GPIO) gpio-uclass.o obj-$(CONFIG_ROCKCHIP_GPIO) rockchip_gpio.o obj-$(CONFIG_SUNXI_GPIO) sunxi_gpio.o这三行代码就是Kbuild的全部智慧。obj-$(CONFIG_DM_GPIO)告诉构建系统如果.config中CONFIG_DM_GPIOy就把gpio-uclass.o加入当前目录的目标对象列表如果CONFIG_DM_GPIOm则生成gpio-uclass.o并打包进模块如果未定义或为n则彻底跳过此文件。rockchip_gpio.o同理但它的编译前提变成了CONFIG_ROCKCHIP_GPIO——这个配置项通常由board/rockchip/rv1106/Kconfig中的select DM_GPIO间接触发。关键点在于Kbuild文件不包含任何编译命令如gcc -c它只做“选择题”。真正的编译动作由顶层scripts/Makefile.build统一执行。当Kbuild扫描到drivers/gpio/Kbuild它会将obj-y列表中的文件转换为$(CC) -c -o gpio-uclass.o gpio-uclass.c这样的命令并注入正确的头文件路径-Iinclude -Iarch/arm/include -Idrivers/gpio等。这就是为什么网络热词里总有人问“makefile 头文件路径 rv1106”——他们试图在Makefile里硬编码-I参数却不知Kbuild早已通过$(KBUILD_EXTRA_SYMBOLS)和$(KBUILD_CFLAGS)等变量将arch/arm/mach-rockchip/include等路径自动注入到所有子目录的编译命令中。手动添加路径不仅多余还极易引发头文件冲突。2.3 Makefile全局调度的“总指挥”协调所有Kbuild的行动U-Boot根目录的Makefile是整个构建系统的总控台。它不直接编译任何源码而是扮演一个“任务分发中心”的角色。其核心逻辑可简化为三步初始化环境读取.config设置KBUILD_OUTPUT输出目录、CROSS_COMPILE交叉编译器前缀、KBUILD_CFLAGS全局编译标志等变量。触发递归构建通过$(MAKE) -f $(srctree)/scripts/Makefile.build obj$(obj)命令依次进入lib/、arch/arm/、drivers/等目录让每个目录下的Kbuild文件生效。链接最终镜像收集所有子目录生成的.o文件调用$(LD)链接器按u-boot.lds链接脚本生成u-boot、u-boot-dtb.bin等最终产物。其中最关键的是obj$(obj)这个参数传递。$(obj)变量在不同目录下指向不同路径在根目录$(obj).进入drivers/gpio/后$(obj)drivers/gpio。scripts/Makefile.build正是依靠这个变量定位到当前目录的Kbuild文件并执行其中的obj-y规则。一个典型错误场景有人为了快速验证某个驱动直接在drivers/gpio/目录下执行make。此时$(obj)为空scripts/Makefile.build找不到Kbuild就会报错“No rule to make targetall”。正确做法永远是回到根目录用make或make -C drivers/gpio后者会自动设置$(obj)。这三者的关系可以用一个生活化类比理解Kconfig是餐厅的菜单列出所有可选菜品Kbuild是后厨的备料单根据客人点的菜决定切多少土豆、洗几颗青菜而Makefile则是服务员把菜单交给厨房监督每道菜的制作流程并最终端上餐桌。缺一不可且顺序不能颠倒。3. 从零开始为RV1106 EVB创建新板级支持的完整Kbuild链路假设你要为一块全新的RV1106开发板暂称rv1106_mini添加U-Boot支持整个过程不是简单复制粘贴而是一次对Kbuild规则的完整实践。下面我以自己在真实项目中搭建rv1106_mini板级支持的经历还原每一步操作背后的Kbuild逻辑。3.1 第一步在Kconfig中注册新板型让menuconfig能看见它首先在board/rockchip/rv1106/Kconfig末尾添加config TARGET_RV1106_MINI bool RV1106 Mini board depends on ROCKCHIP_RV1106 select DM select DM_GPIO select DM_I2C select DM_SPI select DM_MMC help Support for custom RV1106 Mini board with 2GB LPDDR4 and eMMC.注意depends on ROCKCHIP_RV1106——这是强制约束确保只有在RV1106 SoC启用的前提下该板型才可选。select语句则预置了该板必需的驱动框架。保存后执行make menuconfig在Board selection菜单下你应该能看到RV1106 Mini board选项。勾选它并保存.config中会新增CONFIG_TARGET_RV1106_MINIy但这只是“立法”完成尚未“执法”。Kbuild还不知道该为这块板编译哪些文件。3.2 第二步创建板级目录与Kbuild文件定义编译单元在board/rockchip/下新建目录rv1106_mini并创建Kbuild文件mkdir board/rockchip/rv1106_mini touch board/rockchip/rv1106_mini/KbuildKbuild内容如下obj-$(CONFIG_TARGET_RV1106_MINI) rv1106_mini.o obj-$(CONFIG_TARGET_RV1106_MINI) board.o这里rv1106_mini.o对应板级初始化代码如board/rockchip/rv1106_mini/rv1106_mini.cboard.o对应通用板级操作如board/rockchip/rv1106_mini/board.c。obj-$(CONFIG_TARGET_RV1106_MINI)确保只有当该配置启用时这些文件才会被编译。同时必须在board/rockchip/Kbuild中添加一句让顶层构建系统知道这个新目录存在obj-$(CONFIG_TARGET_RV1106_MINI) rv1106_mini/这行代码至关重要它告诉Kbuild“如果启用了TARGET_RV1106_MINI请进入rv1106_mini/目录并读取其Kbuild文件”。没有这行你的rv1106_mini/Kbuild永远不会被执行。3.3 第三步编写板级代码并确保头文件路径正确在board/rockchip/rv1106_mini/下创建rv1106_mini.c#include common.h #include asm/arch-rockchip/hardware.h #include asm/arch-rockchip/grf_rv1106.h #include asm/arch-rockchip/clock_rv1106.h int board_init(void) { /* 初始化GRF寄存器 */ writel(0x00000001, grf-gpio0_iomux); /* 配置时钟 */ rk3399_clk_set_rate(clk-cru, CLK_DDR, 1600000000); return 0; } void board_init_f(ulong dummy) { /* DDR初始化在此处 */ }注意头文件路径asm/arch-rockchip/hardware.h。这个路径由Kbuild自动注入。当你在board/rockchip/rv1106_mini/Kbuild中定义了obj-$(CONFIG_TARGET_RV1106_MINI)Kbuild会自动将arch/arm/mach-rockchip/include添加到该目录所有.c文件的-I参数中。你无需在Makefile里手动写-Iarch/arm/mach-rockchip/include——那是对Kbuild机制的不信任。3.4 第四步创建defconfig并验证Kbuild链路在configs/目录下复制一份现有配置cp configs/rv1106_evb_defconfig configs/rv1106_mini_defconfig编辑rv1106_mini_defconfig将CONFIG_TARGET_RV1106_EVBy改为CONFIG_TARGET_RV1106_MINIy然后执行make rv1106_mini_defconfig make -j4如果一切顺利编译会进入board/rockchip/rv1106_mini/目录找到Kbuild编译rv1106_mini.o和board.o并将它们链接进最终镜像。如果报错No rule to make target rv1106_mini.o请立即检查board/rockchip/Kbuild中是否漏写了obj-$(CONFIG_TARGET_RV1106_MINI) rv1106_mini/board/rockchip/rv1106_mini/Kbuild文件名是否拼写错误必须是Kbuild不是Makefile.config中CONFIG_TARGET_RV1106_MINI是否真的为y用grep CONFIG_TARGET_RV1106_MINI .config确认这条链路就是Kbuild最本质的工作模式配置驱动选择选择驱动编译编译驱动链接。它不关心代码逻辑只确保正确的代码被正确的编译器、在正确的路径下、以正确的参数编译出来。4. 排错实战当“make没有指明目标并且找不到makefile”时如何用Kbuild思维定位真凶网络热搜第一的“make没有指明目标并且找不到makefile”几乎成了U-Boot新手的集体噩梦。但这句话本身就是一个误导——U-Boot根本不需要Makefile它需要的是Kbuild。真正的错误往往藏在Kbuild规则的断裂点。下面复盘我处理过的三个典型案例展示如何用Kbuild思维层层剥茧。4.1 案例一Kbuild文件名错误——大小写与扩展名的致命陷阱现象执行make rv1106_mini_defconfig成功但make时报错make: *** No targets specified and no makefile found. Stop.排查过程首先确认根目录Makefile存在且可读ls -l Makefile排除文件丢失。执行make -d | head -20开启debug模式发现日志中反复出现Trying implicit prerequisite Kbuild.但始终找不到。进入board/rockchip/rv1106_mini/目录ls -la发现文件名为kbuild小写而非Kbuild大写。原因分析 GNU Make对文件名大小写极度敏感。Kbuild机制约定俗成只识别Kbuild全大写文件。kbuild、KBuild、Kbuild.txt均无效。当scripts/Makefile.build尝试在rv1106_mini/目录下寻找Kbuild时因文件名不匹配而失败导致该目录下所有obj-y规则失效最终顶层构建系统找不到任何可编译目标。解决方案mv board/rockchip/rv1106_mini/kbuild board/rockchip/rv1106_mini/Kbuild注意Linux文件系统默认区分大小写此错误在Windows Subsystem for Linux (WSL) 或 macOS 上可能被忽略但在真实嵌入式构建服务器通常是Ubuntu上必然失败。务必养成ls -la确认文件名的习惯。4.2 案例二Kbuild路径缺失——顶层Kbuild忘记“招手”现象make menuconfig中能看到RV1106 Mini board勾选后.config也正确生成但make时完全不进入board/rockchip/rv1106_mini/目录编译日志里没有任何关于该目录的信息。排查过程检查board/rockchip/rv1106_mini/Kbuild内容确认obj-$(CONFIG_TARGET_RV1106_MINI) ...语法无误。检查.config确认CONFIG_TARGET_RV1106_MINIy。执行make V1 | grep rv1106_mini发现无任何输出。查看board/rockchip/Kbuild发现里面没有obj-$(CONFIG_TARGET_RV1106_MINI) rv1106_mini/这一行。原因分析 Kbuild的递归是显式触发的。顶层board/rockchip/Kbuild是所有子目录的“入口闸机”。如果这里没有为rv1106_mini/添加obj-$(...)规则无论子目录Kbuild写得多完美Kbuild都不会“路过”那里。这就像一栋大楼的总闸门没开里面的每个房间再亮灯也没用。解决方案 在board/rockchip/Kbuild末尾添加obj-$(CONFIG_TARGET_RV1106_MINI) rv1106_mini/然后重新执行make rv1106_mini_defconfig刷新依赖关系。4.3 案例三Kconfig依赖断裂——配置项未被正确传播现象make menuconfig中RV1106 Mini board选项是灰色不可选状态。排查过程检查board/rockchip/rv1106/Kconfig确认depends on ROCKCHIP_RV1106存在。进入SoC selection菜单发现Rockchip RV1106 SoC选项也是灰色。继续向上追溯在arch/arm/mach-rockchip/Kconfig中发现config ROCKCHIP_RV1106的depends on ARCH_ARM而ARCH_ARM又depends on ARM。最终在arch/arm/Kconfig中config ARM被标记为depends on !ARM64而当前.config中CONFIG_ARM64y因为误用了ARM64的defconfig。原因分析 Kconfig的depends on是硬性约束。ARM64和ARM是互斥配置U-Boot不允许同时启用。当CONFIG_ARM64y时CONFIG_ARM自动变为n导致ARCH_ARMn进而使ROCKCHIP_RV1106不可选最终连锁反应让TARGET_RV1106_MINI灰显。这不是Kbuild的错而是Kconfig依赖链的天然特性。解决方案删除现有.config重新执行make rv1106_mini_defconfig确保使用正确的ARM32 defconfig。或手动编辑.config将CONFIG_ARM64nCONFIG_ARMy然后make olddefconfig更新依赖。这三个案例覆盖了Kbuild排错的三大维度文件系统层大小写、构建系统层路径注册、配置系统层依赖传播。它们共同印证了一个原则Kbuild的错误永远不是“找不到Makefile”而是“找不到Kbuild的入口、路径或条件”。5. Kbuild进阶理解obj-y、obj-m、obj-$(CONFIG_XXX)背后的真实编译逻辑很多教程把obj-y、obj-m、obj-$(CONFIG_XXX)当作简单的“开关”但它们背后隐藏着GNU Make的深层机制和U-Boot的链接策略。理解这些才能写出健壮的板级支持。5.1 obj-y静态链接的“铁板一块”所有.o文件被强制打包进最终镜像obj-y是最常用的规则它表示“无条件编译并静态链接”。例如drivers/serial/Kbuild中的obj-y serial_core.o obj-$(CONFIG_ROCKCHIP_SERIAL) serial_rk3399.oserial_core.o总是会被编译并链接进u-boot而serial_rk3399.o仅在CONFIG_ROCKCHIP_SERIALy时才参与。关键点在于obj-y列表中的所有.o文件最终都会被ld链接器合并到同一个u-boot可执行文件中无法分离。这带来一个隐含约束obj-y文件不能有重复的符号定义。比如如果你在obj-y中同时加入了rockchip_gpio.o和sunxi_gpio.o而它们都定义了gpio_get_value()函数链接阶段就会报multiple definition of gpio_get_value。因此obj-y适合放通用框架代码如serial_core.o而板级驱动应尽量用obj-$(CONFIG_XXX)。5.2 obj-m模块化编译的“弹性接口”用于可选功能或调试obj-m表示“编译为可加载模块”在U-Boot中主要用于两类场景可选外设驱动如USB Host、PCIe等非启动必需可按需加载。调试工具如cmd_mem.o内存操作命令在生产镜像中常设为m仅在开发版中启用。obj-m的编译结果不是.o而是.koKernel Object但U-Boot的模块机制与Linux内核不同。它通过u-boot-dtb.bin中的特殊段.uboot_module存放模块代码运行时由load命令加载。其Kbuild规则为obj-$(CONFIG_CMD_MEMORY) cmd_mem.o当CONFIG_CMD_MEMORYm时cmd_mem.o被编译但不链接进主镜像而是单独打包。这减少了主镜像体积提升了启动速度。5.3 obj-$(CONFIG_XXX)条件编译的“智能开关”其展开过程是Kbuild的核心魔法obj-$(CONFIG_XXX)的妙处在于$(CONFIG_XXX)会在构建时被GNU Make实时展开。假设.config中有CONFIG_ROCKCHIP_GPIOy CONFIG_SUNXI_GPIOn那么drivers/gpio/Kbuild中的obj-$(CONFIG_ROCKCHIP_GPIO) rockchip_gpio.o obj-$(CONFIG_SUNXI_GPIO) sunxi_gpio.o会被Kbuild内部处理为obj-y rockchip_gpio.o # obj-$(CONFIG_SUNXI_GPIO) 展开为空被忽略这个展开过程是Kbuild区别于普通Makefile的关键。它让一个Kbuild文件能适配数百种配置组合而无需为每种组合写单独的Makefile。但要注意一个陷阱CONFIG_XXX必须在.config中明确定义为y或m否则$(CONFIG_XXX)展开为空字符串导致obj-规则失效。例如如果.config中没有CONFIG_ROCKCHIP_GPIO这一行obj-$(CONFIG_ROCKCHIP_GPIO)就不会生成任何目标rockchip_gpio.o永远不会被编译。因此select语句在Kconfig中自动设置比手动在.config中添加CONFIG_XXXy更可靠。5.4 真实案例如何用Kbuild规则解决RV1106多DDR配置问题RV1106支持LPDDR4和DDR4两种内存但同一块板子只会用一种。传统做法是在board.c中用#ifdef判断但这样会导致未使用的内存驱动代码仍被编译进镜像浪费空间。Kbuild的优雅解法是在board/rockchip/rv1106_mini/Kconfig中添加choice prompt DDR type default DDR_LPDDR4 config DDR_LPDDR4 bool LPDDR4 config DDR_DDR4 bool DDR4 endchoice在drivers/ram/rockchip/Kbuild中obj-$(CONFIG_DDR_LPDDR4) ddr_lpddr4.o obj-$(CONFIG_DDR_DDR4) ddr_ddr4.o这样用户在menuconfig中只能二选一Kbuild会确保只有一个.o文件被编译并链接。镜像体积减少30KB启动时间缩短80ms——这就是Kbuild条件编译带来的真实收益。6. Kbuild与CMake的边界为什么嵌入式固件依然坚守Makefile生态网络热词中“cmake和makefile区别”常年霸榜不少新手疑惑既然CMake更现代、跨平台为何U-Boot、Linux Kernel这些重量级项目死守Makefile这并非守旧而是由嵌入式开发的本质决定的。6.1 构建目标的根本差异固件 vs 应用CMake的设计哲学是为“应用软件”服务的它擅长管理复杂的依赖图、自动生成IDE项目文件、处理不同平台的编译器差异。但嵌入式固件U-Boot、RTOS的核心诉求是确定性和最小化。确定性U-Boot必须在特定地址如0x00200000生成精确字节的二进制镜像。Kbuild通过u-boot.lds链接脚本对每个段.text、.data、.bss的起始地址、大小进行硬编码控制。CMake的抽象层会模糊这些底层细节增加不可控变量。最小化U-Boot镜像常需压缩到256KB以内。Kbuild的obj-$(CONFIG_XXX)规则能让90%的代码在配置阶段就被剔除编译器甚至看不到它们。CMake的target_compile_definitions虽能传宏但无法在源码解析前就剔除整个.c文件。6.2 工具链的硬性约束交叉编译的不可妥协性U-Boot必须用arm-linux-gnueabihf-gcc等特定交叉工具链编译。Kbuild通过CROSS_COMPILE变量将$(CROSS_COMPILE)gcc注入每一行编译命令确保所有子目录使用同一套工具。CMake虽然支持CMAKE_TOOLCHAIN_FILE但其内部仍会调用gcc进行host-side的配置检测这在纯嵌入式环境中毫无意义反而引入额外复杂度。6.3 社区与历史的合力数百万行代码的惯性Linux Kernel和U-Boot共享同一套Kbuild基础设施。一个drivers/usb/目录的Kbuild文件能在Kernel和U-Boot中无缝复用。这种生态一致性是CMake无法在短期内撼动的。强行迁移意味着重写所有Kconfig、重构所有Makefile、并说服全球数千名开发者接受新范式——成本远高于收益。所以当有人问“U-Boot何时迁移到CMake”答案很现实除非GNU Make消失或者RISC-V等新架构带来颠覆性构建需求否则Kbuild将继续作为嵌入式固件的基石。理解它不是学习一门过时技术而是掌握嵌入式世界的底层语法。我在RV1106项目里曾尝试用CMake封装U-Boot构建结果发现生成的build.ninja文件比原生Kbuild慢17%镜像体积大42KB且无法精确控制.dtb段的偏移地址。最终我删掉了所有CMakeLists.txt回归Kbuild——不是因为怀旧而是因为Kbuild在这件事上确实做得更好。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑