如何使用 workspace_status_command 的输出构建自定义规则?

How can I build custom rules using the output of workspace_status_command?

bazel build 标志 --workspace_status_command 支持调用脚本来检索例如存储库元数据,这也称为构建标记,可在 java_binary.

等规则中使用

我想使用此元数据创建自定义规则。 我想将其用于通用支持功能。它应该接收 git 版本和一些其他属性,并创建一个 version.go 可用作依赖项的输出文件。

所以我开始了在各种 bazel 存储库中查看规则的旅程。

rules_docker这样的规则支持在container_image中使用stamp标记,并让您在属性中引用状态输出。

rules_gogo_binaryx_defs属性中支持它。

这对我的目的来说是理想的,我深入研究了...

看来我可以用 ctx.actions.expand_template using the entries in ctx.info_file or ctx.version_file 作为 substitutions 的字典来得到我想要的东西。但是我没有弄清楚如何获取这些文件的字典。这两个文件似乎是 "unofficial",它们不是 ctx 文档的一部分。

基于我已经发现的内容:如何根据状态命令输出获得 dict

如果那不可能,shortest/simplest 访问自定义规则的 workspace_status_command 输出的方法是什么?

我一直在你所在的地方,我最终沿着你开始探索的道路前进。我生成了一个 JSON 描述,其中还包括从 git 收集的信息以与结果打包在一起,我最终做了这样的事情:

def _build_mft_impl(ctx):
    args = ctx.actions.args()
    args.add('-f')
    args.add(ctx.info_file)
    args.add('-i')
    args.add(ctx.files.src)
    args.add('-o')
    args.add(ctx.outputs.out)
    ctx.actions.run(
        outputs = [ctx.outputs.out],
        inputs = ctx.files.src + [ctx.info_file],
        arguments = [args],
        progress_message = "Generating manifest: " + ctx.label.name,
        executable = ctx.executable._expand_template,
    )

def _get_mft_outputs(src):
    return {"out": src.name[:-len(".tmpl")]}

build_manifest = rule(
        implementation = _build_mft_impl,
        attrs = {
            "src": attr.label(mandatory=True,
                              allow_single_file=[".json.tmpl", ".json_tmpl"]),
            "_expand_template": attr.label(default=Label("//:expand_template"),
                                           executable=True,
                                           cfg="host"),
        },
        outputs = _get_mft_outputs,
    )

//:expand_template 在我的例子中是一个标签,指向 py_binary 执行转换本身。我很乐意了解一种更好的(更原生的,更少的跃点)方法来做到这一点,但是(现在)我选择了:它有效。对该方法和您的担忧的评论很少:

  • 据我所知,您无法读入(文件并在 Skylark 中执行操作)本身...
  • ...说到这里,无论如何,将转换(工具)和构建描述(bazel)分开可能不是一件坏事。
  • 官方文档的构成可能存在争议,但ctx.info_file可能不会出现在参考手册中,它在源代码树中有记载。 :) 其他领域也是如此(我希望这不是因为那些接口被认为还没有提交)。

为了 src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkRuleContextApi.java 中的完整性,有:

@SkylarkCallable(
  name = "info_file",
  structField = true,
  documented = false,
  doc =
  "Returns the file that is used to hold the non-volatile workspace status for the "
      + "current build request."
)
public FileApi getStableWorkspaceStatus() throws InterruptedException, EvalException;

编辑:评论中要求的一些额外细节。

在我的 workspace_status.sh 中,例如下面一行:

echo STABLE_GIT_REF $(git log -1 --pretty=format:%H)

在我的 .json.tmpl 文件中我会有:

"ref": "${STABLE_GIT_REF}",

我选择了 shell 类似要替换的文本符号,因为它对许多用户来说很直观并且易于匹配。

至于替换,实际代码的相关(CLI 不包括在内)部分为:

def get_map(val_file):
    """
    Return dictionary of key/value pairs from ``val_file`.
    """
    value_map = {}

    for line in val_file:
        (key, value) = line.split(' ', 1)
        value_map.update(((key, value.rstrip('\n')),))
    return value_map


def expand_template(val_file, in_file, out_file):
    """
    Read each line from ``in_file`` and write it to ``out_file`` replacing all
    ${KEY} references with values from ``val_file``.
    """
    def _substitue_variable(mobj):
        return value_map[mobj.group('var')]
    re_pat = re.compile(r'${(?P<var>[^} ]+)}')
    value_map = get_map(val_file)
    for line in in_file:
        out_file.write(re_pat.subn(_substitue_variable, line)[0])

EDIT2:这就是 Python 脚本如何将 python 脚本暴露给其余的 bazel。

py_binary(
    name = "expand_template",
    main = "expand_template.py",
    srcs = ["expand_template.py"],
    visibility = ["//visibility:public"],
)

基于 Ondrej 的回答,我现在使用这样的东西(在 SO 编辑器中改编,可能包含小错误):

tools/bazel.rc:

build --workspace_status_command=tools/workspace_status.sh

tools/workspace_status.sh:

echo STABLE_GIT_REV $(git rev-parse HEAD)

version.bzl:

_VERSION_TEMPLATE_SH = """
set -e -u -o pipefail

while read line; do
  export "${line% *}"="${line#* }"
done <"$INFILE" \
&& cat <<EOF >"$OUTFILE"
{ "ref": "${STABLE_GIT_REF}"
, "service": "${SERVICE_NAME}"
}
EOF
"""

def _commit_info_impl(ctx):
  ctx.actions.run_shell(
      outputs = [ctx.outputs.outfile],
      inputs = [ctx.info_file],
      progress_message = "Generating version file: " + ctx.label.name,
      command = _VERSION_TEMPLATE_SH,
      env = {
        'INFILE': ctx.info_file.path,
        'OUTFILE': ctx.outputs.version_go.path,
        'SERVICE_NAME': ctx.attr.service,
      },
  )

commit_info = rule(
    implementation = _commit_info_impl,
    attrs = {
      'service': attr.string(
          mandatory = True,
          doc = 'name of versioned service',
      ),
    },
    outputs = {
      'outfile': 'manifest.json',
    },
)