xref: /aosp_15_r20/external/bazel-skylib/rules/private/copy_directory_private.bzl (revision bcb5dc7965af6ee42bf2f21341a2ec00233a8c8a)
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