1# Copyright 2022 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Implementation of copy_directory macro and underlying rules. 16 17This rule copies a directory to another location using Bash (on Linux/macOS) or 18cmd.exe (on Windows). 19""" 20 21load(":copy_common.bzl", "COPY_EXECUTION_REQUIREMENTS") 22 23def _copy_cmd(ctx, src, dst): 24 # Most Windows binaries built with MSVC use a certain argument quoting 25 # scheme. Bazel uses that scheme too to quote arguments. However, 26 # cmd.exe uses different semantics, so Bazel's quoting is wrong here. 27 # To fix that we write the command to a .bat file so no command line 28 # quoting or escaping is required. 29 # Put a hash of the file name into the name of the generated batch file to 30 # make it unique within the package, so that users can define multiple copy_file's. 31 bat = ctx.actions.declare_file("%s-%s-cmd.bat" % (ctx.label.name, hash(src.path))) 32 33 # Flags are documented at 34 # https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy 35 # NB: robocopy return non-zero exit codes on success so we must exit 0 after calling it 36 cmd_tmpl = """\ 37if not exist \"{src}\\\" ( 38 echo Error: \"{src}\" is not a directory 39 @exit 1 40) 41@robocopy \"{src}\" \"{dst}\" /E /MIR >NUL & @exit 0 42""" 43 mnemonic = "CopyDirectory" 44 progress_message = "Copying directory %{input}" 45 46 ctx.actions.write( 47 output = bat, 48 # Do not use lib/shell.bzl's shell.quote() method, because that uses 49 # Bash quoting syntax, which is different from cmd.exe's syntax. 50 content = cmd_tmpl.format( 51 src = src.path.replace("/", "\\"), 52 dst = dst.path.replace("/", "\\"), 53 ), 54 is_executable = True, 55 ) 56 ctx.actions.run( 57 inputs = [src, bat], 58 outputs = [dst], 59 executable = "cmd.exe", 60 arguments = ["/C", bat.path.replace("/", "\\")], 61 mnemonic = mnemonic, 62 progress_message = progress_message, 63 use_default_shell_env = True, 64 execution_requirements = COPY_EXECUTION_REQUIREMENTS, 65 ) 66 67def _copy_bash(ctx, src, dst): 68 cmd = """\ 69if [ ! -d \"$1\" ]; then 70 echo \"Error: $1 is not a directory\" 71 exit 1 72fi 73 74rm -rf \"$2\" && cp -fR \"$1/\" \"$2\" 75""" 76 mnemonic = "CopyDirectory" 77 progress_message = "Copying directory %s" % src.path 78 79 ctx.actions.run_shell( 80 inputs = [src], 81 outputs = [dst], 82 command = cmd, 83 arguments = [src.path, dst.path], 84 mnemonic = mnemonic, 85 progress_message = progress_message, 86 use_default_shell_env = True, 87 execution_requirements = COPY_EXECUTION_REQUIREMENTS, 88 ) 89 90def copy_directory_action(ctx, src, dst, is_windows = False): 91 """Helper function that creates an action to copy a directory from src to dst. 92 93 This helper is used by copy_directory. It is exposed as a public API so it can be used within 94 other rule implementations. 95 96 Args: 97 ctx: The rule context. 98 src: The directory to make a copy of. Can be a source directory or TreeArtifact. 99 dst: The directory to copy to. Must be a TreeArtifact. 100 is_windows: If true, an cmd.exe action is created so there is no bash dependency. 101 """ 102 if dst.is_source or not dst.is_directory: 103 fail("dst must be a TreeArtifact") 104 if is_windows: 105 _copy_cmd(ctx, src, dst) 106 else: 107 _copy_bash(ctx, src, dst) 108 109def _copy_directory_impl(ctx): 110 dst = ctx.actions.declare_directory(ctx.attr.out) 111 copy_directory_action(ctx, ctx.file.src, dst, ctx.attr.is_windows) 112 113 files = depset(direct = [dst]) 114 runfiles = ctx.runfiles(files = [dst]) 115 116 return [DefaultInfo(files = files, runfiles = runfiles)] 117 118_copy_directory = rule( 119 implementation = _copy_directory_impl, 120 provides = [DefaultInfo], 121 attrs = { 122 "src": attr.label(mandatory = True, allow_single_file = True), 123 "is_windows": attr.bool(mandatory = True), 124 # Cannot declare out as an output here, because there's no API for declaring 125 # TreeArtifact outputs. 126 "out": attr.string(mandatory = True), 127 }, 128) 129 130def copy_directory(name, src, out, **kwargs): 131 """Copies a directory to another location. 132 133 This rule uses a Bash command on Linux/macOS/non-Windows, and a cmd.exe command on Windows (no Bash is required). 134 135 If using this rule with source directories, it is recommended that you use the 136 `--host_jvm_args=-DBAZEL_TRACK_SOURCE_DIRECTORIES=1` startup option so that changes 137 to files within source directories are detected. See 138 https://github.com/bazelbuild/bazel/commit/c64421bc35214f0414e4f4226cc953e8c55fa0d2 139 for more context. 140 141 Args: 142 name: Name of the rule. 143 src: The directory to make a copy of. Can be a source directory or TreeArtifact. 144 out: Path of the output directory, relative to this package. 145 **kwargs: further keyword arguments, e.g. `visibility` 146 """ 147 _copy_directory( 148 name = name, 149 src = src, 150 is_windows = select({ 151 "@bazel_tools//src/conditions:host_windows": True, 152 "//conditions:default": False, 153 }), 154 out = out, 155 **kwargs 156 ) 157