前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >日拱一卒,元编程不是元宇宙,麻省理工教你makefile、依赖管理和CI

日拱一卒,元编程不是元宇宙,麻省理工教你makefile、依赖管理和CI

作者头像
TechFlow-承志
发布2022-09-21 11:03:33
2820
发布2022-09-21 11:03:33
举报
文章被收录于专栏:TechFlow

作者 | 梁唐

出品 | 公众号:Coder梁(ID:Coder_LT)

大家好,日拱一卒,我是梁唐。

今天我们继续麻省理工missing smester,消失的学期的学习。这一节课的内容关于元编程。

B站视频链接:https://www.bilibili.com/video/BV1x7411H7wa?p=7

元编程(metaprogramming)并不是一个新鲜的概念或者是什么技术,这节课的老师用它来泛指一些和编程的流程相关的内容。比如构建系统、代码测试和依赖管理。

这些东西和git有点相似,在我们单打独斗的时候,看起来无关紧要。但是当我们进入公司,参与到一些大型的项目当中的时候,这些东西随处可见。等到那个时候才发现一无所知就不好了,所以这节课会带领大家稍稍了解一下这个概念,为以后做准备。

有一点需要注意,在一些编程语言当中,元编程拥有另外的含义,用来指代操作程序的程序,比如Python中的元类等。这和这节课的内容是完全不同的。

日拱一卒,欢迎大家打卡一起学习。

构建系统

如果你用LaTeX写一篇论文,你会需要运行什么命令呢?是不是需要一个命令生成benchmark,一个命令生成图表,一个命令将图插入论文?

对于大多数项目而言,不论是否包含代码,一般都会有一个创建过程。你需要执行一系列操作来得到想要的结果。通常,这个过程包含许多步骤或者是很多分支。跑这个命令生成这个,跑那个命令生成那个,最终可能还要把多个结果进行合并。整个过程可能非常麻烦,好在有很多工具可以帮助我们解决这个问题。

这些工具通常叫做创建系统,种类很多,你可以根据你的任务、喜好的语言、项目大小进行选择。但根本上,这些工具大同小异,你需要定义依赖、创建目标以及创建规则。你告诉系统你需要得到的结果,工具会找到构建这些目标需要的依赖,并且根据规则进行创建。理想情况下,如果依赖没有变化,系统不会重新创建目标结果。

make是最常见的构建系统之一,它几乎内置在所有Unix系统当中。虽然也有短板,但对于小型项目来说,它非常完美。当你执行make的时候,它在当前目录寻找一个叫做Makefile的文件,其中包含所有的目标、依赖以及创建规则。让我们来看一个例子:

代码语言:javascript
复制
paper.pdf: paper.tex plot-data.png
    pdflatex paper.tex

plot-%.png: %.dat plot.py
 ./plot.py -i $*.dat -o $@

文件中的内容都是规则:如何使用冒号右侧的文件创建冒号左侧的文件。换句话说,冒号左侧的是目标,右侧的是依赖。缩进的部分是一个用来从依赖创建目标的程序。在make当中,第一条指令同样表明了最终目标。如果你运行make的时候不带任何参数,它就是我们最终创建的结果。或者,你可以加上参数,比如make plot-data.png,它将会创建你给定的目标。

规则当中的%是一个模式,它将会匹配左右两侧相同的字符串。比如,如果目标是plot-foo.png,那么make将会查找foo.datplot.py这两个依赖。让我们在不提供任何依赖的情况下,运行一下查看一下结果:

代码语言:javascript
复制
$ make
make: *** No rule to make target 'paper.tex', needed by 'paper.pdf'.  Stop.

make告诉我们为了创建paper.pdf,它需要paper.tex,但没有任何一条规则关于如何创建它,所以停止了。让我们试着修改一下:

代码语言:javascript
复制
$ touch paper.tex
$ make
make: *** No rule to make target 'plot-data.png', needed by 'paper.pdf'.  Stop.

有意思的是,我们有创建plot-data.png的规则,但这是一条模式规则。因为创建plot-data.png的依赖文件data.dat不存在,所以make告诉我们,它无法创建。让我们再试着改一下:

代码语言:javascript
复制
$ cat paper.tex
\documentclass{article}
\usepackage{graphicx}
\begin{document}
\includegraphics[scale=0.65]{plot-data.png}
\end{document}
$ cat plot.py
#!/usr/bin/env python
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-i', type=argparse.FileType('r'))
parser.add_argument('-o')
args = parser.parse_args()

data = np.loadtxt(args.i)
plt.plot(data[:, 0], data[:, 1])
plt.savefig(args.o)
$ cat data.dat
1 1
2 2
3 3
4 4
5 8

当我们执行make的时候会发生什么?

代码语言:javascript
复制
$ make
./plot.py -i data.dat -o plot-data.png
pdflatex paper.tex
... lots of output ...

创建成功了!

如果再执行一次会怎么样?

代码语言:javascript
复制
$ make
make: 'paper.pdf' is up to date.

答案是什么也没有发生,因为make检查了它所有的依赖,发现都没有更新,那么它也就不会重新创建paper.pdf。让我们试着修改一下paper.tex,重新make:

代码语言:javascript
复制
$ vim paper.tex
$ make
pdflatex paper.tex
...

注意make没有重新执行plot.py,因为plot-data.png的依赖没有变化。

依赖管理

从宏观的角度来说,你的项目依赖的可能是其他人的项目。你可能依赖一些需要安装的程序(比如Python),系统包(比如openssl),或者是一些编程语言的库(比如matplotlib)。

大多数依赖可以通过某些仓库获得,这些仓库当中存储了大量的依赖,并且提供非常方便的安装机制。比如Ubuntu系统下的安装包仓库,你可以使用apt工具进行安装。Ruby的仓库RubyGems,以及python库PyPi。

由于这些库的安装方法往往大相径庭,所以我们不会过多地深入细节,或者是其中任何一个工具。我们只会讲述一些通用的术语,比如版本。大多数项目每次发布的时候会提供一个用数字表达的版本号。比如8.1.3或者是64.1.20192004,通常都是数字,有时也有例外。

版本号有很多用途,最重要的功能是确保程序可以正确运行。设想一下,比如我发布了我这个库的新版本,我改了其中一些函数的签名。其他人更新之后可能就会遇到问题,他们的编译和构建可能会失败,因为之前的函数名不存在了。

版本控制可以通过指定版本来解决这个问题。即使最新的库发生了变化,依赖它的项目仍然可以使用它过去的版本。

但这看起来不够理想,比如当我想要修改一个安全问题的时候,它不会影响任何接口(API),但所有使用这个旧版本的都需要升级,怎么样能确保这点呢?

这也是版本号包含多个部分的原因。每个部分的数字代表的含义往往各不相同,但也有一个常用的规范:https://semver.org/。在这个版号当中,通常写成:major.minor.patch。规则如下:

  • 如果新的版本没有改变 API,请将补丁号递增
  • 如果您添加了 API 并且该改动是向后兼容的,请将次版本号递增
  • 如果您修改了 API 但是它并不向后兼容,请将主版本号递增

这会带来很多好处,如果我的项目依赖你的项目,只要使用的主版本号是相同的就没有问题。次版本号不低于之前使用的版本即可。也就是说,如果我依赖你的1.3.7版本,我使用1.3.8,1.6.1或者是1.3.0都是可以的。2.2.4可能不行,因为主版本号增加了。

我们可以把Python当做是一个很好的例子,你可能已经注意到了Python2和Python3的代码并不完全兼容,这是因为它们的主版本号已经变了。同样,使用Python3.5编写的代码在Python3.7上是OK的,但在Python3.4上可能不行。

在使用依赖管理系统的时候,你可能会遇到锁文件(lock file)的概念。锁文件的定义很简单,它会列出你所依赖的每一个项目的具体的版本。通常,你需要显式执行升级程序才能升级你依赖的版本。

这样设计有很多原因,比如避免无意义的重编译、创建可复制的编译,或禁止自动升级到最新版本。还有一种极端依赖锁定叫做vendoring,它会把你依赖当中的所有代码和程序都复制到你的项目当中。这样,你就可以完全掌控所有依赖的变化,并且允许你引入你自己的修改,这同样意味着你需要显式地去拉取上游维护者的更新。

持续集成系统

当你工作在越来越膨胀的项目中时,你会发现当你创建变更的时候总有许多额外的工作。你需要上传一份新版本的文档,上传编译好的软件版本,发布代码到pypi,运行你的单元测试,以及等等这类的事情。

也许每次当有人向的GitHub仓库发送pull request时,你都希望它们的代码会被检查代码风格,以及运行一些基准测试?当这样的需求多了,那么是时候学习一下持续集成了。

持续集成也被缩写成CI(continuous intergration),是一种雨伞术语,意思是当代码变更时需要做的事。有很多公司提供了各种CI的工具,大部分都是开源和免费的。比较大的有Travis CI,Azure Pipelines以及GitHub Actions。它们工作起来都差不多:你在你的仓库当中创建一个文件,描述当仓库发生变更的时候需要做的事情。

最常见的是,当有人上传了代码之后,运行单元测试。当事件被触发了之后,CI提供方会启动一个或更多虚拟机,执行你指定的命令。它们通常会记录下来运行的结果。你可以进行一些设置让你注意到单元测试失败或者是通过的时候,或者是当测试通过的时候,你的仓库会获得一个徽标。

本节课的课程网页基于GitHub pages搭建,这就是一个很好的CI系统的例子。每次当master分支有了代码提交的时候,它都会运行Jekyll博客软件并且在GitHub的特定域名重新构建网页。这就让我们可以很方便地更新网站了,我们只需要在本地进行修改,然后使用git进行提交和push,CI就会做完剩下的事情。

测试简介

多数的大型软件都有“测试组件(test suite)”。您可能已经对测试的相关概念有所了解,但是我们觉得有些测试方法和测试术语还是应该再次提醒一下:

  • 测试组件:所有测试的统称
  • 单元测试:一种“微型测试”,用于对某个封装的特性进行测试
  • 集成测试:一种“宏观测试”,针对系统的某一大部分进行,测试其不同的特性或组件是否能协同工作
  • 回归测试:一种实现特定模式的测试,用于保证之前引起问题的 bug 不会再次出现
  • 模拟(Mocking): 使用一个假的实现来替换函数、模块或类型,屏蔽那些和测试不相关的内容。例如,您可能会“模拟网络连接” 或 “模拟硬盘”

练习

  1. 大多数的 makefiles 都提供了 一个名为 clean 的构建目标,这并不是说我们会生成一个名为clean的文件,而是我们可以使用它清理可以被make重新创建的文件。您可以理解为它的作用是“撤销”所有构建步骤。在上面的 makefile 中为paper.pdf实现一个clean 目标。您需要将构建目标设置为phony。可以使用 git ls-files 的子命令。其他一些有用的 make 构建目标可以访问这个网站:https://www.gnu.org/software/make/manual/html_node/Standard-Targets.html#Standard-Targets
  2. 指定版本要求的方法很多,让我们学习一下 Rust的构建系统的依赖管理:https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html。大多数的包管理仓库都支持类似的语法。对于每种语法(尖号、波浪号、通配符、比较、乘积),构建一种场景使其具有实际意义
  3. Git 可以作为一个简单的 CI 系统来使用,在任何 git 仓库中的 .git/hooks 目录中,您可以找到一些文件(当前处于未激活状态),它们的作用和脚本一样,当某些事件发生时便可以自动执行。请编写一个pre-commit (https://git-scm.com/docs/githooks#_pre_commit)钩子,它会在提交前执行 make paper.pdf并在出现构建失败的情况拒绝您的提交。这样做可以避免产生包含不可构建版本的提交信息
  4. 基于 GitHub Pages 创建任意一个可以自动发布的页面。添加一个GitHub Action 到该仓库,对仓库中的所有 shell 文件执行 shellcheck(方法之一);
  5. 构建属于您的 GitHub action,对仓库中所有的.md文件执行proselint 或 write-good,在您的仓库中开启这一功能,提交一个包含错误的文件看看该功能是否生效。

喜欢本文的话不要忘记三连~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Coder梁 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 构建系统
  • 依赖管理
  • 持续集成系统
    • 测试简介
    • 练习
    相关产品与服务
    持续集成
    CODING 持续集成(CODING Continuous Integration,CODING-CI)全面兼容 Jenkins 的持续集成服务,支持 Java、Python、NodeJS 等所有主流语言,并且支持 Docker 镜像的构建。图形化编排,高配集群多 Job 并行构建全面提速您的构建任务。支持主流的 Git 代码仓库,包括 CODING 代码托管、GitHub、GitLab 等。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档