What is the current behavior?
Currently @rules_nodejs//nodejs:toolchain_type is covering 2 separate use cases.
- Direct usage within a rule.
- Runtime for executable outputs.
When execution and target platforms are the same (as is usually the case) scenarios 1 and 2 can be satisfied by the same toolchain implementation.
However when the execution and target platforms are different, problems arise. For example;
# From /___/external/rules_nodejs~~node~nodejs_toolchains/BUILD.bazel
toolchain(
name = "linux_amd64_toolchain",
exec_compatible_with = ["@platforms//os:linux", "@platforms//cpu:x86_64"],
toolchain = "@nodejs_linux_amd64//:toolchain",
toolchain_type = "@rules_nodejs//nodejs:toolchain_type",
)
toolchain(
name = "linux_amd64_toolchain_target",
target_compatible_with = ["@platforms//os:linux", "@platforms//cpu:x86_64"],
toolchain = "@nodejs_linux_amd64//:toolchain",
toolchain_type = "@rules_nodejs//nodejs:toolchain_type",
)
toolchain(
name = "darwin_arm64_toolchain",
exec_compatible_with = ["@platforms//os:macos", "@platforms//cpu:arm64"],
toolchain = "@nodejs_darwin_arm64//:toolchain",
toolchain_type = "@rules_nodejs//nodejs:toolchain_type",
)
toolchain(
name = "darwin_arm64_toolchain_target",
target_compatible_with = ["@platforms//os:macos", "@platforms//cpu:arm64"],
toolchain = "@nodejs_darwin_arm64//:toolchain",
toolchain_type = "@rules_nodejs//nodejs:toolchain_type",
)
Suppose I am on macOS and building for Linux.
:linux_amd64_toolchain will be skipped;
- 🔴
exec_compatible_with
- 🟢
target_compatible_with (no constaints)
:linux_amd64_toolchain_target can be selected;
- 🟢
exec_compatible_with (no constaints)
- 🟢
target_compatible_with
:darwin_arm64_toolchain can be selected;
- 🟢
exec_compatible_with
- 🟢
target_compatible_with (no constaints)
darwin_arm64_toolchain_target will be skipped;
- 🟢
exec_compatible_with (no constaints)
- 🔴
target_compatible_with
That both :linux_amd64_toolchain_target and :darwin_arm64_toolchain represents a contradiction.
Describe the feature
Starting off with an example. Rules Java has 3 toolchain types, 2 of which are of interest.
@bazel_tools//tools/jdk:toolchain_type
The compilation toolchain.
This is always run within a rule (think cfg = "exec") and is used to produce platform-neutral output.
Implementations use exec_compatible_with only.
@bazel_tools//tools/jdk:runtime_toolchain_type
The runtime toolchain.
This is used for outputs (e.g. from java_binary), which indirectly be used in other rules (as an implementation detail that it not directly executed).
Implementations use target_compatible_with only.
So in Java a rule like java_binary would accept both toolchains. @bazel_tools//tools/jdk:toolchain_type for compilation and @bazel_tools//tools/jdk:runtime_toolchain_type so the output has a Java runtime to use.
This split makes it possible to cross-compile (e.g. build for Linux on macOS).
JS is not a compiled language however the Rules JS toolchains are setup to support such a scenario (a subset at least). The problem, as hinted in "What is the current behavior?", is that such cross-compilation scenarios do not work. Attempts to do so lead to one of 2 outcomes.
:*_toolchain is resolved.
- 🟢 Actions using
node from the toolchain runs correctly.
- 🔴
node in the output cannot run on the target platform.
:*_toolchain_target is resolved.
- 🔴 Actions using
node from the toolchain run cannot run.
- 🟡
node in the output runs correctly on the target platform, provided dependent actions do not depend on the resolved node.
Implementing a split like what Rules Java has would address this issue, although it still leaves once use case uncovered.
NodeJS has a handful workflows that require the execution and target platforms to be identical.
- Snapshot generation (
--build-snapshot and --build-snapshot-config)
Produces a snapshot blob that can be later loaded with --snapshot-blob. Requires NodeJS version, architecture, platform, V8 flags and CPU features match what was used to generate the snapshot.
- Single executable applications (
--experimental-sea-config)
This is a multi-step workflow, of which --experimental-sea-config incurs the same requirements as "snapshot generation" when useSnapshot or useCodeCache are enabled.
So, here is my proposal.
- Introduce a new toolchain type
@rules_nodejs//nodejs:runtime_toolchain_type with the same usage semantics as @bazel_tools//tools/jdk:runtime_toolchain_type. Add implementations for this new type to nodejs_toolchains generated external repository.
- [[BREAKING CHANGE]] Remove
:*_toolchain_target toolchain implementations, bringing usage semantics in line with @bazel_tools//tools/jdk:toolchain_type (ignoring that JS doesn't need to be compiled).
- Introduce a new toolchain type to cover cases where execution and target platforms need to be the same. Add implementations for this new type to
nodejs_toolchains generated external repository.
It may make sense to tweak the plan outlined according to what the bulk of current @rules_nodejs//nodejs:toolchain_type usage is for (I suspect right now executable outputs are the primary use case).
How existing toolchain types are supposed to be used isn't always obvious at a glance, same goes for those in Rules Java and especially the generic toolchain_type name. May be worthwhile workshopping the names.
Related
What is the current behavior?
Currently
@rules_nodejs//nodejs:toolchain_typeis covering 2 separate use cases.When execution and target platforms are the same (as is usually the case) scenarios 1 and 2 can be satisfied by the same toolchain implementation.
However when the execution and target platforms are different, problems arise. For example;
Suppose I am on macOS and building for Linux.
:linux_amd64_toolchainwill be skipped;exec_compatible_withtarget_compatible_with(no constaints):linux_amd64_toolchain_targetcan be selected;exec_compatible_with(no constaints)target_compatible_with:darwin_arm64_toolchaincan be selected;exec_compatible_withtarget_compatible_with(no constaints)darwin_arm64_toolchain_targetwill be skipped;exec_compatible_with(no constaints)target_compatible_withThat both
:linux_amd64_toolchain_targetand:darwin_arm64_toolchainrepresents a contradiction.Describe the feature
Starting off with an example. Rules Java has 3 toolchain types, 2 of which are of interest.
@bazel_tools//tools/jdk:toolchain_typeThe compilation toolchain.
This is always run within a rule (think
cfg = "exec") and is used to produce platform-neutral output.Implementations use
exec_compatible_withonly.@bazel_tools//tools/jdk:runtime_toolchain_typeThe runtime toolchain.
This is used for outputs (e.g. from
java_binary), which indirectly be used in other rules (as an implementation detail that it not directly executed).Implementations use
target_compatible_withonly.So in Java a rule like
java_binarywould accept both toolchains.@bazel_tools//tools/jdk:toolchain_typefor compilation and@bazel_tools//tools/jdk:runtime_toolchain_typeso the output has a Java runtime to use.This split makes it possible to cross-compile (e.g. build for Linux on macOS).
JS is not a compiled language however the Rules JS toolchains are setup to support such a scenario (a subset at least). The problem, as hinted in "What is the current behavior?", is that such cross-compilation scenarios do not work. Attempts to do so lead to one of 2 outcomes.
:*_toolchainis resolved.nodefrom the toolchain runs correctly.nodein the output cannot run on the target platform.:*_toolchain_targetis resolved.nodefrom the toolchain run cannot run.nodein the output runs correctly on the target platform, provided dependent actions do not depend on the resolvednode.Implementing a split like what Rules Java has would address this issue, although it still leaves once use case uncovered.
NodeJS has a handful workflows that require the execution and target platforms to be identical.
--build-snapshotand--build-snapshot-config)Produces a snapshot blob that can be later loaded with
--snapshot-blob. Requires NodeJS version, architecture, platform, V8 flags and CPU features match what was used to generate the snapshot.--experimental-sea-config)This is a multi-step workflow, of which
--experimental-sea-configincurs the same requirements as "snapshot generation" whenuseSnapshotoruseCodeCacheare enabled.So, here is my proposal.
@rules_nodejs//nodejs:runtime_toolchain_typewith the same usage semantics as@bazel_tools//tools/jdk:runtime_toolchain_type. Add implementations for this new type tonodejs_toolchainsgenerated external repository.:*_toolchain_targettoolchain implementations, bringing usage semantics in line with@bazel_tools//tools/jdk:toolchain_type(ignoring that JS doesn't need to be compiled).nodejs_toolchainsgenerated external repository.It may make sense to tweak the plan outlined according to what the bulk of current
@rules_nodejs//nodejs:toolchain_typeusage is for (I suspect right now executable outputs are the primary use case).How existing toolchain types are supposed to be used isn't always obvious at a glance, same goes for those in Rules Java and especially the generic
toolchain_typename. May be worthwhile workshopping the names.Related