跳转到主要内容

未提供项目描述

项目描述

概述

这是一个具有更简单语法和一些有用功能的 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 mainbt another来生成a.outb.out

规则依赖

除了依赖其他文件外,规则还可以依赖其他规则。与依赖文件不同,依赖其他规则不会改变在沙盒目录中运行构建时提供的文件。相反,如果规则A依赖于规则B,那么每次我们构建规则A时,规则B都会被保证构建。

例如,我们可能希望有一个构建目标来一起构建mainanother。可以这样操作

# 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.outb.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 时希望清除的。

项目详细信息


发布历史 发布通知 | RSS 源

下载文件

下载适用于您平台的文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。

源分布

buildtool-0.2.9.tar.gz (49.4 kB 查看哈希值)

上传时间

构建分布

buildtool-0.2.9-py3-none-any.whl (58.9 kB 查看哈希值)

上传于 Python 3

由以下支持