Skip to content

Commit

Permalink
feat(builtin): introduce copy_to_bin rule
Browse files Browse the repository at this point in the history
Based on meteorcloudy@ awesome work in bazelbuild/bazel-skylib#217
That PR isn't getting approved for upstream so we vendor it into our own ruleset
  • Loading branch information
alexeagle committed Dec 13, 2019
1 parent 45f4fe6 commit 1e4b5df
Show file tree
Hide file tree
Showing 11 changed files with 1,021 additions and 51 deletions.
12 changes: 5 additions & 7 deletions examples/kotlin/BUILD.bazel
@@ -1,7 +1,7 @@
# Add rules here to build your software
# See https://docs.bazel.build/versions/master/build-ref.html#BUILD_files

load("@build_bazel_rules_nodejs//:index.bzl", "pkg_web")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin", "pkg_web")
load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_js_import", "kt_js_library")
load("@npm//http-server:index.bzl", "http_server")
load("@npm_bazel_jasmine//:index.bzl", "jasmine_node_test")
Expand All @@ -22,20 +22,18 @@ kt_js_library(
deps = [":kotlinx-html-js"],
)

# Copy bootstrap.js to the bin folder as _bootstrap.js
# so that relative import to `./hello.js` is valid
genrule(
# Copy bootstrap.js to the output folder, so all files are next to each other at runtime
# Allows the `./hello.js` relative import to work while referencing an output file
copy_to_bin(
name = "bootstrap",
srcs = ["bootstrap.js"],
outs = ["_bootstrap.js"],
cmd = "cp $< $@",
)

rollup_bundle(
name = "bundle",
srcs = ["hello.js"],
config_file = "rollup.config.js",
entry_point = "_bootstrap.js",
entry_point = "bootstrap",
# TODO: make this example work with format = "esm"
format = "cjs",
output_dir = True,
Expand Down
1 change: 1 addition & 0 deletions examples/kotlin/rollup.config.js
@@ -1,6 +1,7 @@
const node = require('rollup-plugin-node-resolve');
const commonjs = require('rollup-plugin-commonjs');

console.log(process.env['COMPILATION_MODE']);
module.exports = {
plugins: [
node({
Expand Down
2 changes: 2 additions & 0 deletions index.bzl
Expand Up @@ -19,6 +19,7 @@ Users should not load files under "/internal"

load("//internal/common:check_bazel_version.bzl", _check_bazel_version = "check_bazel_version")
load("//internal/common:check_version.bzl", "check_version")
load("//internal/common:copy_to_bin.bzl", _copy_to_bin = "copy_to_bin")
load("//internal/jasmine_node_test:jasmine_node_test.bzl", _jasmine_node_test = "jasmine_node_test")
load(
"//internal/node:node.bzl",
Expand All @@ -39,6 +40,7 @@ jasmine_node_test = _jasmine_node_test
npm_package = _npm_package
npm_package_bin = _npm_bin
pkg_web = _pkg_web
copy_to_bin = _copy_to_bin
# ANY RULES ADDED HERE SHOULD BE DOCUMENTED, see index.for_docs.bzl

# Allows us to avoid a transitive dependency on bazel_skylib from leaking to users
Expand Down
58 changes: 58 additions & 0 deletions internal/common/copy_to_bin.bzl
@@ -0,0 +1,58 @@
# Copyright 2019 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"copy_to_bin() rule"

load("//third_party/github.com/bazelbuild/bazel-skylib:rules/private/copy_file_private.bzl", "copy_bash", "copy_cmd")

def _copy_to_bin_impl(ctx):
all_dst = []
for src in ctx.files.srcs:
if not src.is_source:
fail("A source file must be specified in copy_to_bin rule, %s is not a source file." % src.path)
dst = ctx.actions.declare_file(src.basename, sibling = src)
if ctx.attr.is_windows:
copy_cmd(ctx, src, dst)
else:
copy_bash(ctx, src, dst)
all_dst.append(dst)
return DefaultInfo(files = depset(all_dst), runfiles = ctx.runfiles(files = all_dst))

_copy_to_bin = rule(
implementation = _copy_to_bin_impl,
attrs = {
"srcs": attr.label_list(mandatory = True, allow_files = True),
"is_windows": attr.bool(mandatory = True, doc = "Automatically set by macro"),
},
)

def copy_to_bin(name, srcs, **kwargs):
"""Copies a source file to bazel-bin at the same execroot path.
Eg. <source_root>/foo/bar/a.txt -> <bazel-bin>/foo/bar/a.txt
Args:
name: Name of the rule.
src: A List of Labels. File(s) to to copy.
**kwargs: further keyword arguments, e.g. `visibility`
"""
_copy_to_bin(
name = name,
srcs = srcs,
is_windows = select({
"@bazel_tools//src/conditions:host_windows": True,
"//conditions:default": False,
}),
**kwargs
)
20 changes: 20 additions & 0 deletions internal/common/test/BUILD.bazel
@@ -0,0 +1,20 @@
load("//internal/common:copy_to_bin.bzl", "copy_to_bin")

licenses(["notice"])

package(default_testonly = 1)

sh_test(
name = "copy_to_bin_tests",
srcs = ["copy_to_bin_tests.sh"],
data = [
":a",
"//third_party/github.com/bazelbuild/bazel-skylib:tests/unittest.bash",
],
deps = ["@bazel_tools//tools/bash/runfiles"],
)

copy_to_bin(
name = "a",
srcs = ["foo/bar/a.txt"],
)
51 changes: 51 additions & 0 deletions internal/common/test/copy_to_bin_tests.sh
@@ -0,0 +1,51 @@
# Copyright 2019 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# --- begin runfiles.bash initialization ---
# Copy-pasted from Bazel's Bash runfiles library (tools/bash/runfiles/runfiles.bash).
set -euo pipefail
if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
if [[ -f "$0.runfiles_manifest" ]]; then
export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
elif [[ -f "$0.runfiles/MANIFEST" ]]; then
export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
export RUNFILES_DIR="$0.runfiles"
fi
fi
if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
"$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
else
echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"
exit 1
fi
# --- end runfiles.bash initialization ---

source "$(rlocation build_bazel_rules_nodejs/third_party/github.com/bazelbuild/bazel-skylib/tests/unittest.bash)" \
|| { echo "Could not source build_bazel_rules_nodejs/third_party/github.com/bazelbuild/bazel-skylib/tests/unittest.bash" >&2; exit 1; }

function test_map_to_output() {
echo "$(rlocation build_bazel_rules_nodejs/internal/common/test/foo/bar/a.txt)" >"$TEST_log"
# Test the foo/bar/a.txt is copied to bazel-out/
expect_log 'bazel-out/'
cat "$(rlocation build_bazel_rules_nodejs/internal/common/test/foo/bar/a.txt)" >"$TEST_log"
# Test the content of foo/bar/a.txt is correct
expect_log '#!/bin/bash'
expect_log '^echo aaa$'
}

run_suite "map_to_output test suite"
2 changes: 2 additions & 0 deletions internal/common/test/foo/bar/a.txt
@@ -0,0 +1,2 @@
#!/bin/bash
echo aaa
2 changes: 2 additions & 0 deletions third_party/github.com/bazelbuild/bazel-skylib/BUILD
Expand Up @@ -2,6 +2,8 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library")

licenses(["notice"])

exports_files(["tests/unittest.bash"])

filegroup(
name = "package_contents",
srcs = glob(["**"]),
Expand Down
Expand Up @@ -19,43 +19,50 @@ cmd.exe (on Windows). '_copy_xfile' marks the resulting file executable,
'_copy_file' does not.
"""

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")
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" % (
src.path.replace("/", "\\"),
dst.path.replace("/", "\\"),
),
is_executable = True,
)
ctx.actions.run(
inputs = [src],
tools = [bat],
outputs = [dst],
executable = "cmd.exe",
arguments = ["/C", bat.path.replace("/", "\\")],
mnemonic = "CopyFile",
progress_message = "Copying files",
use_default_shell_env = True,
)

def copy_bash(ctx, src, dst):
ctx.actions.run_shell(
tools = [src],
outputs = [dst],
command = "cp -f \"$1\" \"$2\"",
arguments = [src.path, dst.path],
mnemonic = "CopyFile",
progress_message = "Copying files",
use_default_shell_env = True,
)

def _common_impl(ctx, is_executable):
if ctx.attr.is_windows:
# 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")
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" % (
ctx.file.src.path.replace("/", "\\"),
ctx.outputs.out.path.replace("/", "\\"),
),
is_executable = True,
)
ctx.actions.run(
inputs = [ctx.file.src, bat],
outputs = [ctx.outputs.out],
executable = "cmd.exe",
arguments = ["/C", bat.path.replace("/", "\\")],
mnemonic = "CopyFile",
progress_message = "Copying files",
use_default_shell_env = True,
)
copy_cmd(ctx, ctx.file.src, ctx.outputs.out)
else:
ctx.actions.run_shell(
inputs = [ctx.file.src],
outputs = [ctx.outputs.out],
command = "cp -f \"$1\" \"$2\"",
arguments = [ctx.file.src.path, ctx.outputs.out.path],
mnemonic = "CopyFile",
progress_message = "Copying files",
use_default_shell_env = True,
)
copy_bash(ctx, ctx.file.src, ctx.outputs.out)

files = depset(direct = [ctx.outputs.out])
runfiles = ctx.runfiles(files = [ctx.outputs.out])
Expand Down
Expand Up @@ -18,11 +18,18 @@ These rules write a UTF-8 encoded text file, using Bazel's FileWriteAction.
'_write_xfile' marks the resulting file executable, '_write_file' does not.
"""

def _common_impl(ctx, is_executable):
def _common_impl(ctx, is_windows, is_executable):
if ctx.attr.newline == "auto":
newline = "\r\n" if is_windows else "\n"
elif ctx.attr.newline == "windows":
newline = "\r\n"
else:
newline = "\n"

# ctx.actions.write creates a FileWriteAction which uses UTF-8 encoding.
ctx.actions.write(
output = ctx.outputs.out,
content = "\n".join(ctx.attr.content) if ctx.attr.content else "",
content = newline.join(ctx.attr.content) if ctx.attr.content else "",
is_executable = is_executable,
)
files = depset(direct = [ctx.outputs.out])
Expand All @@ -33,14 +40,16 @@ def _common_impl(ctx, is_executable):
return [DefaultInfo(files = files, runfiles = runfiles)]

def _impl(ctx):
return _common_impl(ctx, False)
return _common_impl(ctx, ctx.attr.is_windows, False)

def _ximpl(ctx):
return _common_impl(ctx, True)
return _common_impl(ctx, ctx.attr.is_windows, True)

_ATTRS = {
"content": attr.string_list(mandatory = False, allow_empty = True),
"out": attr.output(mandatory = True),
"content": attr.string_list(mandatory = False, allow_empty = True),
"newline": attr.string(values = ["unix", "windows", "auto"], default = "auto"),
"is_windows": attr.bool(mandatory = True),
}

_write_file = rule(
Expand All @@ -56,30 +65,49 @@ _write_xfile = rule(
attrs = _ATTRS,
)

def write_file(name, out, content = [], is_executable = False, **kwargs):
def write_file(
name,
out,
content = [],
is_executable = False,
newline = "auto",
**kwargs):
"""Creates a UTF-8 encoded text file.
Args:
name: Name of the rule.
out: Path of the output file, relative to this package.
content: A list of strings. Lines of text, the contents of the file.
Newlines are added automatically after every line except the last one.
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.
**kwargs: further keyword arguments, e.g. `visibility`
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.
newline: one of ["auto", "unix", "windows"]: line endings to use. "auto"
for platform-determined, "unix" for LF, and "windows" for CRLF.
**kwargs: further keyword arguments, e.g. <code>visibility</code>
"""
if is_executable:
_write_xfile(
name = name,
content = content,
out = out,
newline = newline or "auto",
is_windows = select({
"@bazel_tools//src/conditions:host_windows": True,
"//conditions:default": False,
}),
**kwargs
)
else:
_write_file(
name = name,
content = content,
out = out,
newline = newline or "auto",
is_windows = select({
"@bazel_tools//src/conditions:host_windows": True,
"//conditions:default": False,
}),
**kwargs
)

0 comments on commit 1e4b5df

Please sign in to comment.