Add DefaultOutputPathInfo provider and update write_source_files to accept it (#48)

Also update write_source_files to accept DirectoryPathInfo
This commit is contained in:
Greg Magolan 2022-03-14 17:33:52 -07:00 committed by GitHub
parent 7f2641cd07
commit 3b93ee0baa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 623 additions and 320 deletions

View File

@ -1,5 +1,5 @@
docs/*.md docs/*.md
lib/tests/jq/*.json lib/tests/jq/*.json
lib/tests/write_source_files/a2.js lib/lib/tests/write_source_files/*.js
lib/tests/write_source_files/b2.js lib/lib/tests/write_source_files/subdir/*.js
lib/tests/write_source_files/e_dir/e.js lib/lib/tests/write_source_files/subdir/subsubdir/*.js

View File

@ -48,6 +48,9 @@ stardoc_with_diff_test(
bzl_library_target = "//lib:directory_path", bzl_library_target = "//lib:directory_path",
) )
update_docs( stardoc_with_diff_test(
name = "update", name = "default_info_files",
bzl_library_target = "//lib:default_info_files",
) )
update_docs()

View File

@ -0,0 +1,49 @@
<!-- Generated with Stardoc: http://skydoc.bazel.build -->
A rule that provides file(s) from a given target's DefaultInfo
<a id="#default_info_files"></a>
## default_info_files
<pre>
default_info_files(<a href="#default_info_files-name">name</a>, <a href="#default_info_files-paths">paths</a>, <a href="#default_info_files-target">target</a>)
</pre>
A rule that provides file(s) from a given target's DefaultInfo
**ATTRIBUTES**
| Name | Description | Type | Mandatory | Default |
| :------------- | :------------- | :------------- | :------------- | :------------- |
| <a id="default_info_files-name"></a>name | A unique name for this target. | <a href="https://bazel.build/docs/build-ref.html#name">Name</a> | required | |
| <a id="default_info_files-paths"></a>paths | the paths of the files to provide in the DefaultInfo of the target relative to its root | List of strings | required | |
| <a id="default_info_files-target"></a>target | the target to look in for requested paths in its' DefaultInfo | <a href="https://bazel.build/docs/build-ref.html#labels">Label</a> | required | |
<a id="#make_default_info_files"></a>
## make_default_info_files
<pre>
make_default_info_files(<a href="#make_default_info_files-name">name</a>, <a href="#make_default_info_files-target">target</a>, <a href="#make_default_info_files-paths">paths</a>)
</pre>
Helper function to generate a default_info_files target and return its label.
**PARAMETERS**
| Name | Description | Default Value |
| :------------- | :------------- | :------------- |
| <a id="make_default_info_files-name"></a>name | unique name for the generated <code>default_info_files</code> target. | none |
| <a id="make_default_info_files-target"></a>target | the target to look in for requested paths in its' DefaultInfo | none |
| <a id="make_default_info_files-paths"></a>paths | the paths of the files to provide in the DefaultInfo of the target relative to its root | none |
**RETURNS**
The label `name`

View File

@ -53,7 +53,7 @@ Joins a label pointing to a TreeArtifact with a path nested within that director
make_directory_path(<a href="#make_directory_path-name">name</a>, <a href="#make_directory_path-directory">directory</a>, <a href="#make_directory_path-path">path</a>) make_directory_path(<a href="#make_directory_path-name">name</a>, <a href="#make_directory_path-directory">directory</a>, <a href="#make_directory_path-path">path</a>)
</pre> </pre>
Helper function to convert generate a directory_path target and return its label. Helper function to generate a directory_path target and return its label.
**PARAMETERS** **PARAMETERS**

View File

@ -87,8 +87,8 @@ If you have many sources that you want to update as a group, we recommend wrappi
| :------------- | :------------- | :------------- | | :------------- | :------------- | :------------- |
| <a id="write_source_files-name"></a>name | Name of the executable target that creates or updates the source file | none | | <a id="write_source_files-name"></a>name | Name of the executable target that creates or updates the source file | none |
| <a id="write_source_files-files"></a>files | A dict where the keys are source files or folders to write to and the values are labels pointing to the desired content. Sources must be within the same bazel package as the target. | <code>{}</code> | | <a id="write_source_files-files"></a>files | A dict where the keys are source files or folders to write to and the values are labels pointing to the desired content. Sources must be within the same bazel package as the target. | <code>{}</code> |
| <a id="write_source_files-additional_update_targets"></a>additional_update_targets | (Optional) List of other write_source_files targets to update in the same run | <code>[]</code> | | <a id="write_source_files-additional_update_targets"></a>additional_update_targets | (Optional) List of other write_source_file or other executable updater targets to call in the same run | <code>[]</code> |
| <a id="write_source_files-suggested_update_target"></a>suggested_update_target | (Optional) Label of the write_source_files target to suggest running when files are out of date | <code>None</code> | | <a id="write_source_files-suggested_update_target"></a>suggested_update_target | (Optional) Label of the write_source_file target to suggest running when files are out of date | <code>None</code> |
| <a id="write_source_files-kwargs"></a>kwargs | Other common named parameters such as <code>tags</code> or <code>visibility</code> | none | | <a id="write_source_files-kwargs"></a>kwargs | Other common named parameters such as <code>tags</code> or <code>visibility</code> | none |

View File

@ -63,6 +63,13 @@ bzl_library(
deps = ["//lib/private:directory_path"], deps = ["//lib/private:directory_path"],
) )
bzl_library(
name = "default_info_files",
srcs = ["default_info_files.bzl"],
visibility = ["//visibility:public"],
deps = ["//lib/private:default_info_files"],
)
bzl_library( bzl_library(
name = "copy_to_directory", name = "copy_to_directory",
srcs = ["copy_to_directory.bzl"], srcs = ["copy_to_directory.bzl"],
@ -80,9 +87,16 @@ bzl_library(
srcs = ["write_source_files.bzl"], srcs = ["write_source_files.bzl"],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
":diff_test",
":utils", ":utils",
"//lib/private:fail_with_message_test", "//lib/private:fail_with_message_test",
"//lib/private:write_source_files", "//lib/private:write_source_file",
"@bazel_skylib//rules:diff_test",
], ],
) )
bzl_library(
name = "diff_test",
srcs = ["diff_test.bzl"],
visibility = ["//visibility:public"],
deps = ["//lib/private:diff_test"],
)

View File

@ -0,0 +1,11 @@
"""A rule that provides file(s) from a given target's DefaultInfo
"""
load(
"//lib/private:default_info_files.bzl",
_default_info_files = "default_info_files",
_make_default_info_files = "make_default_info_files",
)
default_info_files = _default_info_files
make_default_info_files = _make_default_info_files

View File

@ -1,17 +1,3 @@
# Copyright 2019 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Rule and corresponding provider that joins a label pointing to a TreeArtifact """Rule and corresponding provider that joins a label pointing to a TreeArtifact
with a path nested within that directory with a path nested within that directory
""" """

View File

@ -10,6 +10,7 @@ bzl_library(
srcs = ["copy_to_directory.bzl"], srcs = ["copy_to_directory.bzl"],
visibility = ["//lib:__subpackages__"], visibility = ["//lib:__subpackages__"],
deps = [ deps = [
":default_info_files",
":directory_path", ":directory_path",
":paths", ":paths",
"@bazel_skylib//lib:paths", "@bazel_skylib//lib:paths",
@ -61,10 +62,14 @@ bzl_library(
) )
bzl_library( bzl_library(
name = "write_source_files", name = "write_source_file",
srcs = ["write_source_files.bzl"], srcs = ["write_source_file.bzl"],
visibility = ["//lib:__subpackages__"], visibility = ["//lib:__subpackages__"],
deps = ["//lib:utils"], deps = [
":default_info_files",
":directory_path",
"//lib:utils",
],
) )
bzl_library( bzl_library(
@ -79,3 +84,16 @@ bzl_library(
visibility = ["//lib:__subpackages__"], visibility = ["//lib:__subpackages__"],
deps = ["//lib:utils"], deps = ["//lib:utils"],
) )
bzl_library(
name = "default_info_files",
srcs = ["default_info_files.bzl"],
visibility = ["//lib:__subpackages__"],
deps = ["//lib:utils"],
)
bzl_library(
name = "diff_test",
srcs = ["diff_test.bzl"],
visibility = ["//lib:__subpackages__"],
)

View File

@ -127,7 +127,7 @@ def _copy_file_impl(ctx):
src_path = "/".join([src_file.path, ctx.attr.src[DirectoryPathInfo].path]) src_path = "/".join([src_file.path, ctx.attr.src[DirectoryPathInfo].path])
else: else:
if len(ctx.files.src) != 1: if len(ctx.files.src) != 1:
fail("src must be a single file or a target with a DirectoryPathInfo provider") fail("src must be a single file or a target that provides a DirectoryPathInfo")
src_file = ctx.files.src[0] src_file = ctx.files.src[0]
src_path = src_file.path src_path = src_file.path
if ctx.attr.is_windows: if ctx.attr.is_windows:

View File

@ -0,0 +1,70 @@
"""default_info_files implementation
"""
load("//lib:utils.bzl", _to_label = "to_label")
def _default_info_files(ctx):
files = []
for path in ctx.attr.paths:
file = find_short_path_in_default_info(
ctx.attr.target,
path,
)
if not file:
fail("%s file not found within the DefaultInfo of %s" % (ctx.attr.path, ctx.attr.target))
files.append(file)
return [DefaultInfo(
files = depset(direct = files),
runfiles = ctx.runfiles(files = files),
)]
default_info_files = rule(
doc = "A rule that provides file(s) from a given target's DefaultInfo",
implementation = _default_info_files,
attrs = {
"target": attr.label(
doc = "the target to look in for requested paths in its' DefaultInfo",
mandatory = True,
),
"paths": attr.string_list(
doc = "the paths of the files to provide in the DefaultInfo of the target relative to its root",
mandatory = True,
allow_empty = False,
),
},
provides = [DefaultInfo],
)
def make_default_info_files(name, target, paths):
"""Helper function to generate a default_info_files target and return its label.
Args:
name: unique name for the generated `default_info_files` target.
target: the target to look in for requested paths in its' DefaultInfo
paths: the paths of the files to provide in the DefaultInfo of the target relative to its root
Returns:
The label `name`
"""
default_info_files(
name = name,
target = target,
paths = paths,
)
return _to_label(name)
def find_short_path_in_default_info(default_info, short_path):
"""Helper function find a file in a DefaultInfo by short path
Args:
default_info: a DefaultInfo
short_path: the short path (path relative to root) to search for
Returns:
The File if found else None
"""
if default_info.files:
for file in default_info.files.to_list():
if file.short_path == short_path:
return file
return None

View File

@ -18,6 +18,8 @@ The rule uses a Bash command (diff) on Linux/macOS/non-Windows, and a cmd.exe
command (fc.exe) on Windows (no Bash is required). command (fc.exe) on Windows (no Bash is required).
""" """
load(":directory_path.bzl", "DirectoryPathInfo")
def _runfiles_path(f): def _runfiles_path(f):
if f.root.path: if f.root.path:
return f.path[len(f.root.path) + 1:] # generated file return f.path[len(f.root.path) + 1:] # generated file
@ -25,6 +27,24 @@ def _runfiles_path(f):
return f.path # source file return f.path # source file
def _diff_test_impl(ctx): def _diff_test_impl(ctx):
if DirectoryPathInfo in ctx.attr.file1:
file1 = ctx.attr.file1[DirectoryPathInfo].directory
file1_path = "/".join([_runfiles_path(file1), ctx.attr.file1[DirectoryPathInfo].path])
else:
if len(ctx.files.file1) != 1:
fail("file1 must be a single file or a target that provides a DirectoryPathInfo")
file1 = ctx.files.file1[0]
file1_path = _runfiles_path(file1)
if DirectoryPathInfo in ctx.attr.file2:
file2 = ctx.attr.file2[DirectoryPathInfo].directory
file2_path = "/".join([_runfiles_path(file2), ctx.attr.file2[DirectoryPathInfo].path])
else:
if len(ctx.files.file2) != 1:
fail("file2 must be a single file or a target that provides a DirectoryPathInfo")
file2 = ctx.files.file2[0]
file2_path = _runfiles_path(file2)
if ctx.attr.is_windows: if ctx.attr.is_windows:
test_bin = ctx.actions.declare_file(ctx.label.name + "-test.bat") test_bin = ctx.actions.declare_file(ctx.label.name + "-test.bat")
ctx.actions.write( ctx.actions.write(
@ -138,8 +158,8 @@ exit /b 0
exit /b 1 exit /b 1
""".format( """.format(
fail_msg = ctx.attr.failure_message, fail_msg = ctx.attr.failure_message,
file1 = _runfiles_path(ctx.file.file1), file1 = file1_path,
file2 = _runfiles_path(ctx.file.file2), file2 = file2_path,
), ),
is_executable = True, is_executable = True,
) )
@ -191,26 +211,26 @@ else
fi fi
""".format( """.format(
fail_msg = ctx.attr.failure_message, fail_msg = ctx.attr.failure_message,
file1 = _runfiles_path(ctx.file.file1), file1 = file1_path,
file2 = _runfiles_path(ctx.file.file2), file2 = file2_path,
), ),
is_executable = True, is_executable = True,
) )
return DefaultInfo( return DefaultInfo(
executable = test_bin, executable = test_bin,
files = depset(direct = [test_bin]), files = depset(direct = [test_bin]),
runfiles = ctx.runfiles(files = [test_bin, ctx.file.file1, ctx.file.file2]), runfiles = ctx.runfiles(files = [test_bin, file1, file2]),
) )
_diff_test = rule( _diff_test = rule(
attrs = { attrs = {
"failure_message": attr.string(), "failure_message": attr.string(),
"file1": attr.label( "file1": attr.label(
allow_single_file = True, allow_files = True,
mandatory = True, mandatory = True,
), ),
"file2": attr.label( "file2": attr.label(
allow_single_file = True, allow_files = True,
mandatory = True, mandatory = True,
), ),
"is_windows": attr.bool(mandatory = True), "is_windows": attr.bool(mandatory = True),

View File

@ -33,10 +33,11 @@ Otherwise there is no way to give a Bazel label for it.""",
mandatory = True, mandatory = True,
), ),
}, },
provides = [DirectoryPathInfo],
) )
def make_directory_path(name, directory, path): def make_directory_path(name, directory, path):
"""Helper function to convert generate a directory_path target and return its label. """Helper function to generate a directory_path target and return its label.
Args: Args:
name: Unique name for the generated `directory_path` target. name: Unique name for the generated `directory_path` target.

View File

@ -0,0 +1,186 @@
"write_source_file implementation"
load("//lib:utils.bzl", "is_external_label")
load(":directory_path.bzl", "DirectoryPathInfo")
_write_source_file_attrs = {
"in_file": attr.label(allow_files = True, mandatory = False),
"out_file": attr.label(allow_files = True, mandatory = False),
"additional_update_targets": attr.label_list(cfg = "host", mandatory = False),
"is_windows": attr.bool(mandatory = True),
}
def _write_source_file_sh(ctx, paths):
updater = ctx.actions.declare_file(
ctx.label.name + "_update.sh",
)
additional_update_scripts = []
for target in ctx.attr.additional_update_targets:
if target[DefaultInfo].files_to_run and target[DefaultInfo].files_to_run.executable:
additional_update_scripts.append(target[DefaultInfo].files_to_run.executable)
else:
fail("additional_update_targets target %s does not provide an executable")
contents = ["""#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail
runfiles_dir=$PWD
# BUILD_WORKSPACE_DIRECTORY not set when running as a test, uses the sandbox instead
if [[ ! -z "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then
cd "$BUILD_WORKSPACE_DIRECTORY"
fi"""]
for in_path, out_path in paths:
contents.append("""
in=$runfiles_dir/{in_path}
out={out_path}
mkdir -p "$(dirname "$out")"
echo "Copying $in to $out in $PWD"
if [[ -f "$in" ]]; then
cp -f "$in" "$out"
chmod 664 "$out"
else
mkdir -p "$out"
cp -rf "$in"/* "$out"
chmod 664 "$out"/*
fi
""".format(in_path = in_path, out_path = out_path))
contents.extend([
"cd \"$runfiles_dir\"",
"# Run the update scripts for all write_source_file deps",
])
for update_script in additional_update_scripts:
contents.append("\"{update_script}\"".format(update_script = update_script.short_path))
ctx.actions.write(
output = updater,
is_executable = True,
content = "\n".join(contents),
)
return updater
def _write_source_file_bat(ctx, paths):
updater = ctx.actions.declare_file(
ctx.label.name + "_update.bat",
)
additional_update_scripts = []
for target in ctx.attr.additional_update_targets:
if target[DefaultInfo].files_to_run and target[DefaultInfo].files_to_run.executable:
additional_update_scripts.append(target[DefaultInfo].files_to_run.executable)
else:
fail("additional_update_targets target %s does not provide an executable")
contents = ["""@rem Generated by write_source_file.bzl, do not edit.
@echo off
set runfiles_dir=%cd%
if defined BUILD_WORKSPACE_DIRECTORY (
cd %BUILD_WORKSPACE_DIRECTORY%
)"""]
for in_path, out_path in paths:
contents.append("""
set in=%runfiles_dir%\\{in_path}
set out={out_path}
if not defined BUILD_WORKSPACE_DIRECTORY (
@rem Because there's no sandboxing in windows, if we copy over the target
@rem file's symlink it will get copied back into the source directory
@rem during tests. Work around this in tests by deleting the target file
@rem symlink before copying over it.
del %out%
)
echo Copying %in% to %out% in %cd%
if exist "%in%\\*" (
mkdir "%out%" >NUL 2>NUL
robocopy "%in%" "%out%" /E >NUL
) else (
copy %in% %out% >NUL
)
""".format(in_path = in_path.replace("/", "\\"), out_path = out_path.replace("/", "\\")))
contents.extend([
"cd %runfiles_dir%",
"@rem Run the update scripts for all write_source_file deps",
])
for update_script in additional_update_scripts:
contents.append("call {update_script".format(update_script = update_script.short_path))
ctx.actions.write(
output = updater,
is_executable = True,
context = "\n".join(contents).replace("\n", "\r\n"),
)
return updater
def _write_source_file_impl(ctx):
if ctx.attr.out_file:
if not ctx.attr.in_file:
fail("in_file must be specified if out_file is set")
if is_external_label(ctx.attr.out_file.label):
fail("out file %s must be in the user workspace" % ctx.attr.out_file.label)
if ctx.attr.out_file.label.package != ctx.label.package:
fail("out file %s (in package '%s') must be a source file within the target's package: '%s'" % (ctx.attr.out_file.label, ctx.attr.out_file.label.package, ctx.label.package))
if ctx.attr.in_file and not ctx.attr.out_file:
if not ctx.attr.in_file:
fail("out_file must be specified if in_file is set")
paths = []
runfiles = []
if ctx.attr.in_file and ctx.attr.out_file:
if DirectoryPathInfo in ctx.attr.in_file:
in_path = "/".join([
ctx.attr.in_file[DirectoryPathInfo].directory.short_path,
ctx.attr.in_file[DirectoryPathInfo].path,
])
runfiles.append(ctx.attr.in_file[DirectoryPathInfo].directory)
elif len(ctx.files.in_file) == 0:
fail("in file %s must provide files" % ctx.attr.in_file.label)
elif len(ctx.files.in_file) == 1:
in_path = ctx.files.in_file[0].short_path
else:
fail("in file %s must be a single file or a target that provides DefaultOutputPathInfo or DirectoryPathInfo" % ctx.attr.in_file.label)
if len(ctx.files.out_file) != 1:
fail("out file %s must be a single file or directory" % ctx.attr.out_file.label)
elif not ctx.files.out_file[0].is_source:
fail("out file %s must be a source file or directory, not a generated file" % ctx.attr.out_file.label)
out_path = ctx.files.out_file[0].short_path
paths.append((in_path, out_path))
if ctx.attr.is_windows:
updater = _write_source_file_bat(ctx, paths)
else:
updater = _write_source_file_sh(ctx, paths)
runfiles = ctx.runfiles(
files = runfiles,
transitive_files = ctx.attr.in_file.files if ctx.attr.in_file else None,
)
deps_runfiles = [dep[DefaultInfo].default_runfiles for dep in ctx.attr.additional_update_targets]
if "merge_all" in dir(runfiles):
runfiles = runfiles.merge_all(deps_runfiles)
else:
for dep in deps_runfiles:
runfiles = runfiles.merge(dep)
return [
DefaultInfo(
executable = updater,
runfiles = runfiles,
),
]
write_source_file_lib = struct(
attrs = _write_source_file_attrs,
implementation = _write_source_file_impl,
)

View File

@ -1,161 +0,0 @@
"write_source_file implementation"
load("//lib:utils.bzl", "is_external_label")
_WriteSourceFilesInfo = provider(
"Provider to enforce deps are other write_source_files targets",
fields = {
"executable": "Generated update script",
},
)
_write_source_files_attrs = {
"in_files": attr.label_list(allow_files = True, allow_empty = True, mandatory = False),
"out_files": attr.label_list(allow_files = True, allow_empty = True, mandatory = False),
"additional_update_targets": attr.label_list(allow_files = False, providers = [_WriteSourceFilesInfo], mandatory = False),
"is_windows": attr.bool(mandatory = True),
}
def _write_source_files_sh(ctx):
updater = ctx.actions.declare_file(
ctx.label.name + "_update.sh",
)
additional_update_scripts = [target[_WriteSourceFilesInfo].executable for target in ctx.attr.additional_update_targets]
ctx.actions.write(
output = updater,
is_executable = True,
content = """
#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail
runfiles_dir=$PWD
# BUILD_WORKSPACE_DIRECTORY not set when running as a test, uses the sandbox instead
if [[ ! -z "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then
cd "$BUILD_WORKSPACE_DIRECTORY"
fi
""" + "\n".join([
"""
in=$runfiles_dir/{in_file}
out={out_file}
mkdir -p "$(dirname "$out")"
echo "Copying $in to $out in $PWD"
if [[ -f "$in" ]]; then
cp -f "$in" "$out"
chmod 664 "$out"
else
mkdir -p "$out"
cp -rf "$in"/* "$out"
chmod 664 "$out"/*
fi
""".format(in_file = ctx.files.in_files[i].short_path, out_file = ctx.files.out_files[i].short_path)
for i in range(len(ctx.attr.in_files))
]) + """
cd "$runfiles_dir"
# Run the update scripts for all write_source_file deps
""" + "\n".join(["""
{update_script}
""".format(update_script = update_script.short_path) for update_script in additional_update_scripts]),
)
return updater
def _write_source_files_bat(ctx):
updater = ctx.actions.declare_file(
ctx.label.name + "_update.bat",
)
additional_update_scripts = [target[_WriteSourceFilesInfo].executable for target in ctx.attr.additional_update_targets]
content = """
@rem Generated by write_source_files.bzl, do not edit.
@echo off
set runfiles_dir=%cd%
if defined BUILD_WORKSPACE_DIRECTORY (
cd %BUILD_WORKSPACE_DIRECTORY%
)
""" + "\n".join([
"""
set in=%runfiles_dir%\\{in_file}
set out={out_file}
if not defined BUILD_WORKSPACE_DIRECTORY (
@rem Because there's no sandboxing in windows, if we copy over the target
@rem file's symlink it will get copied back into the source directory
@rem during tests. Work around this in tests by deleting the target file
@rem symlink before copying over it.
del %out%
)
echo Copying %in% to %out% in %cd%
if exist "%in%\\*" (
mkdir "%out%" >NUL 2>NUL
robocopy "%in%" "%out%" /E >NUL
) else (
copy %in% %out% >NUL
)
""".format(in_file = ctx.files.in_files[i].short_path.replace("/", "\\"), out_file = ctx.files.out_files[i].short_path.replace("/", "\\"))
for i in range(len(ctx.attr.in_files))
]) + """
cd %runfiles_dir%
@rem Run the update scripts for all write_source_file deps
""" + "\n".join(["""
call {update_script}
""".format(update_script = update_script.short_path) for update_script in additional_update_scripts])
content = content.replace("\n", "\r\n")
ctx.actions.write(
output = updater,
is_executable = True,
content = content,
)
return updater
def _write_source_files_impl(ctx):
if (len(ctx.attr.in_files) != len(ctx.attr.out_files)):
fail("in_files and out_files must be the same length")
for i in range(len(ctx.attr.in_files)):
out_file_label = ctx.attr.out_files[i].label
if is_external_label(out_file_label):
fail("out file %s must be a source file in the user workspace" % out_file_label)
if not ctx.files.out_files[i].is_source:
fail("out file %s must be a source file, not a generated file" % out_file_label)
if out_file_label.package != ctx.label.package:
fail("out file %s (in package '%s') must be a source file within the target's package: '%s'" % (out_file_label, out_file_label.package, ctx.label.package))
if ctx.attr.is_windows:
updater = _write_source_files_bat(ctx)
else:
updater = _write_source_files_sh(ctx)
runfiles = ctx.runfiles(files = ctx.files.in_files)
deps_runfiles = [dep[DefaultInfo].default_runfiles for dep in ctx.attr.additional_update_targets]
if "merge_all" in dir(runfiles):
runfiles = runfiles.merge_all(deps_runfiles)
else:
for dep in deps_runfiles:
runfiles = runfiles.merge(dep)
return [
DefaultInfo(
executable = updater,
runfiles = runfiles,
),
_WriteSourceFilesInfo(
executable = updater,
),
]
write_source_files_lib = struct(
attrs = _write_source_files_attrs,
implementation = _write_source_files_impl,
)

View File

@ -0,0 +1,30 @@
"""A simple rule that generates provides a DefaultOutput with some files"""
def _impl(ctx):
if len(ctx.attr.out_files) != len(ctx.attr.out_contents):
fail("Number of out_files must match number of out_contents")
outputs = []
for i, file in enumerate(ctx.attr.out_files):
content = ctx.attr.out_contents[i]
out = ctx.actions.declare_file(file)
# ctx.actions.write creates a FileWriteAction which uses UTF-8 encoding.
ctx.actions.write(
output = out,
content = content,
)
outputs.append(out)
return [DefaultInfo(
files = depset(direct = outputs),
runfiles = ctx.runfiles(files = outputs),
)]
default_output_gen = rule(
implementation = _impl,
provides = [DefaultInfo],
attrs = {
"out_files": attr.string_list(),
"out_contents": attr.string_list(),
},
)

View File

@ -1,42 +1,84 @@
load("//lib/tests/write_source_files:write_source_files_test.bzl", "write_source_files_test") load("//lib/tests/write_source_files:write_source_file_test.bzl", "write_source_file_test")
load("//lib/tests:default_output_gen.bzl", "default_output_gen")
load("//lib:write_source_files.bzl", "write_source_files") load("//lib:write_source_files.bzl", "write_source_files")
load("//lib:copy_to_directory.bzl", "copy_to_directory") load("//lib:copy_to_directory.bzl", "copy_to_directory")
load("//lib:directory_path.bzl", "directory_path")
load("//lib:default_info_files.bzl", "default_info_files")
genrule( genrule(
name = "a-desired", name = "a-desired",
outs = ["a-desired.js"], outs = ["a-desired.js"],
cmd = "echo 'console.log(\"a*\")' > $@", cmd = "echo 'console.log(\"a*\");' > $@",
) )
genrule( default_output_gen(
name = "b_c-desired",
out_contents = [
"""console.log(\"b*\");
""",
"not used!",
],
out_files = [
"b-desired.js",
"c-desired.js",
],
)
default_info_files(
name = "b-desired", name = "b-desired",
outs = ["b-desired.js"], paths = ["%s/b-desired.js" % package_name()],
cmd = "echo 'console.log(\"b*\")' > $@", target = ":b_c-desired",
) )
genrule( genrule(
name = "e", name = "e-contained",
outs = ["e.js"], outs = ["e-contained.js"],
cmd = "echo 'console.log(\"e*\")' > $@", cmd = "echo 'console.log(\"e*\");' > $@",
) )
copy_to_directory( copy_to_directory(
name = "e_dir-desired", name = "e_dir-desired",
srcs = [":e"], srcs = [":e-contained"],
) )
write_source_files_test( genrule(
name = "write_to_source_files_test", name = "f-contained",
in_files = [ outs = ["f-contained.js"],
":a-desired", cmd = "echo 'console.log(\"f*\");' > $@",
":b-desired", )
],
out_files = [ copy_to_directory(
"a.js", name = "e_f_dir-desired",
"b.js", srcs = [
":e-contained",
":f-contained",
], ],
) )
directory_path(
name = "f-desired",
directory = ":e_f_dir-desired",
path = "f-contained.js",
)
write_source_file_test(
name = "write_to_source_files_a_test",
in_file = ":a-desired",
out_file = "a.js",
)
write_source_file_test(
name = "write_to_source_files_b_test",
in_file = ":b-desired",
out_file = "b.js",
)
write_source_file_test(
name = "write_to_source_files_f_test",
in_file = ":f-desired",
out_file = "f.js",
)
write_source_files( write_source_files(
name = "macro_smoke_test", name = "macro_smoke_test",
additional_update_targets = [ additional_update_targets = [
@ -45,6 +87,7 @@ write_source_files(
files = { files = {
"a2.js": ":a-desired", "a2.js": ":a-desired",
"b2.js": ":b-desired", "b2.js": ":b-desired",
"e_dir": ":e_dir-desired", "e2_dir": ":e_dir-desired",
"f2.js": ":f-desired",
}, },
) )

View File

@ -1 +1 @@
console.log("a*") console.log("a*");

View File

@ -1 +1 @@
console.log("b*") console.log("b*");

View File

@ -0,0 +1 @@
console.log("e*");

View File

@ -0,0 +1 @@
console.log("e");

View File

@ -1 +0,0 @@
console.log("e*")

View File

@ -0,0 +1 @@
console.log("f");

View File

@ -0,0 +1 @@
console.log("f*");

View File

@ -8,12 +8,12 @@ genrule(
write_source_files( write_source_files(
name = "macro_smoke_test", name = "macro_smoke_test",
additional_update_targets = [
"//lib/tests/write_source_files/subdir/subsubdir:macro_smoke_test",
],
files = { files = {
"c.js": ":c-desired", "c.js": ":c-desired",
}, },
suggested_update_target = "//lib/tests/write_source_files:macro_smoke_test", suggested_update_target = "//lib/tests/write_source_files:macro_smoke_test",
visibility = ["//visibility:public"], visibility = ["//lib/tests/write_source_files:__pkg__"],
additional_update_targets = [
"//lib/tests/write_source_files/subdir/subsubdir:macro_smoke_test",
],
) )

View File

@ -12,5 +12,5 @@ write_source_files(
"d.js": ":d-desired", "d.js": ":d-desired",
}, },
suggested_update_target = "//lib/tests/write_source_files:macro_smoke_test", suggested_update_target = "//lib/tests/write_source_files:macro_smoke_test",
visibility = ["//visibility:public"], visibility = ["//lib/tests/write_source_files/subdir:__pkg__"],
) )

View File

@ -1,23 +1,23 @@
"""Tests for write_source_files""" """Tests for write_source_files"""
# Inspired by https://github.com/cgrindel/bazel-starlib/blob/main/updatesrc/private/updatesrc_update_test.bzl # Inspired by https://github.com/cgrindel/bazel-starlib/blob/main/updatesrc/private/updatesrc_update_test.bzl
load("//lib/private:write_source_files.bzl", _lib = "write_source_files_lib") load("//lib/private:write_source_file.bzl", _lib = "write_source_file_lib")
load("//lib/private:directory_path.bzl", "DirectoryPathInfo")
_write_source_files = rule( _write_source_file = rule(
attrs = _lib.attrs, attrs = _lib.attrs,
implementation = _lib.implementation, implementation = _lib.implementation,
executable = True, executable = True,
) )
def _impl_sh(ctx): def _impl_sh(ctx, in_file_path, out_file_path):
test = ctx.actions.declare_file( test = ctx.actions.declare_file(
ctx.label.name + "_test.sh", ctx.label.name + "_test.sh",
) )
ctx.actions.write( contents = []
output = test,
is_executable = True, contents.append("""
content = """
#!/usr/bin/env bash #!/usr/bin/env bash
set -o errexit -o nounset -o pipefail set -o errexit -o nounset -o pipefail
@ -32,32 +32,35 @@ assert_same() {
local in_file="${1}" local in_file="${1}"
local out_file="${2}" local out_file="${2}"
diff "${in_file}" "${out_file}" || (echo >&2 "Expected files to be same. in: ${in_file}, out: ${out_file}" && return -1) diff "${in_file}" "${out_file}" || (echo >&2 "Expected files to be same. in: ${in_file}, out: ${out_file}" && return -1)
} }""")
contents.append("""
# Check that in and out files are different # Check that in and out files are different
""" + "\n".join([ assert_different {in_file} {out_file}
"assert_different {in_file} {out_file}".format( """.format(
in_file = ctx.files.in_files[i].short_path, in_file = in_file_path,
out_file = ctx.files.out_files[i].short_path, out_file = out_file_path,
) ))
for i in range(len(ctx.files.in_files))
]) + """
# Write to the source files
{write_source_files}
# Check that in and out files are the same contents.append("""# Write to the source files
""".format(write_source_files = ctx.file.write_source_files_target.short_path) + "\n".join([ {write_source_files}
"assert_same {in_file} {out_file}".format( """.format(write_source_files = ctx.file.write_source_file_target.short_path))
in_file = ctx.files.in_files[i].short_path,
out_file = ctx.files.out_files[i].short_path, contents.append("""# Check that in and out files are the same
) assert_same {in_file} {out_file}""".format(
for i in range(len(ctx.files.in_files)) in_file = in_file_path,
]), out_file = out_file_path,
))
ctx.actions.write(
output = test,
is_executable = True,
content = "\n".join(contents),
) )
return test return test
def _impl_bat(ctx): def _impl_bat(ctx, in_file_path, out_file_path):
test = ctx.actions.declare_file( test = ctx.actions.declare_file(
ctx.label.name + "_test.bat", ctx.label.name + "_test.bat",
) )
@ -66,40 +69,42 @@ def _impl_bat(ctx):
# but it is able to execute the actual output script so point to that for now. # but it is able to execute the actual output script so point to that for now.
# #
# What we would use if this bug didn't exist: # What we would use if this bug didn't exist:
# write_source_files = ctx.executable.write_source_files_target.short_path.replace("/", "\\") # write_source_files = ctx.executable.write_source_file_target.short_path.replace("/", "\\")
# #
# Instead back out of the runfiles execution directory: # Instead back out of the runfiles execution directory:
# write_to_source_files_test_test.bat.runfiles/aspect_bazel_lib # write_to_source_files_test_test.bat.runfiles/aspect_bazel_lib
# And point to the output script. # And point to the output script.
write_source_files = "..\\..\\%s" % ctx.executable.write_source_files_target.basename write_source_files = "..\\..\\%s" % ctx.executable.write_source_file_target.basename
content = """ contents = []
contents.append("""
@rem Generated by copy_to_directory.bzl, do not edit. @rem Generated by copy_to_directory.bzl, do not edit.
@echo off @echo off
@rem Check that in and out files are different @rem Check that in and out files are different
""" + "\n".join(["""
call :assert_different {in_file}, {out_file} call :assert_different {in_file}, {out_file}
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
""".format( """.format(
in_file = ctx.files.in_files[i].short_path.replace("/", "\\"), in_file = in_file_path.replace("/", "\\"),
out_file = ctx.files.out_files[i].short_path.replace("/", "\\"), out_file = out_file_path.replace("/", "\\"),
) ))
for i in range(len(ctx.files.in_files))
]) + """ contents.append("""
@rem Write to the source files @rem Write to the source files
call {write_source_files} call {write_source_files}
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
""".format(write_source_files = write_source_files))
contents.append("""
@rem Check that in and out files are the same @rem Check that in and out files are the same
""".format(write_source_files = write_source_files) + "\n".join(["""
call :assert_same {in_file}, {out_file} call :assert_same {in_file}, {out_file}
if %errorlevel% neq 0 exit /b 1 if %errorlevel% neq 0 exit /b 1
""".format( """.format(
in_file = ctx.files.in_files[i].short_path.replace("/", "\\"), in_file = in_file_path.replace("/", "\\"),
out_file = ctx.files.out_files[i].short_path.replace("/", "\\"), out_file = out_file_path.replace("/", "\\"),
) ))
for i in range(len(ctx.files.in_files))
]) + """ contents.append("""
exit /b 0 exit /b 0
:assert_different :assert_different
@ -113,48 +118,54 @@ fc /b %~1 %~2 > nul
if %errorlevel% equ 0 exit /b 0 if %errorlevel% equ 0 exit /b 0
echo Error: %~1 and %~2 are not the same echo Error: %~1 and %~2 are not the same
exit /b 1 exit /b 1
""" """)
content = content.replace("\n", "\r\n")
ctx.actions.write( ctx.actions.write(
output = test, output = test,
is_executable = True, is_executable = True,
content = content content = "\n".join(contents).replace("\n", "\r\n"),
) )
return test return test
def _impl(ctx): def _impl(ctx):
if ctx.attr.is_windows: if DirectoryPathInfo in ctx.attr.in_file:
test = _impl_bat(ctx) in_file = ctx.attr.in_file[DirectoryPathInfo].directory
in_file_path = "/".join([in_file.short_path, ctx.attr.in_file[DirectoryPathInfo].path])
else: else:
test = _impl_sh(ctx) if len(ctx.files.in_file) != 1:
fail("in_file must be a single file or a target that provides a DirectoryPathInfo")
in_file = ctx.files.in_file[0]
in_file_path = in_file.short_path
if ctx.attr.is_windows:
test = _impl_bat(ctx, in_file_path, ctx.file.out_file.short_path)
else:
test = _impl_sh(ctx, in_file_path, ctx.file.out_file.short_path)
return DefaultInfo( return DefaultInfo(
executable = test, executable = test,
runfiles = ctx.runfiles( runfiles = ctx.runfiles(
files = [ctx.executable.write_source_files_target] + ctx.files.in_files + ctx.files.out_files, files = [ctx.executable.write_source_file_target, in_file, ctx.file.out_file],
), ),
) )
_write_source_files_test = rule( _write_source_file_test = rule(
implementation = _impl, implementation = _impl,
attrs = { attrs = {
"write_source_files_target": attr.label( "write_source_file_target": attr.label(
allow_single_file = True, allow_single_file = True,
executable = True, executable = True,
# Should be cfg = "exec" but a bazel bug causes a wrong executable symlink on windows # Should be cfg = "exec" but a bazel bug causes a wrong executable symlink on windows
cfg = "target", cfg = "target",
mandatory = True, mandatory = True,
), ),
"out_files": attr.label_list( "out_file": attr.label(
allow_files = True, allow_single_file = True,
allow_empty = False,
mandatory = True, mandatory = True,
), ),
"in_files": attr.label_list( "in_file": attr.label(
allow_files = True, allow_files = True,
allow_empty = False,
mandatory = True, mandatory = True,
), ),
"is_windows": attr.bool(mandatory = True), "is_windows": attr.bool(mandatory = True),
@ -162,13 +173,13 @@ _write_source_files_test = rule(
test = True, test = True,
) )
def write_source_files_test(name, in_files, out_files): def write_source_file_test(name, in_file, out_file):
"""Stamp a write_source_files executable and a test to run against it""" """Stamp a write_source_files executable and a test to run against it"""
_write_source_files( _write_source_file(
name = name + "_updater", name = name + "_updater",
out_files = out_files, in_file = in_file,
in_files = in_files, out_file = out_file,
is_windows = select({ is_windows = select({
"@bazel_tools//src/conditions:host_windows": True, "@bazel_tools//src/conditions:host_windows": True,
"//conditions:default": False, "//conditions:default": False,
@ -177,11 +188,11 @@ def write_source_files_test(name, in_files, out_files):
# Note that for testing we update the source files in the sandbox, # Note that for testing we update the source files in the sandbox,
# not the actual source tree. # not the actual source tree.
_write_source_files_test( _write_source_file_test(
name = name, name = name,
write_source_files_target = name + "_updater", write_source_file_target = name + "_updater",
out_files = out_files, in_file = in_file,
in_files = in_files, out_file = out_file,
is_windows = select({ is_windows = select({
"@bazel_tools//src/conditions:host_windows": True, "@bazel_tools//src/conditions:host_windows": True,
"//conditions:default": False, "//conditions:default": False,

View File

@ -1,11 +1,14 @@
"Public API for write_source_files" "Public API for write_source_files"
load("//lib/private:write_source_files.bzl", _lib = "write_source_files_lib") load(
"//lib/private:write_source_file.bzl",
_lib = "write_source_file_lib",
)
load("//lib:utils.bzl", _to_label = "to_label") load("//lib:utils.bzl", _to_label = "to_label")
load("@bazel_skylib//rules:diff_test.bzl", _diff_test = "diff_test") load("//lib/private:diff_test.bzl", _diff_test = "diff_test")
load("//lib/private:fail_with_message_test.bzl", "fail_with_message_test") load("//lib/private:fail_with_message_test.bzl", "fail_with_message_test")
_write_source_files = rule( _write_source_file = rule(
attrs = _lib.attrs, attrs = _lib.attrs,
implementation = _lib.implementation, implementation = _lib.implementation,
executable = True, executable = True,
@ -85,41 +88,45 @@ def write_source_files(name, files = {}, additional_update_targets = [], suggest
name: Name of the executable target that creates or updates the source file name: Name of the executable target that creates or updates the source file
files: A dict where the keys are source files or folders to write to and the values are labels pointing to the desired content. files: A dict where the keys are source files or folders to write to and the values are labels pointing to the desired content.
Sources must be within the same bazel package as the target. Sources must be within the same bazel package as the target.
additional_update_targets: (Optional) List of other write_source_files targets to update in the same run additional_update_targets: (Optional) List of other write_source_file or other executable updater targets to call in the same run
suggested_update_target: (Optional) Label of the write_source_files target to suggest running when files are out of date suggested_update_target: (Optional) Label of the write_source_file target to suggest running when files are out of date
**kwargs: Other common named parameters such as `tags` or `visibility` **kwargs: Other common named parameters such as `tags` or `visibility`
""" """
out_files = files.keys() single_update_target = len(files.keys()) == 1
in_files = [files[f] for f in out_files] update_targets = []
for i, pair in enumerate(files.items()):
out_file, in_file = pair
# Stamp an executable rule that writes to the out file in_file = _to_label(in_file)
_write_source_files( out_file = _to_label(out_file)
name = name,
in_files = in_files,
out_files = out_files,
additional_update_targets = additional_update_targets,
is_windows = select({
"@bazel_tools//src/conditions:host_windows": True,
"//conditions:default": False,
}),
visibility = kwargs.get("visibility"),
tags = kwargs.get("tags"),
)
# Fail if user passes args that would conflict with stamped out targets below if single_update_target:
if kwargs.pop("file1", None) != None: update_target_name = name
fail("file1 not a valid parameter in write_source_file") else:
if kwargs.pop("file2", None) != None: update_target_name = "%s_%d" % (name, i)
fail("file2 not a valid parameter in write_source_file") update_targets.append(update_target_name)
if kwargs.pop("failure_message", None) != None:
fail("failure_message not a valid parameter in write_source_file") # Runnable target that writes to the out file to the source tree
_write_source_file(
name = update_target_name,
in_file = in_file,
out_file = out_file,
additional_update_targets = additional_update_targets if single_update_target else [],
is_windows = select({
"@bazel_tools//src/conditions:host_windows": True,
"//conditions:default": False,
}),
visibility = kwargs.get("visibility"),
tags = kwargs.get("tags"),
)
for i in range(len(out_files)):
out_file = _to_label(out_files[i])
out_file_missing = _is_file_missing(out_file) out_file_missing = _is_file_missing(out_file)
name_test = "%s_%d_test" % (name, i) if single_update_target:
test_target_name = "%s_test" % name
else:
test_target_name = "%s_%d_test" % (name, i)
if out_file_missing: if out_file_missing:
if suggested_update_target == None: if suggested_update_target == None:
@ -147,7 +154,7 @@ To create an update *only* this file, run:
# Note that we cannot simply call fail() here since it will fail during the analysis # Note that we cannot simply call fail() here since it will fail during the analysis
# phase and prevent the user from calling bazel run //update/the:file. # phase and prevent the user from calling bazel run //update/the:file.
fail_with_message_test( fail_with_message_test(
name = name_test, name = test_target_name,
message = message, message = message,
visibility = kwargs.get("visibility"), visibility = kwargs.get("visibility"),
tags = kwargs.get("tags"), tags = kwargs.get("tags"),
@ -176,13 +183,25 @@ To update *only* this file, run:
# Stamp out a diff test the check that the source file is up to date # Stamp out a diff test the check that the source file is up to date
_diff_test( _diff_test(
name = name_test, name = test_target_name,
file1 = in_files[i], file1 = in_file,
file2 = out_file, file2 = out_file,
failure_message = message, failure_message = message,
**kwargs **kwargs
) )
if not single_update_target:
_write_source_file(
name = name,
additional_update_targets = update_targets + additional_update_targets,
is_windows = select({
"@bazel_tools//src/conditions:host_windows": True,
"//conditions:default": False,
}),
visibility = kwargs.get("visibility"),
tags = kwargs.get("tags"),
)
def _is_file_missing(label): def _is_file_missing(label):
"""Check if a file is missing by passing its relative path through a glob() """Check if a file is missing by passing its relative path through a glob()