| # Copyright 2017 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. |
| |
| """Executing programs |
| |
| These rules run the node executable with the given sources. |
| |
| They support module mapping: any targets in the transitive dependencies with |
| a `module_name` attribute can be `require`d by that name. |
| """ |
| |
| load("//:providers.bzl", "JSNamedModuleInfo", "NodeRuntimeDepsInfo", "NpmPackageInfo", "node_modules_aspect") |
| load("//internal/common:expand_into_runfiles.bzl", "expand_location_into_runfiles") |
| load("//internal/common:module_mappings.bzl", "module_mappings_runtime_aspect") |
| load("//internal/common:path_utils.bzl", "strip_external") |
| load("//internal/common:windows_utils.bzl", "create_windows_native_launcher_script", "is_windows") |
| load("//internal/linker:link_node_modules.bzl", "write_node_modules_manifest") |
| load("//internal/node:node_repositories.bzl", "BUILT_IN_NODE_PLATFORMS") |
| |
| def _trim_package_node_modules(package_name): |
| # trim a package name down to its path prior to a node_modules |
| # segment. 'foo/node_modules/bar' would become 'foo' and |
| # 'node_modules/bar' would become '' |
| segments = [] |
| for n in package_name.split("/"): |
| if n == "node_modules": |
| break |
| segments += [n] |
| return "/".join(segments) |
| |
| def _compute_node_modules_root(ctx): |
| """Computes the node_modules root from the node_modules and deps attributes. |
| |
| Args: |
| ctx: the skylark execution context |
| |
| Returns: |
| The node_modules root as a string |
| """ |
| node_modules_root = None |
| if ctx.attr.node_modules: |
| if NpmPackageInfo in ctx.attr.node_modules: |
| node_modules_root = "/".join([ctx.attr.node_modules[NpmPackageInfo].workspace, "node_modules"]) |
| elif ctx.files.node_modules: |
| # ctx.files.node_modules is not an empty list |
| workspace = ctx.attr.node_modules.label.workspace_root.split("/")[1] if ctx.attr.node_modules.label.workspace_root else ctx.workspace_name |
| node_modules_root = "/".join([f for f in [ |
| workspace, |
| _trim_package_node_modules(ctx.attr.node_modules.label.package), |
| "node_modules", |
| ] if f]) |
| for d in ctx.attr.data: |
| if NpmPackageInfo in d: |
| possible_root = "/".join([d[NpmPackageInfo].workspace, "node_modules"]) |
| if not node_modules_root: |
| node_modules_root = possible_root |
| elif node_modules_root != possible_root: |
| fail("All npm dependencies need to come from a single workspace. Found '%s' and '%s'." % (node_modules_root, possible_root)) |
| if not node_modules_root: |
| # there are no fine grained deps and the node_modules attribute is an empty filegroup |
| # but we still need a node_modules_root even if its empty |
| workspace = ctx.attr.node_modules.label.workspace_root.split("/")[1] if ctx.attr.node_modules.label.workspace_root else ctx.workspace_name |
| node_modules_root = "/".join([f for f in [ |
| workspace, |
| ctx.attr.node_modules.label.package, |
| "node_modules", |
| ] if f]) |
| return node_modules_root |
| |
| def _write_loader_script(ctx): |
| # Generates the JavaScript snippet of module roots mappings, with each entry |
| # in the form: |
| # {module_name: /^mod_name\b/, module_root: 'path/to/mod_name'} |
| module_mappings = [] |
| for d in ctx.attr.data: |
| if hasattr(d, "runfiles_module_mappings"): |
| for [mn, mr] in d.runfiles_module_mappings.items(): |
| escaped = mn.replace("/", "\/").replace(".", "\.") |
| mapping = "{module_name: /^%s\\b/, module_root: '%s'}" % (escaped, mr) |
| module_mappings.append(mapping) |
| |
| node_modules_root = _compute_node_modules_root(ctx) |
| |
| if len(ctx.attr.entry_point.files.to_list()) != 1: |
| fail("labels in entry_point must contain exactly one file") |
| |
| entry_point_path = _to_manifest_path(ctx, ctx.file.entry_point) |
| |
| # If the entry point specified is a typescript file then set the entry |
| # point to the corresponding .js file |
| if entry_point_path.endswith(".ts"): |
| entry_point_path = entry_point_path[:-3] + ".js" |
| elif entry_point_path.endswith(".tsx"): |
| entry_point_path = entry_point_path[:-4] + ".jsx" |
| |
| ctx.actions.expand_template( |
| template = ctx.file._loader_template, |
| output = ctx.outputs.loader, |
| substitutions = { |
| "TEMPLATED_bin_dir": ctx.bin_dir.path, |
| "TEMPLATED_entry_point": entry_point_path, |
| "TEMPLATED_gen_dir": ctx.genfiles_dir.path, |
| "TEMPLATED_install_source_map_support": str(ctx.attr.install_source_map_support).lower(), |
| "TEMPLATED_module_roots": "\n " + ",\n ".join(module_mappings), |
| "TEMPLATED_node_modules_root": node_modules_root, |
| "TEMPLATED_target": str(ctx.label), |
| "TEMPLATED_user_workspace_name": ctx.workspace_name, |
| }, |
| is_executable = True, |
| ) |
| |
| # Avoid using non-normalized paths (workspace/../other_workspace/path) |
| def _to_manifest_path(ctx, file): |
| if file.short_path.startswith("../"): |
| return file.short_path[3:] |
| else: |
| return ctx.workspace_name + "/" + file.short_path |
| |
| def _to_execroot_path(ctx, file): |
| parts = file.path.split("/") |
| |
| if parts[0] == "external": |
| if parts[2] == "node_modules": |
| # external/npm/node_modules -> node_modules/foo |
| # the linker will make sure we can resolve node_modules from npm |
| return "/".join(parts[2:]) |
| return file.path |
| |
| def _nodejs_binary_impl(ctx): |
| node_modules_manifest = write_node_modules_manifest(ctx) |
| node_modules_depsets = [] |
| node_modules_depsets.append(depset(ctx.files.node_modules)) |
| |
| # Also include files from npm fine grained deps as inputs. |
| # These deps are identified by the NpmPackageInfo provider. |
| for d in ctx.attr.data: |
| if NpmPackageInfo in d: |
| node_modules_depsets.append(d[NpmPackageInfo].sources) |
| |
| node_modules = depset(transitive = node_modules_depsets) |
| |
| # Using an array of depsets will allow us to avoid flattening files and sources |
| # inside this loop. This should reduce the performances hits, |
| # since we don't need to call .to_list() |
| # Also avoid deap transitive depset()s by creating single array of |
| # transitive depset()s |
| sources_depsets = [] |
| |
| for d in ctx.attr.data: |
| # TODO: switch to JSModuleInfo when it is available |
| if JSNamedModuleInfo in d: |
| sources_depsets.append(d[JSNamedModuleInfo].sources) |
| if hasattr(d, "files"): |
| sources_depsets.append(d.files) |
| sources = depset(transitive = sources_depsets) |
| |
| _write_loader_script(ctx) |
| |
| script_path = _to_manifest_path(ctx, ctx.outputs.loader) |
| |
| env_vars = "export BAZEL_TARGET=%s\n" % ctx.label |
| for k in ctx.attr.configuration_env_vars + ctx.attr.default_env_vars: |
| if k in ctx.var.keys(): |
| env_vars += "export %s=\"%s\"\n" % (k, ctx.var[k]) |
| |
| if "DEBUG" in ctx.var and ctx.var["COMPILATION_MODE"] != "dbg": |
| print(""" |
| WARNING: `--define DEBUG` no longer triggers a debugging build, use |
| `--compilation_mode=dbg` instead. |
| |
| """) |
| |
| expected_exit_code = 0 |
| if hasattr(ctx.attr, "expected_exit_code"): |
| expected_exit_code = ctx.attr.expected_exit_code |
| |
| # Add both the node executable for the user's local machine which is in ctx.files._node and comes |
| # from @nodejs//:node_bin and the node executable from the selected node --platform which comes from |
| # ctx.toolchains["@build_bazel_rules_nodejs//toolchains/node:toolchain_type"].nodeinfo. |
| # In most cases these are the same files but for RBE and when explitely setting --platform for cross-compilation |
| # any given nodejs_binary should be able to run on both the user's local machine and on the RBE or selected |
| # platform. |
| # |
| # Rules such as nodejs_image should use only ctx.toolchains["@build_bazel_rules_nodejs//toolchains/node:toolchain_type"].nodeinfo |
| # when building the image as that will reflect the selected --platform. |
| node_tool_files = ctx.files._node[:] |
| node_tool_files.extend(ctx.toolchains["@build_bazel_rules_nodejs//toolchains/node:toolchain_type"].nodeinfo.tool_files) |
| |
| node_tool_files.append(ctx.file._link_modules_script) |
| node_tool_files.append(ctx.file._bazel_require_script) |
| node_tool_files.append(node_modules_manifest) |
| |
| is_builtin = ctx.attr._node.label.workspace_name in ["nodejs_%s" % p for p in BUILT_IN_NODE_PLATFORMS] |
| |
| substitutions = { |
| "TEMPLATED_args": " ".join([ |
| expand_location_into_runfiles(ctx, a, ctx.attr.data) |
| for a in ctx.attr.templated_args |
| ]), |
| "TEMPLATED_bazel_require_script": _to_manifest_path(ctx, ctx.file._bazel_require_script), |
| "TEMPLATED_env_vars": env_vars, |
| "TEMPLATED_expected_exit_code": str(expected_exit_code), |
| "TEMPLATED_link_modules_script": _to_manifest_path(ctx, ctx.file._link_modules_script), |
| "TEMPLATED_loader_path": script_path, |
| "TEMPLATED_modules_manifest": _to_manifest_path(ctx, node_modules_manifest), |
| "TEMPLATED_repository_args": _to_manifest_path(ctx, ctx.file._repository_args), |
| "TEMPLATED_script_path": _to_execroot_path(ctx, ctx.file.entry_point), |
| "TEMPLATED_vendored_node": "" if is_builtin else strip_external(ctx.file._node.path), |
| } |
| ctx.actions.expand_template( |
| template = ctx.file._launcher_template, |
| output = ctx.outputs.script, |
| substitutions = substitutions, |
| is_executable = True, |
| ) |
| |
| runfiles = [] |
| runfiles.extend(node_tool_files) |
| runfiles.extend(ctx.files._bash_runfile_helpers) |
| runfiles.append(ctx.outputs.loader) |
| runfiles.append(ctx.file._repository_args) |
| |
| if is_windows(ctx): |
| runfiles.append(ctx.outputs.script) |
| executable = create_windows_native_launcher_script(ctx, ctx.outputs.script) |
| else: |
| executable = ctx.outputs.script |
| |
| # entry point is only needed in runfiles if it is a .js file |
| if ctx.file.entry_point.extension == "js": |
| runfiles.append(ctx.file.entry_point) |
| |
| return [ |
| DefaultInfo( |
| executable = executable, |
| runfiles = ctx.runfiles( |
| transitive_files = depset(runfiles), |
| files = node_tool_files + [ |
| ctx.outputs.loader, |
| ] + ctx.files._source_map_support_files + |
| |
| # We need this call to the list of Files. |
| # Calling the .to_list() method may have some perfs hits, |
| # so we should be running this method only once per rule. |
| # see: https://docs.bazel.build/versions/master/skylark/depsets.html#performance |
| node_modules.to_list() + sources.to_list(), |
| collect_data = True, |
| ), |
| ), |
| # TODO(alexeagle): remove sources and node_modules from the runfiles |
| # when downstream usage is ready to rely on linker |
| NodeRuntimeDepsInfo( |
| deps = depset([ctx.file.entry_point], transitive = [node_modules, sources]), |
| pkgs = ctx.attr.data, |
| ), |
| ] |
| |
| _NODEJS_EXECUTABLE_ATTRS = { |
| "configuration_env_vars": attr.string_list( |
| doc = """Pass these configuration environment variables to the resulting binary. |
| Chooses a subset of the configuration environment variables (taken from `ctx.var`), which also |
| includes anything specified via the --define flag. |
| Note, this can lead to different outputs produced by this rule.""", |
| default = [], |
| ), |
| "data": attr.label_list( |
| doc = """Runtime dependencies which may be loaded during execution.""", |
| allow_files = True, |
| aspects = [node_modules_aspect, module_mappings_runtime_aspect], |
| ), |
| "default_env_vars": attr.string_list( |
| doc = """Default environment variables that are added to `configuration_env_vars`. |
| |
| This is separate from the default of `configuration_env_vars` so that a user can set `configuration_env_vars` |
| without losing the defaults that should be set in most cases. |
| |
| The set of default environment variables is: |
| |
| - `COMPILATION_MODE`: rules use this environment variable to produce optimized (eg. mangled and minimized) or debugging output |
| - `VERBOSE_LOGS`: rules use this environment variable to turn on debug output in their logs |
| - `DEBUG`: used by some npm packages to print debugging logs |
| - `NODE_DEBUG`: used by node.js itself to print more logs |
| """, |
| default = ["COMPILATION_MODE", "VERBOSE_LOGS", "DEBUG", "NODE_DEBUG"], |
| ), |
| "entry_point": attr.label( |
| doc = """The script which should be executed first, usually containing a main function. |
| |
| If the entry JavaScript file belongs to the same package (as the BUILD file), |
| you can simply reference it by its relative name to the package directory: |
| |
| ``` |
| nodejs_binary( |
| name = "my_binary", |
| ... |
| entry_point = ":file.js", |
| ) |
| ``` |
| |
| You can specify the entry point as a typescript file so long as you also include |
| the ts_library target in data: |
| |
| ``` |
| ts_library( |
| name = "main", |
| srcs = ["main.ts"], |
| ) |
| |
| nodejs_binary( |
| name = "bin", |
| data = [":main"] |
| entry_point = ":main.ts", |
| ) |
| ``` |
| |
| The rule will use the corresponding `.js` output of the ts_library rule as the entry point. |
| |
| If the entry point target is a rule, it should produce a single JavaScript entry file that will be passed to the nodejs_binary rule. |
| For example: |
| |
| ``` |
| filegroup( |
| name = "entry_file", |
| srcs = ["main.js"], |
| ) |
| |
| nodejs_binary( |
| name = "my_binary", |
| entry_point = ":entry_file", |
| ) |
| ``` |
| |
| The entry_point can also be a label in another workspace: |
| |
| ``` |
| nodejs_binary( |
| name = "history-server", |
| entry_point = "@npm//:node_modules/history-server/modules/cli.js", |
| data = ["@npm//history-server"], |
| ) |
| ``` |
| """, |
| mandatory = True, |
| allow_single_file = True, |
| ), |
| "install_source_map_support": attr.bool( |
| doc = """Install the source-map-support package. |
| Enable this to get stack traces that point to original sources, e.g. if the program was written |
| in TypeScript.""", |
| default = True, |
| ), |
| "node_modules": attr.label( |
| doc = """The npm packages which should be available to `require()` during |
| execution. |
| |
| This attribute is DEPRECATED. As of version 0.13.0 the recommended approach |
| to npm dependencies is to use fine grained npm dependencies which are setup |
| with the `yarn_install` or `npm_install` rules. For example, in targets |
| that used a `//:node_modules` filegroup, |
| |
| ``` |
| nodejs_binary( |
| name = "my_binary", |
| ... |
| node_modules = "//:node_modules", |
| ) |
| ``` |
| |
| which specifies all files within the `//:node_modules` filegroup |
| to be inputs to the `my_binary`. Using fine grained npm dependencies, |
| `my_binary` is defined with only the npm dependencies that are |
| needed: |
| |
| ``` |
| nodejs_binary( |
| name = "my_binary", |
| ... |
| data = [ |
| "@npm//foo", |
| "@npm//bar", |
| ... |
| ], |
| ) |
| ``` |
| |
| In this case, only the `foo` and `bar` npm packages and their |
| transitive deps are includes as inputs to the `my_binary` target |
| which reduces the time required to setup the runfiles for this |
| target (see https://github.com/bazelbuild/bazel/issues/5153). |
| |
| The @npm external repository and the fine grained npm package |
| targets are setup using the `yarn_install` or `npm_install` rule |
| in your WORKSPACE file: |
| |
| yarn_install( |
| name = "npm", |
| package_json = "//:package.json", |
| yarn_lock = "//:yarn.lock", |
| ) |
| |
| For other rules such as `jasmine_node_test`, fine grained |
| npm dependencies are specified in the `deps` attribute: |
| |
| ``` |
| jasmine_node_test( |
| name = "my_test", |
| ... |
| deps = [ |
| "@npm//jasmine", |
| "@npm//foo", |
| "@npm//bar", |
| ... |
| ], |
| ) |
| ``` |
| """, |
| default = Label("//:node_modules_none"), |
| ), |
| "templated_args": attr.string_list( |
| doc = """Arguments which are passed to every execution of the program. |
| To pass a node startup option, prepend it with `--node_options=`, e.g. |
| `--node_options=--preserve-symlinks` |
| """, |
| ), |
| "_bash_runfile_helpers": attr.label(default = Label("@bazel_tools//tools/bash/runfiles")), |
| "_bazel_require_script": attr.label( |
| default = Label("//internal/node:bazel_require_script.js"), |
| allow_single_file = True, |
| ), |
| "_launcher_template": attr.label( |
| default = Label("//internal/node:node_launcher.sh"), |
| allow_single_file = True, |
| ), |
| "_link_modules_script": attr.label( |
| default = Label("//internal/linker:index.js"), |
| allow_single_file = True, |
| ), |
| "_loader_template": attr.label( |
| default = Label("//internal/node:node_loader.js"), |
| allow_single_file = True, |
| ), |
| "_node": attr.label( |
| default = Label("@nodejs//:node_bin"), |
| allow_single_file = True, |
| ), |
| "_repository_args": attr.label( |
| default = Label("@nodejs//:bin/node_repo_args.sh"), |
| allow_single_file = True, |
| ), |
| "_source_map_support_files": attr.label_list( |
| default = [ |
| Label("//third_party/github.com/buffer-from:contents"), |
| Label("//third_party/github.com/source-map:contents"), |
| Label("//third_party/github.com/source-map-support:contents"), |
| ], |
| allow_files = True, |
| ), |
| } |
| |
| _NODEJS_EXECUTABLE_OUTPUTS = { |
| "loader": "%{name}_loader.js", |
| "script": "%{name}.sh", |
| } |
| |
| # The name of the declared rule appears in |
| # bazel query --output=label_kind |
| # So we make these match what the user types in their BUILD file |
| # and duplicate the definitions to give two distinct symbols. |
| nodejs_binary = rule( |
| implementation = _nodejs_binary_impl, |
| attrs = _NODEJS_EXECUTABLE_ATTRS, |
| doc = "Runs some JavaScript code in NodeJS.", |
| executable = True, |
| outputs = _NODEJS_EXECUTABLE_OUTPUTS, |
| toolchains = [ |
| "@build_bazel_rules_nodejs//toolchains/node:toolchain_type", |
| "@bazel_tools//tools/sh:toolchain_type", |
| ], |
| ) |
| |
| nodejs_test = rule( |
| implementation = _nodejs_binary_impl, |
| attrs = dict(_NODEJS_EXECUTABLE_ATTRS, **{ |
| "expected_exit_code": attr.int( |
| doc = "The expected exit code for the test. Defaults to 0.", |
| default = 0, |
| ), |
| }), |
| doc = """ |
| Identical to `nodejs_binary`, except this can be used with `bazel test` as well. |
| When the binary returns zero exit code, the test passes; otherwise it fails. |
| |
| `nodejs_test` is a convenient way to write a novel kind of test based on running |
| your own test runner. For example, the `ts-api-guardian` library has a way to |
| assert the public API of a TypeScript program, and uses `nodejs_test` here: |
| https://github.com/angular/angular/blob/master/tools/ts-api-guardian/index.bzl |
| |
| If you just want to run a standard test using a test runner like Karma or Jasmine, |
| use the specific rules for those test runners, e.g. `jasmine_node_test`. |
| |
| To debug a Node.js test, we recommend saving a group of flags together in a "config". |
| Put this in your `tools/bazel.rc` so it's shared with your team: |
| ``` |
| # Enable debugging tests with --config=debug |
| test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results |
| ``` |
| |
| Now you can add `--config=debug` to any `bazel test` command line. |
| The runtime will pause before executing the program, allowing you to connect a |
| remote debugger. |
| """, |
| test = True, |
| outputs = _NODEJS_EXECUTABLE_OUTPUTS, |
| toolchains = [ |
| "@build_bazel_rules_nodejs//toolchains/node:toolchain_type", |
| "@bazel_tools//tools/sh:toolchain_type", |
| ], |
| ) |