Skip to content

Commit 3599515

Browse files
authored
fix: ensure out_file does not show up in source file query of write_source_file so that it can be used with ibazel (#52)
1 parent bda5c63 commit 3599515

File tree

6 files changed

+177
-137
lines changed

6 files changed

+177
-137
lines changed

docs/write_source_files.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ Public API for write_source_files
77
## write_source_files
88

99
<pre>
10-
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>)
10+
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-diff_test">diff_test</a>,
11+
<a href="#write_source_files-kwargs">kwargs</a>)
1112
</pre>
1213

1314
Write to one or more files or folders in the source tree. Stamp out tests that ensure the sources exist and are up to date.
@@ -89,6 +90,7 @@ If you have many sources that you want to update as a group, we recommend wrappi
8990
| <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> |
9091
| <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> |
9192
| <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> |
93+
| <a id="write_source_files-diff_test"></a>diff_test | (Optional) Generate a test target to check that the source file(s) exist and are up to date with the generated files(s). | <code>True</code> |
9294
| <a id="write_source_files-kwargs"></a>kwargs | Other common named parameters such as <code>tags</code> or <code>visibility</code> | none |
9395

9496

lib/private/write_source_file.bzl

+143-19
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,135 @@
11
"write_source_file implementation"
22

3-
load("//lib:utils.bzl", "is_external_label")
43
load(":directory_path.bzl", "DirectoryPathInfo")
4+
load(":diff_test.bzl", _diff_test = "diff_test")
5+
load(":fail_with_message_test.bzl", "fail_with_message_test")
6+
load(":utils.bzl", "utils")
7+
8+
def write_source_file(
9+
name,
10+
in_file = None,
11+
out_file = None,
12+
additional_update_targets = [],
13+
suggested_update_target = None,
14+
diff_test = True,
15+
**kwargs):
16+
"""Write a file or folder to the output tree. Stamp out tests that ensure the sources exist and are up to date.
17+
18+
Args:
19+
name: Name of the executable target that creates or updates the source file
20+
in_file: File to use as the desired content to write to out_file. If in_file is a TreeArtifact then entire directory contents are copied.
21+
out_file: The file to write to in the source tree. Must be within the same bazel package as the target.
22+
additional_update_targets: List of other write_source_file or other executable updater targets to call in the same run
23+
suggested_update_target: Label of the write_source_file target to suggest running when files are out of date
24+
diff_test: Generate a test target to check that the source file(s) exist and are up to date with the generated files(s).
25+
**kwargs: Other common named parameters such as `tags` or `visibility`
26+
"""
27+
if out_file:
28+
if not in_file:
29+
fail("in_file must be specified if out_file is set")
30+
31+
if in_file:
32+
if not out_file:
33+
fail("out_file must be specified if in_file is set")
34+
35+
if in_file and out_file:
36+
in_file = utils.to_label(in_file)
37+
out_file = utils.to_label(out_file)
38+
39+
if utils.is_external_label(out_file):
40+
fail("out file %s must be in the user workspace" % out_file)
41+
if out_file.package != native.package_name():
42+
fail("out file %s (in package '%s') must be a source file within the target's package: '%s'" % (out_file, out_file.package, native.package_name()))
43+
44+
_write_source_file(
45+
name = name,
46+
in_file = in_file,
47+
out_file = out_file.name if out_file else None,
48+
additional_update_targets = additional_update_targets,
49+
is_windows = select({
50+
"@bazel_tools//src/conditions:host_windows": True,
51+
"//conditions:default": False,
52+
}),
53+
**kwargs
54+
)
55+
56+
if not in_file or not out_file or not diff_test:
57+
return
58+
59+
out_file_missing = _is_file_missing(out_file)
60+
test_target_name = "%s_test" % name
61+
62+
if out_file_missing:
63+
if suggested_update_target == None:
64+
message = """
65+
66+
%s does not exist. To create & update this file, run:
67+
68+
bazel run //%s:%s
69+
70+
""" % (out_file, native.package_name(), name)
71+
else:
72+
message = """
73+
74+
%s does not exist. To create & update this and other generated files, run:
75+
76+
bazel run %s
77+
78+
To create an update *only* this file, run:
79+
80+
bazel run //%s:%s
81+
82+
""" % (out_file, utils.to_label(suggested_update_target), native.package_name(), name)
83+
84+
# Stamp out a test that fails with a helpful message when the source file doesn't exist.
85+
# Note that we cannot simply call fail() here since it will fail during the analysis
86+
# phase and prevent the user from calling bazel run //update/the:file.
87+
fail_with_message_test(
88+
name = test_target_name,
89+
message = message,
90+
visibility = kwargs.get("visibility"),
91+
tags = kwargs.get("tags"),
92+
)
93+
else:
94+
if suggested_update_target == None:
95+
message = """
96+
97+
%s is out of date. To update this file, run:
98+
99+
bazel run //%s:%s
100+
101+
""" % (out_file, native.package_name(), name)
102+
else:
103+
message = """
104+
105+
%s is out of date. To update this and other generated files, run:
106+
107+
bazel run %s
108+
109+
To update *only* this file, run:
110+
111+
bazel run //%s:%s
112+
113+
""" % (out_file, utils.to_label(suggested_update_target), native.package_name(), name)
114+
115+
# Stamp out a diff test the check that the source file is up to date
116+
_diff_test(
117+
name = test_target_name,
118+
file1 = in_file,
119+
file2 = out_file,
120+
failure_message = message,
121+
**kwargs
122+
)
5123

6124
_write_source_file_attrs = {
7125
"in_file": attr.label(allow_files = True, mandatory = False),
8-
"out_file": attr.label(allow_files = True, mandatory = False),
126+
# out_file is intentionally an attr.string() and not a attr.label(). This is so that
127+
# bazel query 'kind("source file", deps(//path/to:target))' does not return
128+
# out_file in the list of source file deps. ibazel uses this query to determine
129+
# which source files to watch so if the out_file is returned then ibazel watches
130+
# and it goes into an infinite update, notify loop when running this target.
131+
# See https://github.com/aspect-build/bazel-lib/pull/52 for more context.
132+
"out_file": attr.string(mandatory = False),
9133
"additional_update_targets": attr.label_list(cfg = "host", mandatory = False),
10134
"is_windows": attr.bool(mandatory = True),
11135
}
@@ -120,17 +244,10 @@ if exist "%in%\\*" (
120244
return updater
121245

122246
def _write_source_file_impl(ctx):
123-
if ctx.attr.out_file:
124-
if not ctx.attr.in_file:
125-
fail("in_file must be specified if out_file is set")
126-
if is_external_label(ctx.attr.out_file.label):
127-
fail("out file %s must be in the user workspace" % ctx.attr.out_file.label)
128-
if ctx.attr.out_file.label.package != ctx.label.package:
129-
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))
130-
247+
if ctx.attr.out_file and not ctx.attr.in_file:
248+
fail("in_file must be specified if out_file is set")
131249
if ctx.attr.in_file and not ctx.attr.out_file:
132-
if not ctx.attr.in_file:
133-
fail("out_file must be specified if in_file is set")
250+
fail("out_file must be specified if in_file is set")
134251

135252
paths = []
136253
runfiles = []
@@ -149,12 +266,7 @@ def _write_source_file_impl(ctx):
149266
else:
150267
fail("in file %s must be a single file or a target that provides DefaultOutputPathInfo or DirectoryPathInfo" % ctx.attr.in_file.label)
151268

152-
if len(ctx.files.out_file) != 1:
153-
fail("out file %s must be a single file or directory" % ctx.attr.out_file.label)
154-
elif not ctx.files.out_file[0].is_source:
155-
fail("out file %s must be a source file or directory, not a generated file" % ctx.attr.out_file.label)
156-
157-
out_path = ctx.files.out_file[0].short_path
269+
out_path = "/".join([ctx.label.package, ctx.attr.out_file])
158270
paths.append((in_path, out_path))
159271

160272
if ctx.attr.is_windows:
@@ -180,7 +292,19 @@ def _write_source_file_impl(ctx):
180292
),
181293
]
182294

183-
write_source_file_lib = struct(
295+
_write_source_file = rule(
184296
attrs = _write_source_file_attrs,
185297
implementation = _write_source_file_impl,
298+
executable = True,
186299
)
300+
301+
def _is_file_missing(label):
302+
"""Check if a file is missing by passing its relative path through a glob()
303+
304+
Args
305+
label: the file's label
306+
"""
307+
file_abs = "%s/%s" % (label.package, label.name)
308+
file_rel = file_abs[len(native.package_name()) + 1:]
309+
file_glob = native.glob([file_rel], exclude_directories = 0)
310+
return len(file_glob) == 0
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist.js

lib/tests/write_source_files/BUILD.bazel

+13
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,16 @@ write_source_files(
119119
"g2.js": ":g-desired",
120120
},
121121
)
122+
123+
genrule(
124+
name = "dist",
125+
outs = ["dist.js"],
126+
cmd = "echo 'dist' > $@",
127+
)
128+
129+
# ibazel run //lib/tests/write_source_files:write_dist
130+
write_source_files(
131+
name = "write_dist",
132+
diff_test = False,
133+
files = {"dist.js": ":dist"},
134+
)

lib/tests/write_source_files/write_source_file_test.bzl

+2-11
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
"""Tests for write_source_files"""
22
# Inspired by https://github.com/cgrindel/bazel-starlib/blob/main/updatesrc/private/updatesrc_update_test.bzl
33

4-
load("//lib/private:write_source_file.bzl", _lib = "write_source_file_lib")
4+
load("//lib/private:write_source_file.bzl", _write_source_file = "write_source_file")
55
load("//lib/private:directory_path.bzl", "DirectoryPathInfo")
66

7-
_write_source_file = rule(
8-
attrs = _lib.attrs,
9-
implementation = _lib.implementation,
10-
executable = True,
11-
)
12-
137
def _impl_sh(ctx, in_file_path, out_file_path):
148
test = ctx.actions.declare_file(
159
ctx.label.name + "_test.sh",
@@ -180,10 +174,7 @@ def write_source_file_test(name, in_file, out_file):
180174
name = name + "_updater",
181175
in_file = in_file,
182176
out_file = out_file,
183-
is_windows = select({
184-
"@bazel_tools//src/conditions:host_windows": True,
185-
"//conditions:default": False,
186-
}),
177+
diff_test = False,
187178
)
188179

189180
# Note that for testing we update the source files in the sandbox,

0 commit comments

Comments
 (0)