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 c3ba985
Show file tree
Hide file tree
Showing 12 changed files with 1,037 additions and 53 deletions.
13 changes: 6 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,19 @@ 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",
# Reference the copy of bootstrap.js in the output folder
entry_point = "bootstrap",
# TODO: make this example work with format = "esm"
format = "cjs",
output_dir = True,
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
2 changes: 2 additions & 0 deletions index.for_docs.bzl
Expand Up @@ -17,6 +17,7 @@
This differs from :index.bzl because we don't have wrapping macros that hide the real doc"""

load("//internal/common:check_bazel_version.bzl", _check_bazel_version = "check_bazel_version")
load("//internal/common:copy_to_bin.bzl", _copy_to_bin = "copy_to_bin")
load("//internal/node:node.bzl", _nodejs_binary = "nodejs_binary", _nodejs_test = "nodejs_test")
load("//internal/node:node_repositories.bzl", _node_repositories = "node_repositories_rule")
load("//internal/node:npm_package_bin.bzl", _npm_bin = "npm_package_bin")
Expand All @@ -25,6 +26,7 @@ load("//internal/npm_package:npm_package.bzl", _npm_package = "npm_package")
load("//internal/pkg_web:pkg_web.bzl", _pkg_web = "pkg_web")

check_bazel_version = _check_bazel_version
copy_to_bin = _copy_to_bin
nodejs_binary = _nodejs_binary
nodejs_test = _nodejs_test
node_repositories = _node_repositories
Expand Down
4 changes: 3 additions & 1 deletion internal/common/BUILD.bazel
Expand Up @@ -21,7 +21,9 @@ package(default_visibility = ["//internal:__subpackages__"])

bzl_library(
name = "bzl",
srcs = glob(["*.bzl"]),
srcs = glob(["*.bzl"]) + [
"//third_party/github.com/bazelbuild/bazel-skylib:bzl",
],
visibility = ["//visibility:public"],
)

Expand Down
65 changes: 65 additions & 0 deletions internal/common/copy_to_bin.bzl
@@ -0,0 +1,65 @@
# 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 workspace-relative path path.
e.g. `<workspace_root>/foo/bar/a.txt -> <bazel-bin>/foo/bar/a.txt`
This is useful to populate the output folder with all files needed at runtime, even
those which aren't outputs of a Bazel rule.
This way you can run a binary in the output folder (execroot or runfiles_root)
without that program needing to rely on a runfiles helper library or be aware that
files are divided between the source tree and the output tree.
Args:
name: Name of the rule.
srcs: 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
7 changes: 6 additions & 1 deletion 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 All @@ -10,7 +12,10 @@ filegroup(

bzl_library(
name = "bzl",
srcs = glob(["lib/*.bzl"]),
srcs = glob([
"lib/*.bzl",
"rules/**/*.bzl",
]),
visibility = ["//visibility:public"],
deps = [
"//internal/npm_install:bzl",
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

0 comments on commit c3ba985

Please sign in to comment.