#task #workflow #branch #input #variables #traversal #output

heron-rebuild-traverse

为heron-rebuild提供工作流程图遍历实用工具

1 个不稳定版本

0.1.0 2024年7月29日

#821 in 算法

Download history 116/week @ 2024-07-26 14/week @ 2024-08-02

130 每月下载次数
heron-rebuild 中使用

MPL-2.0 许可证

85KB
2K SLoC

heron-rebuild (hr)

heron-rebuild 是一种基于工作流程的构建系统,旨在设计复杂的、分叉的构建工作流程。它的配置文件语法和整体设计基于 ducttapeducttape 是为构建AI系统而设计的,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选项,但可以省略-c选项,在这种情况下,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_textreplace_text的子目录,在每个这些目录中都有一个由配置文件中的bash代码创建的.txt文件。

这两个任务都没有任何分支功能,但如果它们有,我们将在每个分支的realizations中看到多个子目录(当一个任务在特定的工作流程分支上运行时,该任务被实现)。

请注意,默认情况下,所有输出都写入由hr调用的目录中的output。这可以通过使用-o|--output选项来覆盖。

请注意,hr在每个任务的目录中创建了几个附加文件

  • exit_code:当任务完成时写入,这样我们可以在稍后检查它是否成功
  • stderr.txtstdout.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) 分支包含两个 分支点ProfileOs,其中 Profile 设置为 debugOs 设置为 mac

目前,在计划中仅允许一个目标任务和一个分支——每个分支点一个值。放宽此限制在我们的路线图上,一旦完成,您将能够指定例如 (Profile: debug) * (Os: mac windows) 以运行具有 macwindows 分支的工作流程。在此期间,您可能需要设置配置文件中的两个计划,如下所示

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/myrustlibtarget/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 的变量,并使用其值。还有一些其他缩写在这里没有涉及。有关更多信息,请参阅示例目录。

输入(<

任务输入是文件,它们与其他任务值的主要区别在于在任务运行之前会检查其存在性。如果任务定义的任何输入文件在任务运行之前不存在,则执行停止。workflow 不关心它们是文件还是目录,只关心它们是否存在。

与其他值一样,它们可以分支或接合。

输出(>

任务输出是文件,它们与其他任务值的主要区别在于在任务运行后会检查其存在性。如果任务定义的任何输出文件在任务运行之后不存在,则任务被视为失败,执行停止。workflow 不关心输出是文件还是目录,只关心它们是否存在。并且,与其他值一样,它们可以分支或接合。

输出应定义为相对路径,相对于任务代码将运行的目录(稍后详细介绍任务目录)。

参数(::

参数在任何时候都不会检查其存在性。它们可以定义为文本字符串,或引用在别处定义的配置值,但不能作为任务输出。

模块(@

模块只是一个由一个@符号开头的一组标识符,例如@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)

lib.rs:

此模块中的函数遍历Workflow中的任务,返回一个可以由exec模块中的结构体运行的有序任务列表。

遍历是在3个步骤中创建的

  1. 从目标节点开始反向执行BFS搜索,添加所有必要的先决任务。
  2. 反转任务列表,修正任务间的链接。
  3. 通过任务前进,移除已经被移除的分支点。

最终,您将得到一个有序的任务遍历,其中只包含唯一标识每个任务所需的最小分支点集合。遍历中可能包含重复的任务,这些任务将在后续步骤中被移除。在过程中,我们部分解决任务变量(输入、输出和参数)。我们只部分解决它们,因为我们仍然不知道任务执行目录在磁盘上的实际路径;这些路径将由crate::prep中的结构体提供。

依赖项

~2–13MB
~100K SLoC