feat: add copy_to_directory_action

This commit is contained in:
Greg Magolan 2022-06-27 23:46:00 -07:00 committed by Alex Eagle
parent 910786038c
commit d4f6524fbd
9 changed files with 269 additions and 28 deletions

View File

@ -11,7 +11,7 @@ the `root_paths`, `exclude_prefixes` and `replace_prefixes` attributes.
## copy_to_directory
<pre>
copy_to_directory(<a href="#copy_to_directory-name">name</a>, <a href="#copy_to_directory-allow_overwrites">allow_overwrites</a>, <a href="#copy_to_directory-exclude_prefixes">exclude_prefixes</a>, <a href="#copy_to_directory-include_external_repositories">include_external_repositories</a>,
copy_to_directory(<a href="#copy_to_directory-name">name</a>, <a href="#copy_to_directory-allow_overwrites">allow_overwrites</a>, <a href="#copy_to_directory-exclude_prefixes">exclude_prefixes</a>, <a href="#copy_to_directory-include_external_repositories">include_external_repositories</a>, <a href="#copy_to_directory-out">out</a>,
<a href="#copy_to_directory-replace_prefixes">replace_prefixes</a>, <a href="#copy_to_directory-root_paths">root_paths</a>, <a href="#copy_to_directory-srcs">srcs</a>)
</pre>
@ -23,14 +23,48 @@ copy_to_directory(<a href="#copy_to_directory-name">name</a>, <a href="#copy_to_
| Name | Description | Type | Mandatory | Default |
| :------------- | :------------- | :------------- | :------------- | :------------- |
| <a id="copy_to_directory-name"></a>name | A unique name for this target. | <a href="https://bazel.build/docs/build-ref.html#name">Name</a> | required | |
| <a id="copy_to_directory-allow_overwrites"></a>allow_overwrites | If True, allow files to be overwritten if the same output file is copied to twice. If set, then the order of srcs matters as the last copy of a particular file will win.<br><br> This setting has no effect on Windows where overwrites are always allowed. | Boolean | optional | False |
| <a id="copy_to_directory-allow_overwrites"></a>allow_overwrites | If True, allow files to be overwritten if the same output file is copied to twice.<br><br> If set, then the order of srcs matters as the last copy of a particular file will win.<br><br> This setting has no effect on Windows where overwrites are always allowed. | Boolean | optional | False |
| <a id="copy_to_directory-exclude_prefixes"></a>exclude_prefixes | List of path prefixes to exclude from output directory.<br><br> If the output directory path for a file or directory starts with or is equal to a path in the list then that file is not copied to the output directory.<br><br> Exclude prefixes are matched *before* replace_prefixes are applied. | List of strings | optional | [] |
| <a id="copy_to_directory-include_external_repositories"></a>include_external_repositories | List of external repository names to include in the output directory.<br><br> Files from external repositories are not copied into the output directory unless the external repository they come from is listed here.<br><br> When copied from an external repository, the file path in the output directory defaults to the file's path within the external repository. The external repository name is _not_ included in that path.<br><br> For example, the following copies <code>@external_repo//path/to:file</code> to <code>path/to/file</code> within the output directory.<br><br> <pre><code> copy_to_directory( name = "dir", include_external_repositories = ["external_repo"], srcs = ["@external_repo//path/to:file"], ) </code></pre><br><br> Files from external repositories are subject to <code>root_paths</code>, <code>exclude_prefixes</code> and <code>replace_prefixes</code> in the same way as files form the main repository. | List of strings | optional | [] |
| <a id="copy_to_directory-out"></a>out | Path of the output directory, relative to this package.<br><br> If not set, the name of the target is used. | String | optional | "" |
| <a id="copy_to_directory-replace_prefixes"></a>replace_prefixes | Map of paths prefixes to replace in the output directory path when copying files.<br><br> If the output directory path for a file or directory starts with or is equal to a key in the dict then the matching portion of the output directory path is replaced with the dict value for that key.<br><br> Forward slashes (<code>/</code>) should be used as path separators. The final path segment of the key can be a partial match in the corresponding segment of the output directory path.<br><br> If there are multiple keys that match, the longest match wins. | <a href="https://bazel.build/docs/skylark/lib/dict.html">Dictionary: String -> String</a> | optional | {} |
| <a id="copy_to_directory-root_paths"></a>root_paths | List of paths that are roots in the output directory.<br><br> "." values indicate the targets package path.<br><br> If a file or directory being copied is in one of the listed paths or one of its subpaths, the output directory path is the path relative to the root path instead of the path relative to the file's workspace.<br><br> Forward slashes (<code>/</code>) should be used as path separators. Partial matches on the final path segment of a root path against the corresponding segment in the full workspace relative path of a file are not matched.<br><br> If there are multiple root paths that match, the longest match wins.<br><br> Defaults to [package_name()] so that the output directory path of files in the target's package and and sub-packages are relative to the target's package and files outside of that retain their full workspace relative paths. | List of strings | optional | ["."] |
| <a id="copy_to_directory-srcs"></a>srcs | Files and/or directories or targets that provide DirectoryPathInfo to copy into the output directory. | <a href="https://bazel.build/docs/build-ref.html#labels">List of labels</a> | optional | [] |
<a id="copy_to_directory_action"></a>
## copy_to_directory_action
<pre>
copy_to_directory_action(<a href="#copy_to_directory_action-ctx">ctx</a>, <a href="#copy_to_directory_action-srcs">srcs</a>, <a href="#copy_to_directory_action-dst">dst</a>, <a href="#copy_to_directory_action-additional_files">additional_files</a>, <a href="#copy_to_directory_action-root_paths">root_paths</a>,
<a href="#copy_to_directory_action-include_external_repositories">include_external_repositories</a>, <a href="#copy_to_directory_action-exclude_prefixes">exclude_prefixes</a>, <a href="#copy_to_directory_action-replace_prefixes">replace_prefixes</a>,
<a href="#copy_to_directory_action-allow_overwrites">allow_overwrites</a>, <a href="#copy_to_directory_action-is_windows">is_windows</a>)
</pre>
Helper function to copy files to a directory.
This helper is used by copy_to_directory. It is exposed as a public API so it can be used within
other rule implementations where additional_files can also be passed in.
**PARAMETERS**
| Name | Description | Default Value |
| :------------- | :------------- | :------------- |
| <a id="copy_to_directory_action-ctx"></a>ctx | The rule context. | none |
| <a id="copy_to_directory_action-srcs"></a>srcs | Files and/or directories or targets that provide DirectoryPathInfo to copy into the output directory. | none |
| <a id="copy_to_directory_action-dst"></a>dst | The directory to copy to. Must be a TreeArtifact. | none |
| <a id="copy_to_directory_action-additional_files"></a>additional_files | Additional files to copy that are not in the DefaultInfo or DirectoryPathInfo of srcs | <code>[]</code> |
| <a id="copy_to_directory_action-root_paths"></a>root_paths | List of paths that are roots in the output directory.<br><br>See copy_to_directory rule documentation for more details. | <code>["."]</code> |
| <a id="copy_to_directory_action-include_external_repositories"></a>include_external_repositories | List of external repository names to include in the output directory.<br><br>See copy_to_directory rule documentation for more details. | <code>[]</code> |
| <a id="copy_to_directory_action-exclude_prefixes"></a>exclude_prefixes | List of path prefixes to exclude from output directory.<br><br>See copy_to_directory rule documentation for more details. | <code>[]</code> |
| <a id="copy_to_directory_action-replace_prefixes"></a>replace_prefixes | Map of paths prefixes to replace in the output directory path when copying files.<br><br>See copy_to_directory rule documentation for more details. | <code>{}</code> |
| <a id="copy_to_directory_action-allow_overwrites"></a>allow_overwrites | If True, allow files to be overwritten if the same output file is copied to twice.<br><br>See copy_to_directory rule documentation for more details. | <code>False</code> |
| <a id="copy_to_directory_action-is_windows"></a>is_windows | If true, an cmd.exe action is created so there is no bash dependency. | <code>False</code> |
<a id="copy_to_directory_lib.impl"></a>
## copy_to_directory_lib.impl

View File

@ -6,11 +6,13 @@ the `root_paths`, `exclude_prefixes` and `replace_prefixes` attributes.
load(
"//lib/private:copy_to_directory.bzl",
_copy_to_directory_action = "copy_to_directory_action",
_copy_to_directory_lib = "copy_to_directory_lib",
)
# export the starlark library as a public API
copy_to_directory_lib = _copy_to_directory_lib
copy_to_directory_action = _copy_to_directory_action
copy_to_directory = rule(
implementation = _copy_to_directory_lib.impl,

View File

@ -11,6 +11,13 @@ _copy_to_directory_attr = {
doc = """Files and/or directories or targets that provide DirectoryPathInfo to copy
into the output directory.""",
),
# Cannot declare out as an output here, because there's no API for declaring
# TreeArtifact outputs.
"out": attr.string(
doc = """Path of the output directory, relative to this package.
If not set, the name of the target is used.""",
),
"root_paths": attr.string_list(
default = ["."],
doc = """List of paths that are roots in the output directory.
@ -78,6 +85,7 @@ _copy_to_directory_attr = {
),
"allow_overwrites": attr.bool(
doc = """If True, allow files to be overwritten if the same output file is copied to twice.
If set, then the order of srcs matters as the last copy of a particular file will win.
This setting has no effect on Windows where overwrites are always allowed.""",
@ -98,7 +106,12 @@ def _longest_match(subject, tests, allow_partial = False):
return match
# src can either be a File or a target with a DirectoryPathInfo
def _copy_paths(ctx, root_paths, src):
def _copy_paths(
src,
root_paths,
include_external_repositories,
exclude_prefixes,
replace_prefixes):
if type(src) == "File":
src_file = src
src_path = src_file.path
@ -112,7 +125,7 @@ def _copy_paths(ctx, root_paths, src):
# if the file is from an external repository check if that repository should
# be included in the output directory
if src_file.owner and src_file.owner.workspace_name and not src_file.owner.workspace_name in ctx.attr.include_external_repositories:
if src_file.owner and src_file.owner.workspace_name and not src_file.owner.workspace_name in include_external_repositories:
return None, None, None
# strip root paths
@ -122,19 +135,19 @@ def _copy_paths(ctx, root_paths, src):
output_path = "/".join(output_path.split("/")[strip_depth:])
# check if this file matches an exclude_prefix
match = _longest_match(output_path, ctx.attr.exclude_prefixes, True)
match = _longest_match(output_path, exclude_prefixes, True)
if match:
# file is excluded due to match in exclude_prefix
return None, None, None
# apply a replacement if one is found
match = _longest_match(output_path, ctx.attr.replace_prefixes.keys(), True)
match = _longest_match(output_path, replace_prefixes.keys(), True)
if match:
output_path = ctx.attr.replace_prefixes[match] + output_path[len(match):]
output_path = replace_prefixes[match] + output_path[len(match):]
return src_path, output_path, src_file
def _copy_to_dir_bash(ctx, copy_paths, dst_dir):
def _copy_to_dir_bash(ctx, copy_paths, dst_dir, allow_overwrites):
cmds = [
"set -o errexit -o nounset -o pipefail",
"OUT_CAPTURE=$(mktemp)",
@ -154,15 +167,15 @@ def _copy_to_dir_bash(ctx, copy_paths, dst_dir):
for src_path, dst_path, src_file in copy_paths:
inputs.append(src_file)
maybe_force = "-f " if ctx.attr.allow_overwrites else "-n "
maybe_force = "-f " if allow_overwrites else "-n "
maybe_chmod_file = """if [ -e "{dst}" ]; then
chmod a+w "{dst}"
fi
""" if ctx.attr.allow_overwrites else ""
""" if allow_overwrites else ""
maybe_chmod_dir = """if [ -e "{dst}" ]; then
chmod -R a+w "{dst}"
fi
""" if ctx.attr.allow_overwrites else ""
""" if allow_overwrites else ""
cmds.append("""
if [[ ! -e "{src}" ]]; then echo "file '{src}' does not exist"; exit 1; fi
@ -272,35 +285,122 @@ def _copy_to_directory_impl(ctx):
msg = "srcs must not be empty in copy_to_directory %s" % ctx.label
fail(msg)
# Replace "." root paths with the package name of the target
root_paths = [p if p != "." else ctx.label.package for p in ctx.attr.root_paths]
dst = ctx.actions.declare_directory(ctx.attr.out if ctx.attr.out else ctx.attr.name)
output = ctx.actions.declare_directory(ctx.attr.name)
copy_to_directory_action(
ctx,
srcs = ctx.attr.srcs,
dst = dst,
root_paths = ctx.attr.root_paths,
include_external_repositories = ctx.attr.include_external_repositories,
exclude_prefixes = ctx.attr.exclude_prefixes,
replace_prefixes = ctx.attr.replace_prefixes,
allow_overwrites = ctx.attr.allow_overwrites,
is_windows = is_windows,
)
return [
DefaultInfo(
files = depset([dst]),
runfiles = ctx.runfiles([dst]),
),
]
def copy_to_directory_action(
ctx,
srcs,
dst,
additional_files = [],
root_paths = ["."],
include_external_repositories = [],
exclude_prefixes = [],
replace_prefixes = {},
allow_overwrites = False,
is_windows = False):
"""Helper function to copy files to a directory.
This helper is used by copy_to_directory. It is exposed as a public API so it can be used within
other rule implementations where additional_files can also be passed in.
Args:
ctx: The rule context.
srcs: Files and/or directories or targets that provide DirectoryPathInfo to copy into the output directory.
dst: The directory to copy to. Must be a TreeArtifact.
additional_files: Additional files to copy that are not in the DefaultInfo or DirectoryPathInfo of srcs
root_paths: List of paths that are roots in the output directory.
See copy_to_directory rule documentation for more details.
include_external_repositories: List of external repository names to include in the output directory.
See copy_to_directory rule documentation for more details.
exclude_prefixes: List of path prefixes to exclude from output directory.
See copy_to_directory rule documentation for more details.
replace_prefixes: Map of paths prefixes to replace in the output directory path when copying files.
See copy_to_directory rule documentation for more details.
allow_overwrites: If True, allow files to be overwritten if the same output file is copied to twice.
See copy_to_directory rule documentation for more details.
is_windows: If true, an cmd.exe action is created so there is no bash dependency.
"""
if not srcs:
fail("srcs must not be empty")
# Replace "." root paths with the package name of the target
root_paths = [p if p != "." else ctx.label.package for p in root_paths]
# Gather a list of src_path, dst_path pairs
copy_paths = []
for src in ctx.attr.srcs:
for src in srcs:
if DirectoryPathInfo in src:
src_path, output_path, src_file = _copy_paths(ctx, root_paths, src)
src_path, output_path, src_file = _copy_paths(
src = src,
root_paths = root_paths,
include_external_repositories = include_external_repositories,
exclude_prefixes = exclude_prefixes,
replace_prefixes = replace_prefixes,
)
if src_path != None:
dst_path = skylib_paths.normalize("/".join([output.path, output_path]))
dst_path = skylib_paths.normalize("/".join([dst.path, output_path]))
copy_paths.append((src_path, dst_path, src_file))
for src_file in ctx.files.srcs:
src_path, output_path, src_file = _copy_paths(ctx, root_paths, src_file)
if DefaultInfo in src:
for src_file in src[DefaultInfo].files.to_list():
src_path, output_path, src_file = _copy_paths(
src = src_file,
root_paths = root_paths,
include_external_repositories = include_external_repositories,
exclude_prefixes = exclude_prefixes,
replace_prefixes = replace_prefixes,
)
if src_path != None:
dst_path = skylib_paths.normalize("/".join([dst.path, output_path]))
copy_paths.append((src_path, dst_path, src_file))
for additional_file in additional_files:
src_path, output_path, src_file = _copy_paths(
src = additional_file,
root_paths = root_paths,
include_external_repositories = include_external_repositories,
exclude_prefixes = exclude_prefixes,
replace_prefixes = replace_prefixes,
)
if src_path != None:
dst_path = skylib_paths.normalize("/".join([output.path, output_path]))
dst_path = skylib_paths.normalize("/".join([dst.path, output_path]))
copy_paths.append((src_path, dst_path, src_file))
if is_windows:
_copy_to_dir_cmd(ctx, copy_paths, output)
_copy_to_dir_cmd(ctx, copy_paths, dst)
else:
_copy_to_dir_bash(ctx, copy_paths, output)
return [
DefaultInfo(
files = depset([output]),
runfiles = ctx.runfiles([output]),
),
]
_copy_to_dir_bash(ctx, copy_paths, dst, allow_overwrites)
copy_to_directory_lib = struct(
attrs = _copy_to_directory_attr,

View File

@ -0,0 +1 @@
1

View File

@ -0,0 +1 @@
1

View File

@ -0,0 +1,31 @@
load("//lib:diff_test.bzl", "diff_test")
load("//lib:copy_to_directory.bzl", "copy_to_directory")
load(":lib.bzl", "lib")
load(":pkg.bzl", "pkg")
lib(
name = "lib",
srcs = ["1"],
others = ["2"],
)
# pkg should copy DefaultInfo files and OtherInfo files
pkg(
name = "pkg",
srcs = [":lib"],
out = "pkg",
)
copy_to_directory(
name = "expected_pkg",
srcs = [
"1",
"2",
],
)
diff_test(
name = "test",
file1 = ":pkg",
file2 = ":expected_pkg",
)

View File

@ -0,0 +1,22 @@
"""
Test rule to create a lib with a DefaultInfo and a OtherInfo
"""
load(":other_info.bzl", "OtherInfo")
_attrs = {
"srcs": attr.label_list(allow_files = True),
"others": attr.label_list(allow_files = True),
}
def _impl(ctx):
return [
DefaultInfo(files = depset(ctx.files.srcs)),
OtherInfo(files = depset(ctx.files.others)),
]
lib = rule(
implementation = _impl,
attrs = _attrs,
provides = [DefaultInfo, OtherInfo],
)

View File

@ -0,0 +1,8 @@
"""For testing"""
OtherInfo = provider(
doc = "For testing",
fields = {
"files": "A depset of files",
},
)

View File

@ -0,0 +1,42 @@
"""
Test rule to create a pkg with DefaultInfo and OtherInfo files
"""
load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory_action")
load(":other_info.bzl", "OtherInfo")
_attrs = {
"srcs": attr.label_list(allow_files = True),
"out": attr.string(mandatory = True),
"_windows_constraint": attr.label(default = "@platforms//os:windows"),
}
def _impl(ctx):
is_windows = ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo])
dst = ctx.actions.declare_directory(ctx.attr.out)
additional_files_depsets = []
# include files from OtherInfo of srcs
for src in ctx.attr.srcs:
if OtherInfo in src:
additional_files_depsets.append(src[OtherInfo].files)
copy_to_directory_action(
ctx,
srcs = ctx.attr.srcs,
dst = dst,
additional_files = depset(transitive = additional_files_depsets).to_list(),
is_windows = is_windows,
)
return [
DefaultInfo(files = depset([dst])),
]
pkg = rule(
implementation = _impl,
attrs = _attrs,
provides = [DefaultInfo],
)