date:

搞了个 C++ 构建系统


我平时的工作内容是开发在服务端上运行的网络程序,主要语言是 C++,并且几乎全部跑在统一的机器环境上。所以我们一直以来都在使用一套简单从 blade-build 魔改来的编译系统。这个系统的本质是一个 Unix Makefile 的生成器,这个生成器使用 Python2 编写,代码有点难以维护。(毕竟使用动态语言的东西就是这样,出活还是很快的,但是时间一长了,代码就变的很难维护。因为后来的人,很难完全理解其中每个变量的类型,这对于我这种长期写静态强类型语言的人来说,是很痛苦的。)

在这样的基础上,我就萌生了把这套系统推倒重来的想法,这次我想用一个大家都熟悉,并且一定会写的语言来完成,它还一定是强类型的。在这几个条件下面,我超爱的 Rust 就被我排除在外了,因为好像我们周围会写的人也不是很多。再加上人家 cmake 也是用 C++ 来写的,我一想,我应该也比人家不差啥。再说,我还可以去 cmake 里面去偷很多实现出来。就这么决定了,用 C++ 来实现这个新的构建系统。

JK-Build

首先就是名字了,为啥叫 jk,那肯定有很多不可爱的人有奇奇怪怪的想法了!我当时起这个名字的时候,完全是图这两个按键在键盘上位于很近的位置,并且由于我是个 vim 的用户,其实手也常年在这两个按键附近,他们真的很近,很方便,就随手用了这个名字。

那现在的系统有啥问题呢

  • 首先就是,原罪之一就是 Python。这个系统生成 Makefile 的速度在有的时候,它实在是有点慢了。
  • 如果 BUILD 文件有改动了,它需要自己手动运行命令来重新生成。
  • 对于 lint 来说,它的规则是尝试 lint 所有文件的,但是其实有很多文件是写出来放在那里,可以只搞了一半的(我就经常这样)。在这种时候我的代码是没法通过编译的,更不可能过 lint 了。
  • 没有进度条。看看人家 cmake,还有个进度显示呢。这不就决定了我在编译的时候,是去喝杯咖啡,还是去拿个可乐嘛!
  • 维护困难。Python 的代码有点难以维护,反正我在改的时候,是全靠着 print,到运行的时候观察每个变量到底是什么类型来 debug 的。
  • 对于 clang-base 的工具,没有过匹配。大部分的,C++ 的代码检查和补全工具,其实都是建立在 clang 之上的,而 clang 会需要 compile database 来找到每个文件应该如何被编译,然后才能有 AST 或者其他的补全、提示什么的。

那和以前有什么不同呢

BUILD 文件和 targets

BUILD 文件来看,是没什么不同的:

  • BUILD 文件依旧是 Python 的语法。
  • 所有的目标构建依旧是一些 Python 的函数。
  • 暂时来说,可用的构建目标类型也没有增加。(毕竟完全兼容旧系统就有很多的工作了)

新的系统,第一个目标就是完全兼容旧的所有已有的 BUILD 文件。毕竟,迁移的工作难度小了,新的系统才容易被大家接受。所以,所有的 BUILD 文件还依旧会是以前那样:

cc_library(name = "base",
           cppflags = [ "-Ilibrary", ],
           srcs = [ "*.cpp" ],
           excludes = [ "*_test.cpp" ],
           ldflags = [ "-ldl" ],
           deps = [
                    "//library/logging/BUILD:logging",
                    "//library/memory/BUILD:memory",
                    ":clock",
                  ],
           )

但是从可用的参数和意义上来说,还是有很多不同的。比如,构建目标 cc_library 现在有个参数叫 cppflags,现在我们就面临着这个参数的值没法继承的问题。往往如果在一个 target 里写了一个 -Ixxxx 的参数,在依赖它的 target 里面还是不可避免的要写上。所以在新的系统中,给 cc_library 以及它的所有派生目标都加入了可以继承的,includesdefines 参数,用来传递信息给依赖这个目标的目标。

对于这部分的变化,就去文档上看就好啦。(虽然现在还没有)

编译进度

还有进度条的加入。新的构建系统,在编译的时候,会有一个和 cmake 一样的计数进度的显示。这个地方 cmake 是通过统计一个目录下的文件个数来实现的,非常巧妙。具体的实现是,在一个编译命令的提示信息显示的同时,在一个特定的目录下也 touch 出来一个独一无二的文件,并且进度就应该是这个目录下的文件个数除以总的应有的文件个数。那如果文件没有改变怎么办?不会编译,那对应的计数不就没法统计进去了嘛?是的!所以,它还维护了一份总的计数给到一个 target 最后的命令上,这个命令是无论如何都会强制执行的,它会一次 touch 这个 target 应有的全部的文件。比如像这样:

all: lib.a
.PHONY: all

lib.a: a.o b.o c.o d.o e.o
  @jk-print --number=1,2,3,4,5,6 'lib.a'
  ...

a.o: a.cpp a.h
  @jk-print --number=1 'compiling a.cpp'
  ...

b.o: b.cpp b.h
  @jk-print --number=2 'compiling b.cpp'
  ...

c.o: c.cpp c.h
  @jk-print --number=3 'compiling c.cpp'
  ...

d.o: d.cpp d.h
  @jk-print --number=4 'compiling d.cpp'
  ...

e.o: e.cpp e.h
  @jk-print --number=5 'compiling e.cpp'
  ...

这样就可以完美的把没有进行编译的文件的都补上了。

compile database

现在在生成 Unix Makefile 的同时,还会在项目的根目录生成一个 compile_commands.json 文件,作为其他 clang-base 的工具的基础。你可以把它和比如 clangd 或者 ccls 来配合。提供项目代码的补全、提示个高亮之类的功能。当然,如果你会自己写一些工具的话,就更好了!

内置的 cmake-base 的第三方库支持

原本使用一个第三方库,那可麻烦了。我们要自己写一个用来下载安装这个第三方库的一个脚本。自己要小心的处理好,下载文件的逻辑,是不是使用的代理。文件下载之后是不是完整,要不要进行 md5 校验。文件怎么解压,包括后面的 cmake 命令怎么搞,怎么填写这一系列事情。虽然我尝试将一部分工作,封装在了一些内置的包里面,但是依旧有点麻烦。所以这次趁着重写,干脆就重新完整的提供了一个内置的支持!

cmake_library(
  url = "http://xxxxx.xxx/xxx.tar.gz",
  sha256 = "xxxxxx",
  type = "tar.gz",
  header_only = False,
  job = 10,
)

独立的进程

现在终于不再依赖,所有的项目都是 media_build 的一个子目录了!你可以任意组织你的项目目录,可以随意的把不管是 libray 还是 protocol 都作为一个 sub-project 放在自己的项目目录里,用自己想用的任意方式来管理!

最后的最后

不过,现在还在开发过程啦。 还有一些对以前的兼容没有实现。但是这也已经是一个全新的构建系统了!最初搞的时候,不光考虑了兼容,很多新的功能还有参数设计,也参考了 blazebuck 还有 cmake 的样子。也应该算是取出来这三个里面,我认为用起来比较方便,舒服的部分吧。

不过缺点还是,这个系统的最初目标就是构建服务端的程序的,对于环境方面没有过多的考虑。所以基本不兼容 windowsmacos 能不能用还有全看缘分。这个就没啥计划了… 要是恰好能用!那可太好了!


<- 软件设计哲学(NOTE)
支付宝 | Alipay
微信 | WeChat