Skip to content

Commit

Permalink
feat: bulk updates for write_source_files (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
kormide authored Feb 28, 2022
1 parent 2195e1c commit 71c1b89
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 46 deletions.
53 changes: 43 additions & 10 deletions docs/write_source_files.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Public API for write_source_files
## write_source_files

<pre>
write_source_files(<a href="#write_source_files-name">name</a>, <a href="#write_source_files-files">files</a>, <a href="#write_source_files-kwargs">kwargs</a>)
write_source_files(<a href="#write_source_files-name">name</a>, <a href="#write_source_files-files">files</a>, <a href="#write_source_files-additional_update_targets">additional_update_targets</a>, <a href="#write_source_files-suggested_update_target">suggested_update_target</a>, <a href="#write_source_files-kwargs">kwargs</a>)
</pre>

Write to one or more files in the source tree. Stamp out tests that ensure the files exists and are up to date.
Expand All @@ -30,23 +30,54 @@ To update the source file, run:
bazel run //:write_foobar
```

A test will fail if the source file doesn't exist
```bash
bazel test //...
A test will fail if the source file doesn't exist or if it's out of date with instructions on how to create/update it.

You can declare a tree of generated source file targets:

//:foobar.json does not exist. To create & update this file, run:
```starlark
load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_files")

bazel run //:write_foobar
write_source_files(
name = "write_all",
additional_update_targets = [
# Other write_source_files targets to run when this target is run
"//a/b/c:write_foo",
"//a/b:write_bar",
]
)
```

...or if it's out of date.
And update them with a single run:

```bash
bazel test //...
bazel run //:write_all
```

//:foobar.json is out-of-date. To update this file, run:
When a file is out of date, you can leave a suggestion to run a target further up in the tree by specifying `suggested_update_target`. E.g.,

```starlark
write_source_files(
name = "write_foo",
files = {
"foo.json": ":generated-foo",
},
suggested_update_target = "//:write_all"
)
```

A test failure from foo.json being out of date will yield the following message:

bazel run //:write_foobar
```
//a/b:c:foo.json is out of date. To update this and other generated files, run:
bazel run //:write_all
To update *only* this file, run:
bazel run //a/b/c:write_foo
```

If you have many sources that you want to update as a group, we recommend wrapping write_source_files in a macro that defaults `suggested_update_target` to the umbrella update target.


**PARAMETERS**
Expand All @@ -56,6 +87,8 @@ bazel test //...
| :------------- | :------------- | :------------- |
| <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 to write to and the values are labels pointing to the desired content. Source files must be within the same bazel package as the target. | none |
| <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-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-kwargs"></a>kwargs | Other common named parameters such as <code>tags</code> or <code>visibility</code> | none |


52 changes: 41 additions & 11 deletions lib/private/write_source_files.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@

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 = False, mandatory = True),
"out_files": attr.label_list(allow_files = True, allow_empty = False, mandatory = True),
"in_files": attr.label_list(allow_files = True, allow_empty = False, mandatory = False),
"out_files": attr.label_list(allow_files = True, allow_empty = False, mandatory = False),
"additional_update_targets": attr.label_list(allow_files = False, providers = [_WriteSourceFilesInfo], mandatory = False),
"is_windows": attr.bool(mandatory = True),
}

Expand All @@ -13,6 +21,8 @@ def _write_source_files_sh(ctx):
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,
Expand All @@ -35,7 +45,13 @@ cp -f "$in" "$out"
chmod 644 "$out"
""".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
Expand All @@ -52,7 +68,8 @@ set runfiles_dir=%cd%
if defined BUILD_WORKSPACE_DIRECTORY (
cd %BUILD_WORKSPACE_DIRECTORY%
)
""" + "\n".join(["""
""" + "\n".join([
"""
set in=%runfiles_dir%\\{in_file}
set out={out_file}
Expand All @@ -67,9 +84,9 @@ if not defined BUILD_WORKSPACE_DIRECTORY (
echo Copying %in% to %out% in %cd%
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))
])
for i in range(len(ctx.attr.in_files))
])

content = content.replace("\n", "\r\n")

ctx.actions.write(
Expand Down Expand Up @@ -99,10 +116,23 @@ def _write_source_files_impl(ctx):
else:
updater = _write_source_files_sh(ctx)

return DefaultInfo(
executable = updater,
runfiles = ctx.runfiles(files = ctx.files.in_files),
)
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,
Expand Down
5 changes: 4 additions & 1 deletion lib/tests/write_source_files/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ write_source_files(
"a2.js": ":a-desired",
"b2.js": ":b-desired",
},
)
additional_update_targets = [
"//lib/tests/write_source_files/subdir:macro_smoke_test",
],
)
19 changes: 19 additions & 0 deletions lib/tests/write_source_files/subdir/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
load("//lib:write_source_files.bzl", "write_source_files")

genrule(
name = "c-desired",
outs = ["c-desired.js"],
cmd = "echo 'console.log(\"c*\");' > $@",
)

write_source_files(
name = "macro_smoke_test",
files = {
"c.js": ":c-desired",
},
suggested_update_target = "//lib/tests/write_source_files:macro_smoke_test",
visibility = ["//visibility:public"],
additional_update_targets = [
"//lib/tests/write_source_files/subdir/subsubdir:macro_smoke_test",
],
)
1 change: 1 addition & 0 deletions lib/tests/write_source_files/subdir/c.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("c*");
16 changes: 16 additions & 0 deletions lib/tests/write_source_files/subdir/subsubdir/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
load("//lib:write_source_files.bzl", "write_source_files")

genrule(
name = "d-desired",
outs = ["d-desired.js"],
cmd = "echo 'console.log(\"d*\");' > $@",
)

write_source_files(
name = "macro_smoke_test",
files = {
"d.js": ":d-desired",
},
suggested_update_target = "//lib/tests/write_source_files:macro_smoke_test",
visibility = ["//visibility:public"],
)
1 change: 1 addition & 0 deletions lib/tests/write_source_files/subdir/subsubdir/d.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log("d*");
112 changes: 88 additions & 24 deletions lib/write_source_files.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ _write_source_files = rule(
executable = True,
)

def write_source_files(name, files, **kwargs):
def write_source_files(name, files, additional_update_targets = [], suggested_update_target = None, **kwargs):
"""Write to one or more files in the source tree. Stamp out tests that ensure the files exists and are up to date.
Usage:
Expand All @@ -32,28 +32,61 @@ def write_source_files(name, files, **kwargs):
bazel run //:write_foobar
```
A test will fail if the source file doesn't exist
```bash
bazel test //...
A test will fail if the source file doesn't exist or if it's out of date with instructions on how to create/update it.
You can declare a tree of generated source file targets:
//:foobar.json does not exist. To create & update this file, run:
```starlark
load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_files")
bazel run //:write_foobar
write_source_files(
name = "write_all",
additional_update_targets = [
# Other write_source_files targets to run when this target is run
"//a/b/c:write_foo",
"//a/b:write_bar",
]
)
```
...or if it's out of date.
And update them with a single run:
```bash
bazel test //...
bazel run //:write_all
```
//:foobar.json is out-of-date. To update this file, run:
When a file is out of date, you can leave a suggestion to run a target further up in the tree by specifying `suggested_update_target`. E.g.,
bazel run //:write_foobar
```starlark
write_source_files(
name = "write_foo",
files = {
"foo.json": ":generated-foo",
},
suggested_update_target = "//:write_all"
)
```
A test failure from foo.json being out of date will yield the following message:
```
//a/b:c:foo.json is out of date. To update this and other generated files, run:
bazel run //:write_all
To update *only* this file, run:
bazel run //a/b/c:write_foo
```
If you have many sources that you want to update as a group, we recommend wrapping write_source_files in a macro that defaults `suggested_update_target` to the umbrella update target.
Args:
name: Name of the executable target that creates or updates the source file
files: A dict where the keys are source files to write to and the values are labels pointing to the desired content.
Source files 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
suggested_update_target: (Optional) Label of the write_source_files target to suggest running when files are out of date
**kwargs: Other common named parameters such as `tags` or `visibility`
"""

Expand All @@ -65,6 +98,7 @@ def write_source_files(name, files, **kwargs):
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,
Expand All @@ -88,34 +122,64 @@ def write_source_files(name, files, **kwargs):
name_test = "%s_%d_test" % (name, i)

if out_file_missing:
if suggested_update_target == None:
message = """
%s does not exist. To create & update this file, run:
bazel run //%s:%s
""" % (out_file, native.package_name(), name)
else:
message = """
%s does not exist. To create & update this and other generated files, run:
bazel run %s
To create an update *only* this file, run:
bazel run //%s:%s
""" % (out_file, _to_label(suggested_update_target), native.package_name(), name)

# Stamp out a test that fails with a helpful message when the source file doesn't exist.
# 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.
fail_with_message_test(
name = name_test,
message = """
%s does not exist. To create & update this file, run:
bazel run //%s:%s
""" % (out_file, native.package_name(), name),
message = message,
visibility = kwargs.get("visibility"),
tags = kwargs.get("tags"),
)
else:
if suggested_update_target == None:
message = """
%s is out of date. To update this file, run:
bazel run //%s:%s
""" % (out_file, native.package_name(), name)
else:
message = """
%s is out of date. To update this and other generated files, run:
bazel run %s
To update *only* this file, run:
bazel run //%s:%s
""" % (out_file, _to_label(suggested_update_target), native.package_name(), name)

# Stamp out a diff test the check that the source file is up to date
_diff_test(
name = name_test,
file1 = in_files[i],
file2 = out_file,
failure_message = """
%s is out-of-date. To update this file, run:
bazel run //%s:%s
""" % (out_file, native.package_name(), name),
failure_message = message,
**kwargs
)

Expand Down

0 comments on commit 71c1b89

Please sign in to comment.