Skip to content

Commit

Permalink
copy_file allows directories as well
Browse files Browse the repository at this point in the history
  • Loading branch information
alexeagle committed Oct 9, 2021
1 parent 506c172 commit 9880b1b
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 22 deletions.
90 changes: 68 additions & 22 deletions rules/private/copy_file_private.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,37 @@

"""Implementation of copy_file macro and underlying rules.
These rules copy a file to another location using Bash (on Linux/macOS) or
cmd.exe (on Windows). '_copy_xfile' marks the resulting file executable,
'_copy_file' does not.
These rules copy a file or directory to another location using Bash (on Linux/macOS) or
cmd.exe (on Windows). `_copy_xfile` marks the resulting file executable,
`_copy_file` does not.
"""

# Hints for Bazel spawn strategy
_execution_requirements = {
# Copying files is entirely IO-bound and there is no point doing this work remotely.
# Also, remote-execution does not allow source directory inputs, see
# https://github.com/bazelbuild/bazel/commit/c64421bc35214f0414e4f4226cc953e8c55fa0d2
# So we must not attempt to execute remotely in that case.
"no-remote-exec": "1",
}

def copy_cmd(ctx, src, dst):
# Most Windows binaries built with MSVC use a certain argument quoting
# scheme. Bazel uses that scheme too to quote arguments. However,
# cmd.exe uses different semantics, so Bazel's quoting is wrong here.
# To fix that we write the command to a .bat file so no command line
# quoting or escaping is required.
bat = ctx.actions.declare_file(ctx.label.name + "-cmd.bat")
is_dir = dst.is_directory
# Flags are documented at
# https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/copy
# https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/xcopy
cmd_tmpl = "@xcopy \"%s\" \"%s\\\" /V /E /H /Y /Q >NUL" if is_dir else "@copy /Y \"%s\" \"%s\" >NUL"
ctx.actions.write(
output = bat,
# Do not use lib/shell.bzl's shell.quote() method, because that uses
# Bash quoting syntax, which is different from cmd.exe's syntax.
content = "@copy /Y \"%s\" \"%s\" >NUL" % (
content = cmd_tmpl % (
src.path.replace("/", "\\"),
dst.path.replace("/", "\\"),
),
Expand All @@ -42,74 +56,104 @@ def copy_cmd(ctx, src, dst):
outputs = [dst],
executable = "cmd.exe",
arguments = ["/C", bat.path.replace("/", "\\")],
mnemonic = "CopyFile",
progress_message = "Copying files",
mnemonic = "CopyDirectory" if is_dir else "CopyFile",
progress_message = "Copying directory" if is_dir else "Copying file",
use_default_shell_env = True,
execution_requirements = _execution_requirements,
)

def copy_bash(ctx, src, dst):
is_dir = dst.is_directory
cmd_tmpl = "rm -rf \"$2\" && cp -rf \"$1/\" \"$2\"" if is_dir else "cp -f \"$1\" \"$2\""
ctx.actions.run_shell(
tools = [src],
outputs = [dst],
command = "cp -f \"$1\" \"$2\"",
command = cmd_tmpl,
arguments = [src.path, dst.path],
mnemonic = "CopyFile",
progress_message = "Copying files",
mnemonic = "CopyDirectory" if is_dir else "CopyFile",
progress_message = "Copying directory" if is_dir else "Copying file",
use_default_shell_env = True,
execution_requirements = _execution_requirements,
)

def _copy_file_impl(ctx):
# When creating a directory, declare that to Bazel so downstream rules
# see it as a TreeArtifact and handle correctly, e.g. for remote execution
if getattr(ctx.attr, "is_directory", False):
output = ctx.actions.declare_directory(ctx.attr.out)
else:
output = ctx.outputs.out
if ctx.attr.allow_symlink:
ctx.actions.symlink(
output = ctx.outputs.out,
output = output,
target_file = ctx.file.src,
is_executable = ctx.attr.is_executable,
)
elif ctx.attr.is_windows:
copy_cmd(ctx, ctx.file.src, ctx.outputs.out)
copy_cmd(ctx, ctx.file.src, output)
else:
copy_bash(ctx, ctx.file.src, ctx.outputs.out)
copy_bash(ctx, ctx.file.src, output)

files = depset(direct = [ctx.outputs.out])
runfiles = ctx.runfiles(files = [ctx.outputs.out])
files = depset(direct = [output])
runfiles = ctx.runfiles(files = [output])
if ctx.attr.is_executable:
return [DefaultInfo(files = files, runfiles = runfiles, executable = ctx.outputs.out)]
return [DefaultInfo(files = files, runfiles = runfiles, executable = output)]
else:
return [DefaultInfo(files = files, runfiles = runfiles)]

_ATTRS = {
"src": attr.label(mandatory = True, allow_single_file = True),
"out": attr.output(mandatory = True),
"is_windows": attr.bool(mandatory = True),
"is_executable": attr.bool(mandatory = True),
"allow_symlink": attr.bool(mandatory = True),
}

_copy_directory = rule(
implementation = _copy_file_impl,
provides = [DefaultInfo],
attrs = dict(_ATTRS, **{
"is_directory": attr.bool(default = True),
"out": attr.string(mandatory = True),
}),
)

_copy_file = rule(
implementation = _copy_file_impl,
provides = [DefaultInfo],
attrs = _ATTRS,
attrs = dict(_ATTRS, **{
"out": attr.output(mandatory = True),
}),
)

_copy_xfile = rule(
implementation = _copy_file_impl,
executable = True,
provides = [DefaultInfo],
attrs = _ATTRS,
attrs = dict(_ATTRS, **{
"out": attr.output(mandatory = True),
}),
)

def copy_file(name, src, out, is_executable = False, allow_symlink = False, **kwargs):
"""Copies a file to another location.
def copy_file(name, src, out, is_directory = False, is_executable = False, allow_symlink = False, **kwargs):
"""Copies a file or directory to another location.
`native.genrule()` is sometimes used to copy files (often wishing to rename them). The 'copy_file' rule does this with a simpler interface than genrule.
This rule uses a Bash command on Linux/macOS/non-Windows, and a cmd.exe command on Windows (no Bash is required).
If using this rule with source directories, it is recommended that you use the
`--host_jvm_args=-DBAZEL_TRACK_SOURCE_DIRECTORIES=1` startup option so that changes
to files within source directories are detected. See
https://github.com/bazelbuild/bazel/commit/c64421bc35214f0414e4f4226cc953e8c55fa0d2
for more context.
Args:
name: Name of the rule.
src: A Label. The file to make a copy of. (Can also be the label of a rule
that generates a file.)
src: A Label. The file or directory to make a copy of.
(Can also be the label of a rule that generates a file or directory.)
out: Path of the output file, relative to this package.
is_directory: treat the source file as a directory
Workaround for https://github.com/bazelbuild/bazel/issues/12954
is_executable: A boolean. Whether to make the output file executable. When
True, the rule's output can be executed using `bazel run` and can be
in the srcs of binary and test rules that require executable sources.
Expand All @@ -126,6 +170,8 @@ def copy_file(name, src, out, is_executable = False, allow_symlink = False, **kw
copy_file_impl = _copy_file
if is_executable:
copy_file_impl = _copy_xfile
elif is_directory:
copy_file_impl = _copy_directory

copy_file_impl(
name = name,
Expand Down
7 changes: 7 additions & 0 deletions tests/copy_file/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ copy_file(
is_executable = True,
)

copy_file(
name = "copy_dir",
src = ":subdir",
out = "out/subdir",
is_directory = True,
)

genrule(
name = "gen",
outs = ["b.txt"],
Expand Down
1 change: 1 addition & 0 deletions tests/copy_file/subdir/b.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
contents of b

0 comments on commit 9880b1b

Please sign in to comment.