From 9afb8db79b01ed50741e7a66e7ab3415bcbcb2bf Mon Sep 17 00:00:00 2001 From: globegitter Date: Thu, 13 Jun 2019 20:49:36 +0200 Subject: [PATCH] feat(builtin): add nodejs toolchain support --- BUILD.bazel | 2 + README.md | 18 ++++ internal/common/os_name.bzl | 17 +++- internal/node/BUILD.bazel | 6 +- internal/node/BUILD.nodejs_host_os_alias.tpl | 27 +++++ internal/node/generate_build_file.js | 6 +- internal/node/node.bzl | 60 ++++++----- internal/node/node_labels.bzl | 40 ++++---- internal/node/node_repositories.bzl | 97 ++++++++++++++---- internal/node/test/BUILD.bazel | 3 + internal/node/test/nodejs_toolchain_test.bzl | 101 +++++++++++++++++++ internal/npm_install/npm_install.bzl | 13 +-- internal/npm_package/npm_package.bzl | 4 + internal/npm_package/packager.js | 6 +- package.bzl | 8 +- toolchains/node/BUILD.bazel | 87 ++++++++++++++++ toolchains/node/BUILD.tpl | 25 +++++ toolchains/node/node_configure.bzl | 51 ++++++++++ toolchains/node/node_toolchain.bzl | 53 ++++++++++ 19 files changed, 541 insertions(+), 83 deletions(-) create mode 100644 internal/node/BUILD.nodejs_host_os_alias.tpl create mode 100644 internal/node/test/nodejs_toolchain_test.bzl create mode 100644 toolchains/node/BUILD.bazel create mode 100644 toolchains/node/BUILD.tpl create mode 100644 toolchains/node/node_configure.bzl create mode 100644 toolchains/node/node_toolchain.bzl diff --git a/BUILD.bazel b/BUILD.bazel index f317f88500..dc633d787c 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -19,6 +19,7 @@ bzl_library( "//internal/jasmine_node_test:bzl", "//internal/npm_package:bzl", "//internal/rollup:bzl", + "//toolchains/node:bzl", ], ) @@ -68,6 +69,7 @@ npm_package( "//internal/npm_install:package_contents", "//internal/npm_package:package_contents", "//internal/web_package:package_contents", + "//toolchains/node:package_contents", ], ) diff --git a/README.md b/README.md index 73385ce08c..d1214b536c 100644 --- a/README.md +++ b/README.md @@ -472,6 +472,24 @@ Note: the arguments passed to `bazel run` after `--` are forwarded to the execut [bazel instructions]: https://docs.bazel.build/versions/master/install.html +### Toolchains + +When you add `node_repositories()` to your `WORKSPACE` file it will setup a node toolchain for all currently supported platforms, Linux, macOS and Windows. Amongst other things this adds support for cross-compilations as well as Remote Build Execution support. For more detailed information also see [Bazel Toolchains](https://docs.bazel.build/versions/master/toolchains.html). + +If you have an advanced use-case you can also register your own toolchains and call `node_configure` directly to manually setup a toolchain. + +#### Cross-compilation + +Toolchains allow us to support cross-compilation, e.g. building a linux binary from mac or windows. To tell Bazel to provide a toolchain for a different platform you have to pass in the `--platforms` flag. Currently supported values are: + +- `@build_bazel_rules_nodejs//toolchains/node:linux_amd64` +- `@build_bazel_rules_nodejs//toolchains/node:darwin_amd64` +- `@build_bazel_rules_nodejs//toolchains/node:windows_amd64` + +So if for example you want to build a docker image from a non-linux platform you would run `bazel build --platforms=@build_bazel_rules_nodejs//toolchains/node:linux_amd64 //app`, which will ensure that the linux nodejs binary is downloaded and provided to the nodejs_binary target. + +Note: The toolchain currently only provides a platform-specific nodejs binary. Any native modules will still be fetched/built, by npm/yarn, for your host platform, so they will not work on the target platform. Support for cross-compilation with native dependencies will follow. + ## Usage ### Running a program from npm diff --git a/internal/common/os_name.bzl b/internal/common/os_name.bzl index 7b4229fb63..c0e7894517 100644 --- a/internal/common/os_name.bzl +++ b/internal/common/os_name.bzl @@ -15,6 +15,17 @@ """Helper function for repository rules """ +OS_ARCH_NAMES = [ + ("darwin", "amd64"), + ("windows", "amd64"), + ("linux", "amd64"), +] + +OS_NAMES = ["_".join(os_arch_name) for os_arch_name in OS_ARCH_NAMES] + +def is_windows(os_name): + return os_name == OS_NAMES[1] + def os_name(repository_ctx): """Get the os name for a repository rule @@ -26,10 +37,10 @@ def os_name(repository_ctx): """ os_name = repository_ctx.os.name.lower() if os_name.startswith("mac os"): - return "darwin_amd64" + return OS_NAMES[0] elif os_name.find("windows") != -1: - return "windows_amd64" + return OS_NAMES[1] elif os_name.startswith("linux"): - return "linux_amd64" + return OS_NAMES[2] else: fail("Unsupported operating system: " + os_name) diff --git a/internal/node/BUILD.bazel b/internal/node/BUILD.bazel index 026f5e2037..997c88b658 100644 --- a/internal/node/BUILD.bazel +++ b/internal/node/BUILD.bazel @@ -31,6 +31,7 @@ exports_files([ "node_repositories.bzl", # Exported to be consumed for generating skydoc. "node_launcher.sh", "node_loader.js", + "BUILD.nodejs_host_os_alias.tpl", ]) filegroup( @@ -39,6 +40,9 @@ filegroup( "*.bzl", "*.js", "*.sh", - ]) + ["BUILD.bazel"], + ]) + [ + "BUILD.bazel", + "BUILD.nodejs_host_os_alias.tpl", + ], visibility = ["//:__pkg__"], ) diff --git a/internal/node/BUILD.nodejs_host_os_alias.tpl b/internal/node/BUILD.nodejs_host_os_alias.tpl new file mode 100644 index 0000000000..11d8d70031 --- /dev/null +++ b/internal/node/BUILD.nodejs_host_os_alias.tpl @@ -0,0 +1,27 @@ +# Generated by node_repositories.bzl +package(default_visibility = ["//visibility:public"]) + +# aliases for exports_files +alias(name = "run_npm.sh.template", actual = "TEMPLATE_run_npm") +alias(name = "bin/node_repo_args.sh", actual = "TEMPLATE_node_repo_args") +# windows specific aliases +alias(name = "bin/nodejs/node.exe", actual = "TEMPLATE_actual_node_bin") +alias(name = "bin/node.cmd", actual = "TEMPLATE_wrapped_node_bin") +alias(name = "bin/npm.cmd", actual = "TEMPLATE_npm") +alias(name = "bin/npm_node_repositories.cmd", actual = "TEMPLATE__npm_node_repositories") +alias(name = "bin/yarn.cmd", actual = "TEMPLATE_yarn") +alias(name = "bin/yarn_node_repositories.cmd", actual = "TEMPLATE__yarn_node_repositories") +# linux/mac specific aliases +alias(name = "bin/nodejs/bin/node", actual = "TEMPLATE_actual_node_bin") +alias(name = "bin/node", actual = "TEMPLATE_wrapped_node_bin") +alias(name = "bin/npm", actual = "TEMPLATE_npm") +alias(name = "bin/npm_node_repositories", actual = "TEMPLATE__npm_node_repositories") +alias(name = "bin/yarn", actual = "TEMPLATE_yarn") +alias(name = "bin/yarn_node_repositories", actual = "TEMPLATE__yarn_node_repositories") + + +# aliases for other aliases +alias(name = "node_bin", actual = "TEMPLATE_actual_node_bin") +alias(name = "node", actual = "TEMPLATE_wrapped_node_bin") +alias(name = "npm", actual = "TEMPLATE__npm_node_repositories") +alias(name = "yarn", actual = "TEMPLATE__yarn_node_repositories") diff --git a/internal/node/generate_build_file.js b/internal/node/generate_build_file.js index b1e45b590c..8c51b62390 100644 --- a/internal/node/generate_build_file.js +++ b/internal/node/generate_build_file.js @@ -23,7 +23,7 @@ const fs = require('fs'); const path = require('path'); const IS_WINDOWS = TEMPLATED_is_windows; -const NODE_DIR = 'TEMPLATED_node_dir'; +const IS_VENDORED_NODE = TEMPLATED_vendored_node; const NODE_ACTUAL = 'TEMPLATED_node_actual'; const NODE_BIN_ACTUAL = 'TEMPLATED_node_bin_actual'; const NPM_ACTUAL = 'TEMPLATED_npm_actual'; @@ -65,12 +65,12 @@ module.exports = { main }; function generateBuildFile() { const binaryExt = IS_WINDOWS ? '.cmd' : ''; + const exportedNodeBin = IS_VENDORED_NODE ? '' : `\n "${NODE_BIN_ACTUAL}",`; const buildFile = `# Generated by node_repositories.bzl package(default_visibility = ["//visibility:public"]) exports_files([ "run_npm.sh.template", - "bin/node_repo_args.sh", - "${NODE_DIR}/bin/node", + "bin/node_repo_args.sh",${exportedNodeBin} "bin/node${binaryExt}", "bin/npm${binaryExt}", "bin/npm_node_repositories${binaryExt}", diff --git a/internal/node/node.bzl b/internal/node/node.bzl index a963dc1cfa..72bf3b0261 100644 --- a/internal/node/node.bzl +++ b/internal/node/node.bzl @@ -125,7 +125,6 @@ def _short_path_to_manifest_path(ctx, short_path): return ctx.workspace_name + "/" + short_path def _nodejs_binary_impl(ctx): - node = ctx.file.node node_modules = depset(ctx.files.node_modules) # Also include files from npm fine grained deps as inputs. @@ -164,25 +163,38 @@ def _nodejs_binary_impl(ctx): if hasattr(ctx.attr, "expected_exit_code"): expected_exit_code = ctx.attr.expected_exit_code - substitutions = { - "TEMPLATED_args": " ".join([ - expand_location_into_runfiles(ctx, a) - for a in ctx.attr.templated_args - ]), - "TEMPLATED_env_vars": env_vars, - "TEMPLATED_expected_exit_code": str(expected_exit_code), - "TEMPLATED_node": _short_path_to_manifest_path(ctx, node.short_path), - "TEMPLATED_repository_args": _short_path_to_manifest_path(ctx, ctx.file._repository_args.short_path), - "TEMPLATED_script_path": script_path, - } - ctx.actions.expand_template( - template = ctx.file._launcher_template, - output = ctx.outputs.script, - substitutions = substitutions, - is_executable = True, - ) + node_tool_info = ctx.toolchains["@build_bazel_rules_nodejs//toolchains/node:toolchain_type"].nodeinfo + node_tool_files = [] + if node_tool_info.target_tool_path == "" and not node_tool_info.target_tool: + # If tool_path is empty and tool_target is None then there is no local + # node tool, we will just print a nice error message if the user + # attempts to do bazel run + fail("The node toolchain was not properly configured so %s cannot be executed. Make sure that target_tool_path or target_tool is set." % ctx.attr.name) + else: + node_tool = node_tool_info.target_tool_path + if node_tool_info.target_tool: + node_tool_files += node_tool_info.target_tool.files.to_list() + node_tool = _short_path_to_manifest_path(ctx, node_tool_files[0].short_path) - runfiles = depset([node, ctx.outputs.loader, ctx.file._repository_args], transitive = [sources, node_modules]) + substitutions = { + "TEMPLATED_args": " ".join([ + expand_location_into_runfiles(ctx, a) + for a in ctx.attr.templated_args + ]), + "TEMPLATED_env_vars": env_vars, + "TEMPLATED_expected_exit_code": str(expected_exit_code), + "TEMPLATED_node": node_tool, + "TEMPLATED_repository_args": _short_path_to_manifest_path(ctx, ctx.file._repository_args.short_path), + "TEMPLATED_script_path": script_path, + } + ctx.actions.expand_template( + template = ctx.file._launcher_template, + output = ctx.outputs.script, + substitutions = substitutions, + is_executable = True, + ) + + runfiles = depset(node_tool_files + [ctx.outputs.loader, ctx.file._repository_args], transitive = [sources, node_modules]) # entry point is only needed in runfiles if it is a .js file if ctx.file.entry_point.extension == "js": @@ -192,8 +204,7 @@ def _nodejs_binary_impl(ctx): executable = ctx.outputs.script, runfiles = ctx.runfiles( transitive_files = runfiles, - files = [ - node, + files = node_tool_files + [ ctx.outputs.loader, ] + ctx.files._source_map_support_files + @@ -292,11 +303,6 @@ _NODEJS_EXECUTABLE_ATTRS = { in TypeScript.""", default = True, ), - "node": attr.label( - doc = """The node entry point target.""", - default = Label("@nodejs//:node_bin"), - allow_single_file = True, - ), "node_modules": attr.label( doc = """The npm packages which should be available to `require()` during execution. @@ -406,6 +412,7 @@ nodejs_binary = rule( attrs = _NODEJS_EXECUTABLE_ATTRS, executable = True, outputs = _NODEJS_EXECUTABLE_OUTPUTS, + toolchains = ["@build_bazel_rules_nodejs//toolchains/node:toolchain_type"], ) """Runs some JavaScript code in NodeJS. """ @@ -420,6 +427,7 @@ nodejs_test = rule( }), test = True, outputs = _NODEJS_EXECUTABLE_OUTPUTS, + toolchains = ["@build_bazel_rules_nodejs//toolchains/node:toolchain_type"], ) """ Identical to `nodejs_binary`, except this can be used with `bazel test` as well. diff --git a/internal/node/node_labels.bzl b/internal/node/node_labels.bzl index 8a8d9e9cf6..92b99d7cf0 100644 --- a/internal/node/node_labels.bzl +++ b/internal/node/node_labels.bzl @@ -17,37 +17,37 @@ Labels are different on windows and linux/OSX. """ -def get_node_label(repository_ctx): - if repository_ctx.os.name.lower().find("windows") != -1: - label = Label("@nodejs//:bin/node.cmd") +def get_node_label(os_name): + if os_name.find("windows") != -1: + label = Label("@nodejs_%s//:bin/node.cmd" % os_name) else: - label = Label("@nodejs//:bin/node") + label = Label("@nodejs_%s//:bin/node" % os_name) return label -def get_npm_label(repository_ctx): - if repository_ctx.os.name.lower().find("windows") != -1: - label = Label("@nodejs//:bin/npm.cmd") +def get_npm_label(os_name): + if os_name.find("windows") != -1: + label = Label("@nodejs_%s//:bin/npm.cmd" % os_name) else: - label = Label("@nodejs//:bin/npm") + label = Label("@nodejs_%s//:bin/npm" % os_name) return label -def get_npm_node_repositories_label(repository_ctx): - if repository_ctx.os.name.lower().find("windows") != -1: - label = Label("@nodejs//:bin/npm_node_repositories.cmd") +def get_npm_node_repositories_label(os_name): + if os_name.find("windows") != -1: + label = Label("@nodejs_%s//:bin/npm_node_repositories.cmd" % os_name) else: - label = Label("@nodejs//:bin/npm_node_repositories") + label = Label("@nodejs_%s//:bin/npm_node_repositories" % os_name) return label -def get_yarn_label(repository_ctx): - if repository_ctx.os.name.lower().find("windows") != -1: - label = Label("@nodejs//:bin/yarn.cmd") +def get_yarn_label(os_name): + if os_name.find("windows") != -1: + label = Label("@nodejs_%s//:bin/yarn.cmd" % os_name) else: - label = Label("@nodejs//:bin/yarn") + label = Label("@nodejs_%s//:bin/yarn" % os_name) return label -def get_yarn_node_repositories_label(repository_ctx): - if repository_ctx.os.name.lower().find("windows") != -1: - label = Label("@nodejs//:bin/yarn_node_repositories.cmd") +def get_yarn_node_repositories_label(os_name): + if os_name.find("windows") != -1: + label = Label("@nodejs%s//:bin/yarn_node_repositories.cmd" % os_name) else: - label = Label("@nodejs//:bin/yarn_node_repositories") + label = Label("@nodejs_%s//:bin/yarn_node_repositories" % os_name) return label diff --git a/internal/node/node_repositories.bzl b/internal/node/node_repositories.bzl index adde6f673d..da5eedbbf2 100644 --- a/internal/node/node_repositories.bzl +++ b/internal/node/node_repositories.bzl @@ -20,9 +20,10 @@ See https://docs.bazel.build/versions/master/skylark/repository_rules.html load("//internal/common:check_bazel_version.bzl", "check_bazel_version") load("//internal/common:check_version.bzl", "check_version") -load("//internal/common:os_name.bzl", "os_name") +load("//internal/common:os_name.bzl", "OS_ARCH_NAMES", "is_windows", "os_name") load("//internal/npm_install:npm_install.bzl", "yarn_install") load("//third_party/github.com/bazelbuild/bazel-skylib:lib/paths.bzl", "paths") +load("//toolchains/node:node_configure.bzl", node_toolchain_configure = "node_configure") load(":node_labels.bzl", "get_yarn_node_repositories_label") # Callers that don't specify a particular version will get these. @@ -134,8 +135,10 @@ def _download_node(repository_ctx): """ if repository_ctx.attr.vendored_node: return - - host = os_name(repository_ctx) + if repository_ctx.name == "nodejs": + host = os_name(repository_ctx) + else: + host = repository_ctx.name.split("nodejs_", 1)[1] node_version = repository_ctx.attr.node_version node_repositories = repository_ctx.attr.node_repositories node_urls = repository_ctx.attr.node_urls @@ -192,7 +195,11 @@ def _prepare_node(repository_ctx): Args: repository_ctx: The repository rule context """ - is_windows = os_name(repository_ctx).find("windows") != -1 + is_windows_os = os_name(repository_ctx).find("windows") != -1 + + # TODO: Maybe we want to encode the OS as a specific attribute rather than do it based on naming? + is_windows_repository = repository_ctx.attr.name.find("windows") != -1 + is_windows = is_windows_os or is_windows_repository if repository_ctx.attr.vendored_node: node_exec = "/".join([f for f in [ "../../..", @@ -440,12 +447,27 @@ if %errorlevel% neq 0 exit /b %errorlevel% "TEMPLATED_is_windows": "true" if is_windows else "false", "TEMPLATED_node_actual": node_entry, "TEMPLATED_node_bin_actual": node_exec_label, - "TEMPLATED_node_dir": NODE_DIR, "TEMPLATED_npm_actual": npm_node_repositories_entry, + "TEMPLATED_vendored_node": "true" if repository_ctx.attr.vendored_node else "false", "TEMPLATED_yarn_actual": yarn_node_repositories_entry, }, ) - result = repository_ctx.execute([node_entry, "generate_build_file.js"]) + host_os = os_name(repository_ctx) + if ("_%s" % host_os) in repository_ctx.attr.name or repository_ctx.attr.name == "nodejs": + # We have to use the relative path here otherwise bazel reports a cycle + result = repository_ctx.execute([node_entry, "generate_build_file.js"]) + else: + # NOTE: Ideally we would not need this logic here and could just depend on the @nodejs//:node_bin alias but it is not possible. + # See: https://github.com/bazelbuild/bazel/issues/8674 + # We have to make sure that we run repository_ctx.execute with the right node executable, so if e.g. we are in the repository containing + # the linux executable but on windows we need to ensure that we use the executable for windows. + node_path = "node.exe" if is_windows_os else "bin/node" + + # NOTE: If no vendored node is provided we just assume that there exists a nodejs external repository + node_label = Label(node_exec_label) if repository_ctx.attr.vendored_node else Label("@nodejs_%s//:bin/nodejs/%s" % (host_os, node_path)) + host_node = repository_ctx.path(node_label) + result = repository_ctx.execute([host_node, "generate_build_file.js"]) + if result.return_code: fail("generate_build_file.js failed: \nSTDOUT:\n%s\nSTDERR:\n%s" % (result.stdout, result.stderr)) @@ -483,6 +505,31 @@ _yarn_repo = repository_rule( attrs = {"package_json": attr.label_list()}, ) +def _nodejs_host_os_alias_impl(repository_ctx): + host_os = os_name(repository_ctx) + node_repository = "@nodejs_%s" % host_os + file_ending = ".cmd" if is_windows(host_os) else "" + actual_node_bin = "bin/nodejs/node.exe" if is_windows(host_os) else "bin/nodejs/bin/node" + repository_ctx.template( + "BUILD.bazel", + Label("@build_bazel_rules_nodejs//internal/node:BUILD.nodejs_host_os_alias.tpl"), + substitutions = { + "TEMPLATE__npm_node_repositories": "%s//:bin/npm_node_repositories%s" % (node_repository, file_ending), + "TEMPLATE__yarn_node_repositories": "%s//:bin/yarn_node_repositories%s" % (node_repository, file_ending), + "TEMPLATE_actual_node_bin": "%s//:%s" % (node_repository, actual_node_bin), + "TEMPLATE_node_repo_args": "%s//:bin/node_repo_args.sh" % node_repository, + "TEMPLATE_npm": "%s//:bin/npm%s" % (node_repository, file_ending), + "TEMPLATE_run_npm": "%s//:run_npm.sh.template" % node_repository, + "TEMPLATE_wrapped_node_bin": "%s//:bin/node%s" % (node_repository, file_ending), + "TEMPLATE_yarn": "%s//:bin/yarn%s" % (node_repository, file_ending), + }, + executable = False, + ) + +_nodejs_repo_host_os_alias = repository_rule( + _nodejs_host_os_alias_impl, +) + def node_repositories( package_json = [], node_version = DEFAULT_NODE_VERSION, @@ -571,19 +618,35 @@ def node_repositories( minimum_bazel_version = "0.21.0", ) + # This needs to be setup so toolchains can access nodejs for all different versions + for os_arch_name in OS_ARCH_NAMES: + os_name = "_".join(os_arch_name) + node_repository_name = "nodejs_%s" % os_name + _maybe( + _nodejs_repo, + name = node_repository_name, + package_json = package_json, + node_version = node_version, + yarn_version = yarn_version, + vendored_node = vendored_node, + vendored_yarn = vendored_yarn, + node_repositories = node_repositories, + yarn_repositories = yarn_repositories, + node_urls = node_urls, + yarn_urls = yarn_urls, + preserve_symlinks = preserve_symlinks, + ) + native.register_toolchains("@build_bazel_rules_nodejs//toolchains/node:node_%s_toolchain" % os_arch_name[0]) + node_toolchain_configure( + name = "%s_config" % node_repository_name, + target_tool = "@%s//:node_bin" % node_repository_name, + ) + + # This "nodejs" repo is just for convinience so one does not have to target @nodejs_//... + # All it does is create aliases to the @nodejs__ repository _maybe( - _nodejs_repo, + _nodejs_repo_host_os_alias, name = "nodejs", - package_json = package_json, - node_version = node_version, - yarn_version = yarn_version, - vendored_node = vendored_node, - vendored_yarn = vendored_yarn, - node_repositories = node_repositories, - yarn_repositories = yarn_repositories, - node_urls = node_urls, - yarn_urls = yarn_urls, - preserve_symlinks = preserve_symlinks, ) _maybe( diff --git a/internal/node/test/BUILD.bazel b/internal/node/test/BUILD.bazel index 6174270b7f..286b7aacfb 100644 --- a/internal/node/test/BUILD.bazel +++ b/internal/node/test/BUILD.bazel @@ -1,5 +1,8 @@ load("//:defs.bzl", "nodejs_binary") load("//internal/js_library:js_library.bzl", "js_library") +load(":nodejs_toolchain_test.bzl", "nodejs_binary_test_suite") + +nodejs_binary_test_suite() # You can have a nodejs_binary with no node_modules attribute # and no fine grained deps diff --git a/internal/node/test/nodejs_toolchain_test.bzl b/internal/node/test/nodejs_toolchain_test.bzl new file mode 100644 index 0000000000..ef78783061 --- /dev/null +++ b/internal/node/test/nodejs_toolchain_test.bzl @@ -0,0 +1,101 @@ +"Unit tests for node.bzl toolchain support" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") + +def _runfiles_contents_test_impl(ctx): + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + # check target's runfiles + runfiles = sorted(target_under_test[DefaultInfo].default_runfiles.files.to_list()) + asserts.true(env, ctx.file.node_selected in runfiles) + asserts.false(env, ctx.files.node_other[0] in runfiles) + asserts.false(env, ctx.files.node_other[1] in runfiles) + + # This is a bit of a hack, but because "@nodejs//:node_bin" is just an alias to one of the other nodejs repositories + # bazel automatically filters it from the list if the aliased label already exists. + # So we have to check that it has not been filtered out and then we do expect it to be in runfiles, as it just points + # to the same file as "node_selected" + if len(ctx.files.node_other) == 3: + asserts.true(env, ctx.files.node_other[2] in runfiles) + + return analysistest.end(env) + +linux_platform_toolchain_test = analysistest.make( + _runfiles_contents_test_impl, + config_settings = { + "//command_line_option:platforms": "@build_bazel_rules_nodejs//toolchains/node:linux_amd64", + }, + attrs = { + "node_other": attr.label_list( + default = [Label("@nodejs_windows_amd64//:node_bin"), Label("@nodejs_darwin_amd64//:node_bin"), Label("@nodejs//:node_bin")], + allow_files = True, + ), + "node_selected": attr.label( + default = Label("@nodejs_linux_amd64//:node_bin"), + allow_single_file = True, + ), + }, +) + +windows_platform_toolchain_test = analysistest.make( + _runfiles_contents_test_impl, + config_settings = { + "//command_line_option:platforms": "@build_bazel_rules_nodejs//toolchains/node:windows_amd64", + }, + attrs = { + "node_other": attr.label_list( + default = [Label("@nodejs_linux_amd64//:node_bin"), Label("@nodejs_darwin_amd64//:node_bin"), Label("@nodejs//:node_bin")], + allow_files = True, + ), + "node_selected": attr.label( + default = Label("@nodejs_windows_amd64//:node_bin"), + allow_single_file = True, + ), + }, +) + +darwin_platform_toolchain_test = analysistest.make( + _runfiles_contents_test_impl, + config_settings = { + "//command_line_option:platforms": "@build_bazel_rules_nodejs//toolchains/node:darwin_amd64", + }, + attrs = { + "node_other": attr.label_list( + default = [Label("@nodejs_windows_amd64//:node_bin"), Label("@nodejs_linux_amd64//:node_bin"), Label("@nodejs//:node_bin")], + allow_files = True, + ), + "node_selected": attr.label( + default = Label("@nodejs_darwin_amd64//:node_bin"), + allow_single_file = True, + ), + }, +) + +def test_runfiles_contents(): + linux_platform_toolchain_test( + name = "linux_platform_toolchain_test", + target_under_test = ":no_deps", + ) + + windows_platform_toolchain_test( + name = "windows_platform_toolchain_test", + target_under_test = ":no_deps", + ) + + darwin_platform_toolchain_test( + name = "darwin_platform_toolchain_test", + target_under_test = ":no_deps", + ) + +def nodejs_binary_test_suite(): + test_runfiles_contents() + + native.test_suite( + name = "nodejs_toolchain_test", + tests = [ + ":linux_platform_toolchain_test", + ":windows_platform_toolchain_test", + ":darwin_platform_toolchain_test", + ], + ) diff --git a/internal/npm_install/npm_install.bzl b/internal/npm_install/npm_install.bzl index 2bf4e32f33..9092140417 100644 --- a/internal/npm_install/npm_install.bzl +++ b/internal/npm_install/npm_install.bzl @@ -203,9 +203,10 @@ def _npm_install_impl(repository_ctx): _check_min_bazel_version("npm_install", repository_ctx) - is_windows = os_name(repository_ctx).find("windows") != -1 - node = repository_ctx.path(get_node_label(repository_ctx)) - npm = get_npm_label(repository_ctx) + os = os_name(repository_ctx) + is_windows = os.find("windows") != -1 + node = repository_ctx.path(get_node_label(os)) + npm = get_npm_label(os) npm_args = ["install"] if repository_ctx.attr.prod_only: @@ -315,8 +316,9 @@ def _yarn_install_impl(repository_ctx): _check_min_bazel_version("yarn_install", repository_ctx) - node = repository_ctx.path(get_node_label(repository_ctx)) - yarn = get_yarn_label(repository_ctx) + os = os_name(repository_ctx) + node = repository_ctx.path(get_node_label(os)) + yarn = get_yarn_label(os) # If symlink_node_modules is true then run the package manager # in the package.json folder; otherwise, run it in the root of @@ -370,7 +372,6 @@ def _yarn_install_impl(repository_ctx): timeout = repository_ctx.attr.timeout, quiet = repository_ctx.attr.quiet, ) - if result.return_code: fail("yarn_install failed: %s (%s)" % (result.stdout, result.stderr)) diff --git a/internal/npm_package/npm_package.bzl b/internal/npm_package/npm_package.bzl index 7b19296b2f..be208ecf4c 100644 --- a/internal/npm_package/npm_package.bzl +++ b/internal/npm_package/npm_package.bzl @@ -57,6 +57,10 @@ def create_package(ctx, deps_sources, nested_packages): args.add_joined(ctx.attr.vendor_external, join_with = ",", omit_if_empty = False) args.add("1" if ctx.attr.rename_build_files else "0") + # require.resolve expects the path to start with the workspace name and not "external" + run_npm_template_path = ctx.file._run_npm_template.path[len("external") + 1:] if ctx.file._run_npm_template.path.startswith("external") else ctx.file._run_npm_template.path + args.add(run_npm_template_path) + inputs = ctx.files.srcs + deps_sources + nested_packages + [ctx.file._run_npm_template] # The version_file is an undocumented attribute of the ctx that lets us read the volatile-status.txt file diff --git a/internal/npm_package/packager.js b/internal/npm_package/packager.js index 89513de0c2..d988d187ad 100644 --- a/internal/npm_package/packager.js +++ b/internal/npm_package/packager.js @@ -60,7 +60,8 @@ function main(args) { args = fs.readFileSync(args[0], {encoding: 'utf-8'}).split('\n').map(unquoteArgs); const [outDir, baseDir, srcsArg, binDir, genDir, depsArg, packagesArg, replacementsArg, packPath, - publishPath, replaceWithVersion, stampFile, vendorExternalArg, renameBuildFilesArg] = args; + publishPath, replaceWithVersion, stampFile, vendorExternalArg, renameBuildFilesArg, + runNpmTemplatePath] = args; const renameBuildFiles = parseInt(renameBuildFilesArg); const replacements = [ @@ -169,8 +170,7 @@ function main(args) { }); } - const npmTemplate = - fs.readFileSync(require.resolve('nodejs/run_npm.sh.template'), {encoding: 'utf-8'}); + const npmTemplate = fs.readFileSync(require.resolve(runNpmTemplatePath), {encoding: 'utf-8'}); // Resolve the outDir to an absolute path so it doesn't depend on Bazel's bazel-out symlink fs.writeFileSync(packPath, npmTemplate.replace('TMPL_args', `pack "${path.resolve(outDir)}"`)); fs.writeFileSync(publishPath, npmTemplate.replace('TMPL_args', `publish "${path.resolve(outDir)}"`)); diff --git a/package.bzl b/package.bzl index d68974b064..ba820150e7 100644 --- a/package.bzl +++ b/package.bzl @@ -74,12 +74,12 @@ def rules_nodejs_dev_dependencies(): url = "https://github.com/bazelbuild/skydoc/archive/0.3.0.tar.gz", ) - # bazel-skylib 0.8.0 released 2019.03.20 (https://github.com/bazelbuild/bazel-skylib/releases/tag/0.8.0) + # bazel-skylib master 2019.05.03 to get support for https://github.com/bazelbuild/bazel-skylib/pull/140 http_archive( name = "bazel_skylib", - type = "tar.gz", - url = "https://github.com/bazelbuild/bazel-skylib/releases/download/0.8.0/bazel-skylib.0.8.0.tar.gz", - sha256 = "2ef429f5d7ce7111263289644d233707dba35e39696377ebab8b0bc701f7818e", + sha256 = "afbe4d9d033c007940acd24bb9becf1580a0280ae0b2ebbb5a7cb12912d2c115", + strip_prefix = "bazel-skylib-ffad33e9bfc60bdfa98292ca655a4e7035792046", + urls = ["https://github.com/bazelbuild/bazel-skylib/archive/ffad33e9bfc60bdfa98292ca655a4e7035792046.tar.gz"], ) # Needed for Remote Build Execution diff --git a/toolchains/node/BUILD.bazel b/toolchains/node/BUILD.bazel new file mode 100644 index 0000000000..b7cc85f176 --- /dev/null +++ b/toolchains/node/BUILD.bazel @@ -0,0 +1,87 @@ +# Copyright 2018 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. +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +package(default_visibility = ["//visibility:private"]) + +licenses(["notice"]) # Apache 2.0 + +platform( + name = "darwin_amd64", + constraint_values = [ + "@bazel_tools//platforms:osx", + "@bazel_tools//platforms:x86_64", + ], +) + +platform( + name = "linux_amd64", + constraint_values = [ + "@bazel_tools//platforms:linux", + "@bazel_tools//platforms:x86_64", + ], +) + +platform( + name = "windows_amd64", + constraint_values = [ + "@bazel_tools//platforms:windows", + "@bazel_tools//platforms:x86_64", + ], +) + +bzl_library( + name = "bzl", + srcs = glob(["*.bzl"]), + visibility = ["//visibility:public"], +) + +filegroup( + name = "package_contents", + srcs = glob(["*"]), + visibility = ["//:__pkg__"], +) + +# node toolchain type +toolchain_type(name = "toolchain_type") + +toolchain( + name = "node_linux_toolchain", + target_compatible_with = [ + "@bazel_tools//platforms:linux", + "@bazel_tools//platforms:x86_64", + ], + toolchain = "@nodejs_linux_amd64_config//:toolchain", + toolchain_type = ":toolchain_type", +) + +toolchain( + name = "node_darwin_toolchain", + target_compatible_with = [ + "@bazel_tools//platforms:osx", + "@bazel_tools//platforms:x86_64", + ], + toolchain = "@nodejs_darwin_amd64_config//:toolchain", + toolchain_type = ":toolchain_type", +) + +toolchain( + name = "node_windows_toolchain", + target_compatible_with = [ + "@bazel_tools//platforms:windows", + "@bazel_tools//platforms:x86_64", + ], + toolchain = "@nodejs_windows_amd64_config//:toolchain", + toolchain_type = ":toolchain_type", +) diff --git a/toolchains/node/BUILD.tpl b/toolchains/node/BUILD.tpl new file mode 100644 index 0000000000..4c3fb3c47d --- /dev/null +++ b/toolchains/node/BUILD.tpl @@ -0,0 +1,25 @@ +# Copyright 2018 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. +""" +This BUILD file is auto-generated from toolchains/node/BUILD.tpl +""" + +package(default_visibility = ["//visibility:public"]) + +load("@build_bazel_rules_nodejs//toolchains/node:node_toolchain.bzl", "node_toolchain") + +node_toolchain( + name = "toolchain", +%{TOOL_ATTRS} +) diff --git a/toolchains/node/node_configure.bzl b/toolchains/node/node_configure.bzl new file mode 100644 index 0000000000..e702cfee1b --- /dev/null +++ b/toolchains/node/node_configure.bzl @@ -0,0 +1,51 @@ +# Copyright 2018 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. +""" +Defines a repository rule for configuring the node binary. +""" + +def _impl(repository_ctx): + if repository_ctx.attr.target_tool and repository_ctx.attr.target_tool_path: + fail("Can only set one of target_tool or target_tool_path but both where set.") + + if repository_ctx.attr.target_tool: + substitutions = {"%{TOOL_ATTRS}": " target_tool = \"%s\"\n" % repository_ctx.attr.target_tool} + else: + default_tool_path = repository_ctx.attr.target_tool_path or repository_ctx.which("node") or "" + substitutions = {"%{TOOL_ATTRS}": " target_tool_path = \"%s\"\n" % default_tool_path} + + repository_ctx.template( + "BUILD", + Label("@build_bazel_rules_nodejs//toolchains/node:BUILD.tpl"), + substitutions, + False, + ) + +node_configure = repository_rule( + implementation = _impl, + attrs = { + "target_tool": attr.label( + doc = "Target for a downloaded nodejs binary for the target os.", + mandatory = False, + allow_single_file = True, + ), + "target_tool_path": attr.string( + doc = "Absolute path to a pre-installed nodejs binary for the target os.", + mandatory = False, + ), + }, +) +""" +Creates an external repository with a node_toolchain //:toolchain target properly configured. +""" diff --git a/toolchains/node/node_toolchain.bzl b/toolchains/node/node_toolchain.bzl new file mode 100644 index 0000000000..098267dcd9 --- /dev/null +++ b/toolchains/node/node_toolchain.bzl @@ -0,0 +1,53 @@ +# Copyright 2018 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. +""" +This module implements the node toolchain rule. +""" + +NodeInfo = provider( + doc = "Information about how to invoke the node binary.", + fields = { + "target_tool": "A hermetically downloaded nodejs executable target for the target platform.", + "target_tool_path": "Path to an existing nodejs executable for the target platform..", + }, +) + +def _node_toolchain_impl(ctx): + if ctx.attr.target_tool and ctx.attr.target_tool_path: + fail("Can only set one of target_tool or target_tool_path but both where set.") + if not ctx.attr.target_tool and not ctx.attr.target_tool_path: + print("No nodejs binary was found or built, executing run for rules_nodejs targets might not work.") + + toolchain_info = platform_common.ToolchainInfo( + nodeinfo = NodeInfo( + target_tool_path = ctx.attr.target_tool_path, + target_tool = ctx.attr.target_tool, + ), + ) + return [toolchain_info] + +node_toolchain = rule( + implementation = _node_toolchain_impl, + attrs = { + "target_tool": attr.label( + doc = "A hermetically downloaded nodejs executable target for the target platform..", + mandatory = False, + allow_single_file = True, + ), + "target_tool_path": attr.string( + doc = "Path to an existing nodejs executable for the target platform..", + mandatory = False, + ), + }, +)