未提供项目描述
项目描述
概述
这是一个具有更简单语法和一些有用功能的 make
替代品。
示例
你好,世界!
要创建一个buildtool项目,在你的项目根目录放置一个 WORKSPACE
文件。目前,它可以保持为空。运行 git init
在项目根目录中初始化一个git仓库。
然后你可以在 BUILD
文件中定义构建规则。想象一个简单的项目,有一个源文件:src/main.c
,它可以编译为 gcc main.c -c -o ../build/a.out
产生输出文件 build/a.out
。我们的项目结构如下
- WORKSPACE
- src/
- main.c
- BUILD
我们在 src/
目录中放置了一个 BUILD
文件。我们将在该 BUILD
文件中声明一个名为 main
的规则,用于构建 main.c
并生成 a.out
。
# src/BUILD
def impl(ctx):
ctx.sh("mkdir -p ../build")
ctx.sh("gcc main.c -c -o ../build/a.out")
callback(
name="main",
deps=["main.c"],
impl=impl,
out="../build/a.out",
)
让我们看看这个文件做了什么。callback
通知 buildtool
我们正在声明一个名为 name="main"
的规则。规则的 deps
是规则运行所需的文件。out
是规则从其依赖项产生的文件(或文件列表)。最后,规则的实现 impl
描述了规则执行以从其输入生成输出的操作。
传递给回调的路径相对于包含 BUILD
文件的文件夹。
实现函数只接受一个参数:构建上下文 ctx
。在定义规则实现时,不要直接使用像 os.system
这样的函数 - 除了在 ctx
中调用方法之外,所有 impl
函数都必须是无副作用的。在这里,ctx.sh(...)
在包含 BUILD
文件夹的目录中运行shell命令。
现在我们可以运行这个规则。在项目中运行 buildtool main
。输出文件 build/a.out
应该已经出现。
到目前为止,我们还没有做任何只有简单的 Makefile 才能做的事情。
然而,现在尝试修改构建规则,使deps=[]
,然后重新运行buildtool main
(或者简称为bt main
)。我们得到了一个错误
error: no such file or directory: 'main.c'
尽管src/main.c
显然仍然在我们的仓库中。构建工具在单独的“沙盒”目录中运行所有构建,该目录只提供明确声明的依赖项。这意味着当在沙盒目录中运行时,指定规则依赖项时(几乎)不可能出错,因为构建将失败。
通用规则
现在假设我们在src/
中有一个第二个源文件another.c
,并希望将其编译为build/b.out
。一种方法是将现有的main
构建规则复制粘贴,并为生成b.out
创建第二个规则。然而,我们可以定义一个通用构建规则,并且声明两次——一次用于我们的两个目标中的每一个。
可以这样操作
# src/BUILD
def declare(*, name: str, src: str, out: str):
def impl(ctx):
ctx.sh("mkdir -p ../build")
ctx.sh(f"gcc {src} -c -o {out}")
callback(
name=name,
deps=[src],
impl=impl,
out=out,
)
declare(name="main", src="main.c", out="../build/a.out")
declare(name="another", src="another.c", out="../build/b.out")
declare
函数只是一个标准的Python函数,当src/BUILD
被评估时,callback()
会被declare()
调用两次来声明我们的规则。
加载文件
在较大的仓库中,将这些通用规则移动到单独的文件中可能是有意义的。让我们在这里这样做,在相同的src/
目录中创建一个rules.py
文件,因此我们的文件层次结构现在看起来像
- WORKSPACE
- src/
- main.c
- another.c
- BUILD
- rules.py
我们将declare
移动到rules.py
中,因此
# src/rules.py
def declare(*, name: str, src: str, out: str):
def impl(ctx):
ctx.sh("mkdir -p ../build")
ctx.sh(f"gcc {src} -c -o {out}")
return callback(
name=name,
deps=[src],
impl=impl,
out=out,
)
要从rules.py
中导入它,我们可以使用load()
函数,如下所示
# src/BUILD
rules = load("rules.py")
rules.declare(name="main", src="main.c", out="../build/a.out")
rules.declare(name="another", src="another.c", out="../build/b.out")
传递给load()
的路径相对于加载文件。也可以使用*.py
文件加载其他*.py
文件,只要没有循环。不可能从一个文件中加载一个BUILD
文件。此外,只能从BUILD
文件中声明规则。现在我们可以运行bt main
或bt another
来生成a.out
和b.out
。
规则依赖
除了依赖其他文件外,规则还可以依赖其他规则。与依赖文件不同,依赖其他规则不会改变在沙盒目录中运行构建时提供的文件。相反,如果规则A
依赖于规则B
,那么每次我们构建规则A
时,规则B
都会被保证构建。
例如,我们可能希望有一个构建目标来一起构建main
和another
。可以这样操作
# src/BUILD
rules = load("rules.py")
rules.declare(name="main", src="main.c", out="../build/a.out")
rules.declare(name="another", src="another.c", out="../build/b.out")
callback(
name="all",
deps=[":main", ":another"],
)
通过在依赖项名称前添加一个:
,我们表明它是规则名称,而不是文件名称。在命令行中运行构建时,我们也可以使用这种语法来区分具有相同名称的规则和文件(例如bt :all
),但如果目标可以被明确解决,则不是必需的。
现在,运行bt all
将构建a.out
和b.out
。
如果将name
传递给callback()
,它将返回:<name>
。这让我们可以避免重复规则名称,如下所示
# src/BUILD
rules = load("rules.py")
callback(
name="all",
deps=[
rules.declare(name="main", src="main.c", out="../build/a.out"),
rules.declare(name="another", src="another.c", out="../build/b.out")
],
)
然而,避免以这种方式“嵌套”规则是一种良好的做法。
路径和通配符
与其为src/
中的每个.c
文件编写一个单独的规则,我们可能希望自动声明构建它们的规则。这可以通过使用find()
函数来完成,该函数允许我们进行文件通配符匹配,如下所示
# src/BUILD
from os.path import basename
rules = load("rules.py")
all_rules = []
for src in find("*.c"):
name = basename(src)[:-2]
all_rules.append(rules.declare(name=name, src=src, out=f"../build/{name}.out"))
callback(
name="all",
deps=all_rules,
)
传递给find()
的路径相对于包含BUILD
文件的目录。我们也可以传入相对于项目根的路径,通过在它们前面添加//
。因此,我们可以用find("//src/*.c")
来等效地代替find("*.c")
。这种相对于项目根的路径语法可以在需要路径的其他地方使用,例如deps
元素、out
的值或load()
的参数。
如果我们运行bt main
,我们将得到以下错误
subprocess.CalledProcessError: Command '['gcc //src/main.c -o ../build/main.out']' returned non-zero exit status 1.
我们发现find()
返回了一个相对于项目根的路径,这个路径不能直接传递给shell。一个解决方案是在rules.py
中再次使用os.path.basename
来提取文件名main.c
。然而,如果我们以后尝试使用我们的规则来编译子文件夹中的文件,这将会引起问题。相反,存在一个方法ctx.relative()
,它接受任何格式的路径,并以实现中的工作目录输出相对路径。
我们可以使用这个方法来修改rules.py
如下
# src/rules.py
def declare(*, name: str, src: str, out: str):
def impl(ctx):
ctx.sh("mkdir -p ../build")
ctx.sh(f"gcc {ctx.relative(src)} -c -o {ctx.relative(out)}")
return callback(
name=name,
deps=[src],
impl=impl,
out=out,
)
目前,我们不会考虑更新mkdir
调用以支持子目录。现在bt all
应该可以正常工作。
动态依赖
有时,我们事先不知道规则的所有依赖。例如,假设main.c
依赖于another.c
。如果我们运行bt main
,我们会得到错误
main.c:2:10: fatal error: 'another.c' file not found
因为在运行构建时,只有明确声明的依赖项可用。
一个解决方案是更新declare()
以接受依赖项列表并手动指定main.c
依赖于another.c
。或者,我们可以在运行构建时动态添加依赖项。
首先,我们需要知道如何检测依赖项。如果我们运行gcc main.c -MM
,我们获得
$ gcc main.c -MM
main.o: main.c another.c
这是Makefile可接受的格式,但我们需要处理它以提取原始文件名。我们可以通过修改rules.py
来实现
# src/rules.py
def declare(*, name: str, src: str, out: str):
def impl(ctx):
ctx.sh("mkdir -p ../build")
raw_deps = ctx.input(sh=f"gcc {ctx.relative(src)} -MM")
deps = raw_deps.strip().split(" ")[1:]
ctx.add_deps(deps)
ctx.sh(f"gcc {ctx.relative(src)} -c -o {ctx.relative(out)}")
return callback(
name=name,
impl=impl,
out=out,
)
首先,我们使用ctx.input(sh=...)
运行一个shell命令并读取stdout。在解析输出以确定依赖的文件之后,我们使用ctx.add_deps()
将它们添加为动态依赖,替换之前传递给callback()
的deps=[src]
。最后,我们按照之前的方式运行标准的编译。注意,初始的ctx.input()
调用依赖于src
,但它是在之后才被添加为依赖的之后。这是允许的,只要在impl()
完成后,所有曾经使用过的依赖都已被添加。
现在,bt all
成功构建了目标文件。
工作区
我们现在能够构建一个简单的项目。当管理大型项目时,自动设置构建环境也很有用,这样用户就可以克隆存储库,运行buildtool
,并获得构建输出而无需任何手动配置。这就是WORKSPACE
文件的作用。
在WORKSPACE
文件中,从buildtool
可用一个新的导入:config
。一个简单的WORKSPACE
文件可能看起来像这样
# WORKSPACE
config.register_default_build_rule(":all")
config.register_output_directory("build")
config.require_buildtool_version("0.1.25")
默认的构建规则是当在未指定规则的项目目录中运行bt
时调用的规则。输出目录是当运行bt --clean
时被清理的目录,以删除之前的构建工件(可以注册多个输出目录)。最后,可以要求最低的buildtool
版本,以便如果使用旧版本构建项目,将打印出清晰的错误消息,指导用户更新。
此外,我们可以在WORKSPACE
文件中声明设置规则。与构建规则不同,设置规则不在沙盒目录中运行,因此它们的依赖项不会被自动强制执行。虽然它们必须指定其输出,因为它们在主项目目录中运行,但也不会被验证。与构建规则不同,设置规则不能使用ctx.add_deps()
,而必须静态指定其依赖项。
例如,假设 gcc
不在 /usr/bin/
目录中,而是在 PATH
的其他位置。在构建规则中,PATH
被规范化为 /usr/bin/
,因此我们之前的规则不会工作,因为 shell 找不到 gcc
。相反,我们将使用设置规则来检测 gcc
,并将从 //env/bin/gcc
到机器上的任何位置的符号链接添加。然后我们将在构建规则中使用此符号链接来编译我们的 *.c
文件。
我们按照以下方式修改我们的 WORKSPACE
文件
# WORKSPACE
def declare_gcc_symlink():
def impl(ctx):
target = ctx.input(sh=f"which gcc").strip()
ctx.sh("mkdir -p env/bin")
ctx.sh("rm -f env/bin/gcc")
ctx.sh(f"ln -s {target} env/bin/gcc")
return callback(
name="gcc",
impl=impl,
out="env/bin/gcc",
)
callback(
name="init",
deps=[declare_gcc_symlink()]
)
config.register_default_setup_rule(":init")
config.register_default_build_rule(":all")
config.register_output_directory("build")
config.require_buildtool_version("0.1.25")
请注意,declare_gcc_symlink()
的 impl()
在重新运行之前会清除任何以前的输出,因为它直接在项目目录中运行,而不是在沙盒中。此外,请注意,我们在配置中注册了一个 default_setup_rule
。如果注册了此类规则,buildtool 将确保在构建任何后续目标之前构建它。
要单独运行 gcc
设置规则,请运行 bt setup:gcc
。与构建规则不同,我们不能通过运行 bt env/bin/gcc
来重新生成文件 - 我们只能通过命令行名称来运行设置规则。因此,所有设置规则都必须有一个名称,尽管它们可以 依赖 于源文件或由其他设置规则构建的文件。
接下来,我们将修改 rules.py
以使用 //env/bin/gcc
,而不是 /usr/bin/gcc
。我们不会将此新路径硬编码到 ctx.sh
中,而是将 ctx.sh()
使用的 PATH
修改为首先查找 //env/bin
,然后是 /bin
,而不是 /usr/bin
。可以这样做:
# src/rules.py
ENV = dict(PATH=["@//env/bin/", "/bin"])
def declare(*, name: str, src: str, out: str):
def impl(ctx):
ctx.sh("mkdir -p ../build", env=ENV)
raw_deps = ctx.input(sh=f"gcc {ctx.relative(src)} -MM", env=ENV)
deps = raw_deps.strip().split(" ")[1:]
ctx.add_deps(deps)
ctx.sh(f"gcc {ctx.relative(src)} -c -o {ctx.relative(out)}", env=ENV)
return callback(
name=name,
impl=impl,
out=out,
)
当我们使用 @//
前缀路径时,这意味着路径相对于项目根目录,即使构建在沙盒中运行也是如此。相比之下,如果一个路径以 //
前缀开头,则在沙盒构建时,它被视为相对于沙盒根目录。路径只能在环境变量中使用 @//
前缀,不能在其他地方使用。
请注意,我们没有在 ENV
中将 PATH
定义为字符串,而是使用构建工具语法作为路径列表。构建工具将自动将这些路径解析为绝对路径并将它们连接起来,形成一个将传递到 shell 环境的字符串。这样做是为了确保在构建规则中永远不会直接处理绝对路径 - 应该避免使用绝对路径,因为它们会导致缓存问题。如果需要在 shell 命令中作为一部分使用绝对路径,则可以将其添加到环境变量中,然后使用 shell 语法访问它。
现在我们可以运行 bt all
来重新生成输出。请注意,已创建一个 env/
文件夹,其中包含 gcc
符号链接。传统上,我们不注册 env/
文件夹(或由设置规则构建的其他目标)为输出目录,因为它不太可能是用户在运行 bt --clear
时希望清除的。
项目详细信息
下载文件
下载适用于您平台的文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。