用介子拥抱模块性

2020-05-10 08:56:18

斯蒂芬·布伦南·2020年5月8日大约七年前,我第一次承诺使用我的第一个C库。这是一个个人的C库-非常私人,我给它取名为“libstephen”,以确保其他人甚至不会考虑使用它。公平地说,没有人应该使用它!这是一个由一个孩子编写的数据结构库,他只有几周的C经验和一堆基本的Java课程。在没有详细说明的情况下,代码相当糟糕。

但不管密码有多糟糕,它都是我的。它工作正常!它有测试!我甚至可以开始在其他项目中使用它。有了预制的数据结构,C语言感觉像是一种更容易的语言。它让我处理更大的问题,而不是淹没在每一件小事的细节中。但很快,一个问题出现了。我花了很多时间使这个库真正易于开发,但我不知道如何使它易于使用。据我所知,使用libstephen的最好方法是创建一个git子模块,然后在Makefile中执行如下操作:

libstephen/bin/release/libstehen.a:make-C libstehenCFLAGS+=-I libstephen/include/#不要忘记将库添加到链接命令中。

虽然很笨拙,但还是奏效了。但出于几个原因,我开始讨厌这种方法。首先,没有真正的版本控制--如果我在libstephen中做了突破性的更改,我必须确保在更新依赖项之前不更新某个特定的git版本。其次,我在libstephen的构建系统和工具上投入了如此多的时间,以至于制作新项目是令人沮丧的,因为他们没有任何这些工具。当然,我可以一遍又一遍地为每个新项目添加这些功能,但那是重复忙碌的工作。我找到了一个“更好”的方法。

我并没有创建新的库和新的项目,而是捏造了它:我把我所有的代码都放到了这个库中。想要实现正则表达式吗?砰!把它放入libstephen中。一种基于LISP的编程语言?添加到libstephen中!登录库?为什么不把它也加进去呢?最初的数据结构库变得非常臃肿,这一切都是因为(A)创建新项目并不容易,(B)在项目中使用我现有的库并不容易。

因此,libstephen对我来说有点尴尬。它非常臃肿,代码不是很好,但它有很多有用的东西,我的项目依赖于它。但是几个月前,我偶然发现了一套有可能解决这个问题的工具,我一直在用它们来修复这个烂摊子。所有这一切的核心是被称为介子的构建系统。

对于那些热衷于开源C/C++项目的人来说,Meson并不新鲜。像GNOME和Systemd这样的项目已经迁移到Meson,而将软件迁移到Meson似乎仍然是一件时髦的事情。

对于那些不熟悉的人来说,介子是一个类似于Autotools或CMake的构建系统。它旨在解决以下几个问题:

在此之前,我最熟悉的是Vanilla Makefiles,它擅长#1,但在#2或#3方面帮不了你太多。因此,像CMake和Autotools这样的系统实际上会处理#2和#3,并生成Makefile来做#1。

来自Python等内置了包管理的语言,C语言没有标准的包管理系统,这几乎让人感到奇怪。与C中传统的包管理最接近的是,您可以编译许多这样的Autotools项目:

当然,如果您缺少依赖库,这将失败,因此您需要自己进行依赖解析。所以归根结底,这并不是很容易,而且肯定不是很自动化。

介子为整个混乱提供了一个相当优雅的解决方案。在meson.build文件(大致类似于Makefile)中,编写如下内容:

libstephen_dep=依赖项(';libstephen';,回退:[';libstephen';,';libstephen_dep';],版本:';>;=0.3.1';,)#.MY_PROJECT=可执行文件(#.。依赖项:[libstephen_dep,],)。

否则,退回到一个名为libstephen的“子项目”,并使用在其meson.build中声明的名为libstephen_dep的依赖项。

稍后,Executable()调用声明了一个程序,通过包含依赖项,它可以将库与您的程序链接起来,并为您添加正确的包含目录。相当漂亮!

“子项目”作为依赖项未安装到系统时的后备,但它本身实际上非常有用。实质上,您的项目包含一个子项目目录,其中将包含一个名为libstehen.print的文件:

这将指示介子获取特定版本的GIT存储库。您还可以要求它从Web上抓取一个tarball(并提供该文件的校验和)。生成的目录应该有一个meson.build文件,该文件可以构建项目。如果项目不使用Meson,您甚至可以向包含meson.build的子项目添加“补丁”。最后,您的mesonbuild文件只需要提供一个“依赖”声明,允许该库链接到您的项目中。

MESON将自动定位您的依赖项,并将构建系统设置为链接到系统版本,或构建本地版本。

那么,如果项目A依赖于B,而B依赖于C,那该怎么办呢?嗯,介子让它变得非常简单。项目A需要包括B和C的子项目。如果项目A包括项目B,而不知道C,介子甚至提供了一个很好的警告信息:

|查找依赖项C的备用子项目|subjects/B/meson.build:26:警告:未找到依赖项C,但它在子项目中可用。|若要在当前项目中使用依赖项C,请转到项目源|root并发出以下命令来升级它:|meson WRAP Promote Subproject/B/subjects/C.wire。

这是我最喜欢的一种错误消息:它确切地告诉您如何修复它。因为项目B已经有了编译projectC所需的包装文件,所以Meson可以帮助您将其复制到项目A的子项目目录中。这很好,因为包装文件将指定您正在使用的依赖项的确切版本,并且您可以自由修改B正在使用的版本,或者不去管它。

A-依赖于->;B-依赖于-\--依赖于->;D-依赖于->;C。

在这种情况下,我们将A、B和C放在与上次相同的链中。但除此之外,A依赖于D,D也依赖于C,形成了一个钻石。虽然这看起来更复杂,但同样的原则也适用!项目A将包含B、C和D的包装文件,而B和D将共享C的相同版本。只要有与Band C兼容的C版本,就可以了。而且,更好的是,如果介子找到任何安装到系统上的依赖项,它也会使用这些依赖项。

这真的鼓励我拥抱模块性,编写易于测试并能做好一件事的小而简单的库。我已经将libstephen中的许多代码部分分成了更小的库,这些库都共享相似的命名模式和相似的项目结构:

每一个都更有针对性(尽管藏书库可能会被分成更小的单元),并且可以单独包括在一起。每个库都很容易使用,但是如果我需要更新一下如何使用库,或者如何安装并将它们添加为依赖项,我已经创建了sc-examples存储库,它收集依赖于这些库的小程序的简单、完整的示例,以及构建脚本和所有东西。

为了使我更容易创建新的模块库,我创建了sc-template,只需将其复制到新的存储库中即可简化操作。没有一个模板文件太大或太复杂,但这只是进一步简化了这个过程。最后,为了将所有这些项目收集到一个统一的整体中,我已经开始使用Sourcehut项目特性:sc-libs。

除了对我的所有C库使用Meson之外,我还开始对sc-template中使用的其他几个工具进行标准化,以加强一致性:

meson自动生成COMPILE_COMMANDS.json,clangd可以使用它为任何编辑器提供LSP服务器。

我使用标准的.clang格式文件在我的代码上强制执行大致为Linux内核的编码标准。

我开始标准化Unity测试框架,它很好地集成到Meson的测试系统和自动代码覆盖生成中。

展望未来,我希望我能从更多的共享工具中获益:

使用Sourcehut构建来执行CI测试,并发布文档以及新版本的资源tarball。

这项工作的结果是相当不错的。经过几个月的(断断续续的)努力,我的IRC聊天机器人CBOT已经迁移到Meson上,并设法摆脱了对libstephen的依赖,转而支持我的sc-libs。

虽然我一直使用的这套工具没有什么太大的革命性,但它让我真的感到耳目一新。第一次,我感觉我有了一套有用的工具来管理C库依赖关系,这使我能够像用任何其他语言一样编写模块化代码。希望这篇长篇大论的帖子也能给其他人一些洞察力或灵感来学习介子!

法律·RSS斯蒂芬·布伦南(Stephen Brennan)的博客采用知识共享署名-ShareAlike 4.0国际许可