1 个不稳定版本
0.1.0 | 2024年7月29日 |
---|
在 构建工具 中排名 #432
每月下载量 140 次
被 3 个 crate 使用
11KB
271 行
heron-rebuild (hr
)
heron-rebuild
是一个基于工作流程的构建系统,旨在处理复杂的分支构建工作流程。它的配置文件语法和整体设计基于 ducttape。 ducttape
是为了构建人工智能系统而设计的,而 heron-rebuild
也可以帮助那里,但它主要是为了复杂的软件构建而设计的。
为什么?
我们的音频插件,例如 pgs-1,由一个核心 Rust 库和两个用 C++ 编写的不同插件格式(VST 和 AudioUnit)组成,分别针对两个不同的操作系统和两种不同的 CPU 架构。仅为了构建 Mac 版本,我们需要
cargo build
Rust 库两次,一次用于x86_64
,一次用于aarch64
- 使用
lipo
将两个 Rust 库合并成一个通用库 - 构建 C++ 封装库两次,分别针对每种格式,链接到 Rust 库并使用 Rust 头文件
- 为每种格式构建可以加载到 DAW 中的插件包
- 使用
pkgbuild
为每种插件格式构建一个.pkg
安装程序 - 使用
productbuild
构建一个可以同时安装两种插件格式的组合.pkg
安装程序
以 DAG 的形式写出,这可能看起来像
cargo_build[x86_64] cpp_build[vst] – pkgbuild[vst]
\ / \
lipo productbuild
/ \ /
cargo_build[aarch64] cpp_build[au] – pkgbuild[au]
这些步骤可以在调试或发布模式下运行,并且应取决于其输入的相应调试或发布版本。当为Windows构建时(例如,使用cross而不是cargo build
),我们需要运行不同的步骤集。我们没有展示这一点,但我们还使用cbindgen从我们的Rust代码生成头文件,以便C++构建可以依赖。
我们希望只定义这些步骤一次,并根据我们在DAG的哪个分支上,用不同的值对这些步骤进行参数化。不幸的是,我们的脚本目录无法处理这一点。
heron-rebuild
是什么
它是一系列带有依赖关系的bash片段,可以作为单个工作流程运行。这些片段称为“任务”,看起来像这样
task cargo_build
> rustlib="target/$target/$profile/$lib_name"
:: target=(Target: x86_64=x86_64-apple-darwin aarch64=aarch64-apple-darwin)
:: profile=(Profile: debug release)
:: release_flag=(Profile: debug="" release="--release")
{
cargo build $release_flag --target $target
}
这些括号中的值是分支点;它们告诉我们,在工作流程的一个分支上,我们将使用--target x86_64-apple-darwin
调用cargo,而在另一个分支上,我们将运行相同的命令,只是使用--target aarch64-apple-darwin
。
> rustlib="target/$target/$profile/$lib_name
告诉我们这个片段生成一个名为target/$target/$profile/$lib_name
的输出文件,我们可以用$rustlib
这个名字在其他片段中引用。
它不是什么
它不是真正的构建系统;它对编译代码一无所知,例如,它也不会检查源文件是否发生变化以确定是否需要编译它们。它只是将不同的bash命令组合在一起,并确保它们满足依赖关系。
它也不是一个功能齐全的命令运行器;为此,我们推荐just。
使用heron-rebuild
> hr -h
Usage: hr [OPTIONS]
Options:
-c, --config <FILE> Workflow definition file [env: HERON_REBUILD_CONFIG=] [default: rebuild.hr]
-p, --plan <PLAN> Name of target plan
-t, --task <TASK> Name of target task
-x, --invalidate Invalidate specified task
-o, --output <DIR> Output directory [env: HERON_REBUILD_OUTPUT=] [default: output]
-y, --yes Bypass user confirmation
-v, --verbose Print additional debugging info
-b, --branch <K1.V1[+K2.V2]> Target branch
-B, --baseline Use baseline branch ('-b Baseline.baseline')
-n, --dry-run Dry run; print info but don't modify anything
-h, --help Print help
-V, --version Print version
一个典型的heron-rebuild
调用可能看起来像这样
> hr -p main -c rebuild.hr
这告诉hr
运行在名为rebuild.hr
的配置文件中定义的名为“main”的计划中的任务。在运行工作流程时,始终需要-p
选项,但-
选项可以省略,在这种情况下,hr
将在当前目录中查找名为rebuild.hr
的文件,如果它存在,则使用它。
配置文件看起来像这样
> cat rebuild.hr
plan main {
reach replace_text
}
task write_text > output=write_text_output.txt {
echo "foo" > $output
}
task replace_text < input=$output@write_text > output=replace_text_output.txt {
cat $input | sed 's/foo/bar/' > $output
}
让我们运行上面的命令,并使用上面的配置文件,看看会发生什么
> hr -p main -c rebuild.hr
[command output omitted for brevity]
> tree output
├── branchpoints.txt
├── replace_text
│ ├── Baseline.baseline -> realizations/Baseline.baseline
│ └── realizations
│ └── Baseline.baseline
│ ├── exit_code
│ ├── replace_text_output.txt
│ ├── stderr.txt
│ ├── stdout.txt
│ └── task.sh
└── write_text
├── Baseline.baseline -> realizations/Baseline.baseline
└── realizations
└── Baseline.baseline
├── exit_code
├── write_text_output.txt
├── stderr.txt
├── stdout.txt
└── task.sh
> cat output/write_text/Baseline.baseline/write_text_output.txt
foo
> cat output/replace_text/Baseline.baseline/replace_text_output.txt
bar
这里发生的事情是,hr
创建了一个名为output
的目录,其中包含两个任务write_text
和replace_text
的子目录,在每个子目录中都有一个.txt
文件,该文件是由运行配置文件中的bash代码创建的。
这些任务中没有一个有任何分支功能,但如果它们有,我们会在每个分支的realizations
中看到多个子目录(当一个任务在工作流程的特定分支上运行时,该任务是实现的)。
请注意,所有输出默认都写入到由hr
调用目录下的output
目录中。这可以通过-o|--output
选项来覆盖。
另外请注意,hr
在各个任务的目录中创建了几个额外的文件
exit_code
:任务完成后会写入此信息,这样我们就可以稍后检查它是否成功stderr.txt
和stdout.txt
:捕获并保存bash代码的所有输出(在任务执行时它们也同时写入控制台)task.sh
:一个包含产生此任务输出的所有命令的shell脚本(在执行任务时实际上并没有使用,但作为调试的归档文件存在)
重新运行heron-rebuild
每次调用hr
时,它都会检查输出目录中已经完成的任务,并在可能的情况下使用它们的输出而不重新运行它们。如果在工作流程执行过程中任务的bash代码失败,整个工作流程执行将停止,但任何成功的任务仍然可以重用。在此阶段,您可以纠正错误,再次调用hr
,并完成工作流程的执行,而无需重新执行任何先前的成功步骤。
如果您想强制 hr
重新运行已经成功完成的任务,请参阅下面的使任务无效部分。
语法概述
# you must have at least one plan:
plan plan_name {
reach task_name
}
# variables defined in a global block are available to all tasks:
global {
unquoted_literal=values/can_be_unquoted
quoted_literal="values can be in double quotes"
interpolated="values can interpolate variables, like $unquoted_literal"
task_output=$output@task_name
}
task task_name
< literal_input=/home/me/some-file.txt
> ouput=relative/to/task
:: param=$unquoted_literal
{
echo $unquoted_literal
mkdir -p $(dirname $output)
cp $literal_input $output
}
详细语法
计划
计划由三部分组成:一个名称、一个目标任务和一个分支
plan plan_name {
reach goal_task via (Profile: debug) * (Os: mac)
}
目标任务通过reach
关键字引入,是我们将逆向工作以创建要执行的任务列表的任务:将引用goal_task
输入中的每个任务以及所有依赖项,最终在执行goal_task
后完成工作流程。
分支通过via
关键字引入,并使用交叉积表示法指定。上面的(Profile: debug) * (Os: mac)
分支包含两个分支点,即Profile
和Os
,其中Profile
设置为debug
,Os
设置为mac
。
目前,一个计划中只允许有一个目标任务和一个分支——每个分支点一个值。放宽此限制在我们的路线图上,一旦完成,您将能够指定例如(Profile: debug) * (Os: mac windows)
来运行具有mac
和windows
分支的工作流程。与此同时,您可能需要设置您的配置文件,如上所述使用两个计划
plan mac {
reach goal_task via (Profile: debug) * (Os: mac)
}
plan win {
reach goal_task via (Profile: debug) * (Os: windows)
}
值
工作流程中有几种类型的值
# literal values without spaces can be unquoted:
unquoted_var=literal_value_with_no_spaces
unquoted_var2=/literals/can/also/be/paths
# literal values can be written in double quotes
sentence="put a whole sentence in double quotes"
# values can refer to other values:
renamed=$unquoted_var
# interpolate variables in double quotes:
interpolated="the sentence above is: $sentence"
# the path to a task output can be specified with '@'.
# this variable contains the path to the output file "output_var_name" from the task "task_name":
task_output=$output_var_name@task_name
# values can be *branched* with parentheses.
# this variable has value "brew" on branch (Os: mac), "choco" on branch (Os: windows), etc.:
pkg_mgr=(Os: mac=brew windows=choco ubuntu=apt-get)
# when we want to create a value with the same name as a branchpoint, we can omit the value name.
# this assignment is shorthand for profile=(Debug: debug=debug release=release):
profile=(Debug: debug release)
# another example of a branched value using shorthand notation:
os=(Os: mac windows ubuntu)
# a value evaluated for a specific branch (a "branch graft") can be specified with brackets.
# this variable has the value of the variable "profile" on branch (Profile: release),
# regardless of which branch is currently being evaluated:
grafted=$profile[Profile: release]
# branch grafts can be combined with task outputs:
task_output_release=$output_var_name@task_name[Profile: release]
全局配置
值可以在一个global
块中指定,每行一个
global {
var1="hi there"
var2=$output@some_task
}
这些值然后可以在工作流程中的任何任务中使用。
任务
任务是工作流程文件中大部分逻辑所在的地方。它们看起来像这样
task cargo_build
@cargo
> lib="target/$profile/myrustlib"
:: release_flag=(Profile: debug="" release="--release")
{
cargo build $release_flag
}
这告诉hr
创建一个任务
- 在
cargo
模块目录中运行(模块将在下面解释) - 生成一个输出,该输出由
hr
所知,称为lib
,并位于target/$profile/myrustlib
(这将评估为target/debug/myrustlib
或target/release/myrustlib
,具体取决于我们正在评估哪个分支:(Profile: debug)
或(Profile: release)
)。 - 接受一个名为
release_flag
的参数,该参数的定义取决于我们是否处于(Profile: debug)
或(Profile: release)
状态。 - 运行命令
cargo build $release_flag
。
在开括号之前的内容被称为 任务头。任务头由一个名称和一个可选的值列表组成,用于定义任务如何运行及其与其他任务的关系。这些值可以是以下任意之一
- 任意数量的输入文件,使用
<
(上面未显示)指定。 - 任意数量的输出文件,使用
>
指定。 - 任意数量的参数,使用
::
指定。 - 零个或一个模块定义,使用
@
指定。
在头中定义的任何输入、输出和参数的变量名都可用于下面的代码块中(见上面代码中如何使用 $release_flag
)。
(除模块外)这些值使用与上面相同的值语法,例如
< input=(Branched: branch1=val1 branch2=val2)
> output1=$config_var_defined_in_a_global_block
> output2="put/$variable_interpolation/in/double-quotes"
:: param1=$grafted_variable[Branchpoint: branch1]
可以在空格分隔的列表中定义单个类型的多个任务值
< input1=foo input2=bar
并且可以在单行上定义这些列表的多个
< input1=foo input2=bar > output=output :: param1=x param2=y
此外,任务值中还有一些在全局块中不可用的缩写。任何没有等号的输入或输出值
< input
> output
将被解释为与
< input=input
> output=output
这在你例如不关心输出文件的名称,只关心它存在的情况下很有用。
参数可以用 @
符号指定,如下所示
param=@
这告诉 workflow
在全局块中查找名为 param
的变量,并使用其值。还有一些其他快捷方式我们在这里不会介绍,请参见示例目录以获取更多信息。
输入(<
)
任务输入是文件,它们与其他任务值的主要区别在于在任务运行之前会检查它们的存在。如果任务定义的任何输入文件在任务运行前不存在,则执行停止。《code>workflow 不关心它们是文件还是目录,只关心它们存在。
与其他值一样,它们可以分支或接枝。
输出(>
)
任务的输出是文件,它们与其他任务值的不同之处在于它们在任务运行后会进行存在性检查。如果一个任务的定义输出文件在任务运行后立即不存在,则认为任务失败,执行停止。 workflow
不关心输出是文件还是目录,只关心它们是否存在。而且,像其他值一样,它们可以进行分支或嫁接。
输出应定义为相对于任务代码将运行的目录的相对路径(关于任务目录的更多内容稍后介绍)。
params (::
)
在任何时候都不会检查参数的存在性。它们可以定义为文本字符串,或是对其他地方定义的配置值的引用,但不能是任务输出。
modules (@
)
模块只是一个前面带有 @
符号的单一标识符,例如 @cargo
。为了使类似于 task cargo_build @cargo
的任务头正常工作,必须在配置文件的某个地方定义一个名为 cargo
的模块,如下所示:
module cargo=/home/me/code/my-crate
通常,对于未指定 @module
的任务,workflow
将为每次任务执行创建一个新的目录并在其中运行其代码。所有输出都被假定为相对于这个新目录,并且依赖于它们的其他任务将期望在那里找到它们。
对于 @module
任务,workflow
将在模块目录中执行代码,然后将输出文件从模块目录复制回任务目录(其他任务可以在那里找到它们)。
这对于构建命令非常有用,这些命令依赖于存在于特定位置的源代码,并且我们不一定想每次运行工作流程时都将其复制到新目录中。请参阅 examples
以获取示例。
使任务无效
-x
标志告诉 hr
使已运行的某个任务无效
> hr -x -t pkgbuild -b Framework=vst
上述操作将使分支 (Framework: vst)
的 pkgbuild
任务无效。这意味着下次运行工作流程时,将重新运行该任务,无论它是否成功。由于它们的输入现在被认为是无效的,因此该任务的所有依赖任务也将重新运行。
指定任务名称的 -t
标志始终是必需的,但指定分支的 -b
标志是可选的。如果省略,hr
将使所有分支上的该任务的所有实现无效。
对于更复杂的分支,可以一起指定多个分支点,要么使用多个 -b
标志
> hr -x -t pkgbuild -b Framework=vst -b Profile=release
或者通过 +
将它们链接在一起
> hr -x -t pkgbuild -b Framework=vst+Profile=release
路线图
完成以下大部分内容应使我们达到 1.0 版本
- 更复杂的计划:多个目标节点,多个分支
- 提高测试覆盖率
- 更详尽的错误消息
- 允许在配置文件中导入,以便它们可以分散到多个文件中
- 用户主目录中的全局设置文件
- 在配置中允许使用内置变量,例如
$HOME
- 允许在命令行中覆盖配置值
- 允许用户在不执行的情况下通过命令行验证工作流
- 允许用户使用命令行选项检查工作流
- 扩展此文档
- 决定
hr
是否真的是一个好名字...
之后,我们可以看看如何扩展基本功能
- 添加执行代码在远程或容器中的选项
- 使用多线程同时执行任务
- 引入跨任务重用通用代码的方法
- 与终端复用器交互吗?
- 添加更多表达性的语法,例如将分支定义为范围(N: 0..10)
依赖关系
~1.8–2.5MB
~42K SLoC