Skip to content

Commit c23df4d

Browse files
committed
incremental compilation without a worker
This is considerably less invasive than #667, #517 and #421 - there is no extra code to bootstrap, and no toolchains have to be plumbed into the rules. We had been using a worker to get around sandboxing restrictions, but as @hlopko pointed out on #667, that turns out not to be necessary - even normal rules have access to /tmp. Unfortunately it seems that rustc does not expect the source files to change location, and it will consistently crash when used in a sandboxed context. So the approach this PR takes is to disable sandboxing on targets that are being compiled incrementally. This fixes the compiler crashes, and as a bonus, means we're not limited to saving the cache in /tmp. This PR adds a --@rules_rust//:experimental_incremental_base flag to specify the path where incremental build products are stored - if not provided, the rules will function as they normally do. The default behaviour will incrementally compile crates in the local workspace, like cargo does. The behaviour can be adjusted with another flag, which is covered in docs/index.md.
1 parent 1b27714 commit c23df4d

File tree

6 files changed

+192
-5
lines changed

6 files changed

+192
-5
lines changed

BUILD.bazel

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
2-
load("//rust:rust.bzl", "error_format")
2+
load(
3+
"//rust:defs.bzl",
4+
"error_format",
5+
"experimental_incremental_base",
6+
"experimental_incremental_prefixes",
7+
)
38

49
exports_files(["LICENSE"])
510

@@ -17,3 +22,16 @@ error_format(
1722
build_setting_default = "human",
1823
visibility = ["//visibility:public"],
1924
)
25+
26+
# Optional incremental compilation - see docs/index.md
27+
experimental_incremental_base(
28+
name = "experimental_incremental_base",
29+
build_setting_default = "",
30+
visibility = ["//visibility:public"],
31+
)
32+
33+
experimental_incremental_prefixes(
34+
name = "experimental_incremental_prefixes",
35+
build_setting_default = "//,-//vendored//",
36+
visibility = ["//visibility:public"],
37+
)

docs/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package(default_visibility = ["//visibility:private"])
66
bzl_library(
77
name = "docs_deps",
88
srcs = [
9+
"@bazel_skylib//rules:common_settings",
910
"@bazel_tools//tools:bzl_srcs",
1011
"@build_bazel_rules_nodejs//internal/providers:bzl",
1112
],

docs/index.md

+25
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,28 @@ bazel build @examples//hello_world_wasm --platforms=@rules_rust//rust/platform:w
9090

9191
`rust_wasm_bindgen` will automatically transition to the `wasm` platform and can be used when
9292
building WebAssembly code for the host target.
93+
94+
## Incremental Compilation
95+
96+
There is an experimental flag that enables incremental compilation, which can
97+
considerably speed up rebuilds during development.
98+
99+
Targets built with incremental compilation enabled will have sandboxing
100+
disabled, so enabling this option is trading off some of Bazel's hermeticity in
101+
the name of performance. This option is not recommended for CI or release
102+
builds.
103+
104+
To enable incremental compilation, add the following argument to your bazel
105+
build command, adjusting the directory path to one that suits:
106+
107+
--@rules_rust//:experimental_incremental_base=/home/user/cache/bazel_incremental
108+
109+
A separate flag allows you to control which crates are incrementally compiled.
110+
The default is:
111+
112+
--@rules_rust//:experimental_incremental_prefixes=//,-//vendored
113+
114+
This will incrementally compile any crates in the local workspace, but exclude
115+
other repositories like `@raze__*`, and anything in the //vendored package. This
116+
behaviour is similar to cargo, which only incrementally compiles the local
117+
workspace.

rust/defs.bzl

+8
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ load(
3939
load(
4040
"//rust/private:rustc.bzl",
4141
_error_format = "error_format",
42+
_incremental_base = "incremental_base",
43+
_incremental_prefixes = "incremental_prefixes",
4244
)
4345
load(
4446
"//rust/private:rustdoc.bzl",
@@ -88,6 +90,12 @@ rust_clippy = _rust_clippy
8890
error_format = _error_format
8991
# See @rules_rust//rust/private:rustc.bzl for a complete description.
9092

93+
experimental_incremental_base = _incremental_base
94+
# See @rules_rust//rust/private:rustc.bzl for a complete description.
95+
96+
experimental_incremental_prefixes = _incremental_prefixes
97+
# See @rules_rust//rust/private:rustc.bzl for a complete description.
98+
9199
rust_common = _rust_common
92100
# See @rules_rust//rust/private:common.bzl for a complete description.
93101

rust/private/rust.bzl

+25-1
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
# limitations under the License.
1414

1515
# buildifier: disable=module-docstring
16+
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
1617
load("//rust/private:common.bzl", "rust_common")
17-
load("//rust/private:rustc.bzl", "rustc_compile_action")
18+
load("//rust/private:rustc.bzl", "IncrementalInfo", "IncrementalPrefixInfo", "rustc_compile_action")
1819
load("//rust/private:utils.bzl", "crate_name_from_attr", "determine_output_hash", "expand_locations", "find_toolchain")
1920

2021
# TODO(marco): Separate each rule into its own file.
@@ -98,6 +99,24 @@ def _determine_lib_name(name, crate_type, toolchain, lib_hash = ""):
9899
extension = extension,
99100
)
100101

102+
def incremental_info(attr):
103+
"""Extract incremental compilation info from common rule attributes.
104+
105+
Expects to find the following attributes:
106+
107+
_incremental_base
108+
_incremental_prefixes
109+
110+
Returns:
111+
IncrementalInfo: incremental compilation configuration
112+
"""
113+
prefixes = attr._incremental_prefixes[IncrementalPrefixInfo]
114+
base = attr._incremental_base[BuildSettingInfo].value
115+
return IncrementalInfo(
116+
prefixes = prefixes,
117+
base = base,
118+
)
119+
101120
def get_edition(attr, toolchain):
102121
"""Returns the Rust edition from either the current rule's attirbutes or the current `rust_toolchain`
103122
@@ -263,6 +282,7 @@ def _rust_library_common(ctx, crate_type):
263282
is_test = False,
264283
),
265284
output_hash = output_hash,
285+
incremental_info = incremental_info(ctx.attr),
266286
)
267287

268288
def _rust_binary_impl(ctx):
@@ -297,6 +317,7 @@ def _rust_binary_impl(ctx):
297317
rustc_env = ctx.attr.rustc_env,
298318
is_test = False,
299319
),
320+
incremental_info = incremental_info(ctx.attr),
300321
)
301322

302323
def _create_test_launcher(ctx, toolchain, output, providers):
@@ -434,6 +455,7 @@ def _rust_test_common(ctx, toolchain, output):
434455
crate_type = crate_type,
435456
crate_info = crate_info,
436457
rust_flags = ["--test"],
458+
incremental_info = incremental_info(ctx.attr),
437459
)
438460

439461
return _create_test_launcher(ctx, toolchain, output, providers)
@@ -673,6 +695,8 @@ _common_attrs = {
673695
default = "@bazel_tools//tools/cpp:current_cc_toolchain",
674696
),
675697
"_error_format": attr.label(default = "//:error_format"),
698+
"_incremental_base": attr.label(default = "//:experimental_incremental_base"),
699+
"_incremental_prefixes": attr.label(default = "//:experimental_incremental_prefixes"),
676700
"_process_wrapper": attr.label(
677701
default = Label("//util/process_wrapper"),
678702
executable = True,

rust/private/rustc.bzl

+114-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
# buildifier: disable=module-docstring
16+
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
1617
load(
1718
"@bazel_tools//tools/build_defs/cc:action_names.bzl",
1819
"CPP_LINK_EXECUTABLE_ACTION_NAME",
@@ -47,6 +48,22 @@ AliasableDepInfo = provider(
4748
},
4849
)
4950

51+
IncrementalInfo = provider(
52+
doc = "Data relating to incremental compilation.",
53+
fields = {
54+
"base": "string: base folder to store incremental build products",
55+
"prefixes": "IncrementalPrefixInfo: prefixes to include and exclude",
56+
},
57+
)
58+
59+
IncrementalPrefixInfo = provider(
60+
doc = "Prefixes to include and exclude in incremental compilation.",
61+
fields = {
62+
"exclude": "List[string]: prefixes that will exclude a label if matched",
63+
"include": "List[string]: prefixes that will include a label if matched",
64+
},
65+
)
66+
5067
_error_format_values = ["human", "json", "short"]
5168

5269
ErrorFormatInfo = provider(
@@ -492,7 +509,8 @@ def rustc_compile_action(
492509
crate_info,
493510
output_hash = None,
494511
rust_flags = [],
495-
environ = {}):
512+
environ = {},
513+
incremental_info = None):
496514
"""Create and run a rustc compile action based on the current rule's attributes
497515
498516
Args:
@@ -503,6 +521,7 @@ def rustc_compile_action(
503521
output_hash (str, optional): The hashed path of the crate root. Defaults to None.
504522
rust_flags (list, optional): Additional flags to pass to rustc. Defaults to [].
505523
environ (dict, optional): A set of makefile expandable environment variables for the action
524+
incremental_info (str, optional): path to store incremental build products in.
506525
507526
Returns:
508527
list: A list of the following providers:
@@ -554,14 +573,34 @@ def rustc_compile_action(
554573
else:
555574
formatted_version = ""
556575

576+
if (incremental_info and
577+
incremental_info.base and
578+
_want_incremental_compile(ctx.label, incremental_info.prefixes)):
579+
incremental_dir = "{}/{}_{}".format(
580+
incremental_base,
581+
ctx.var["COMPILATION_MODE"],
582+
toolchain.target_triple,
583+
)
584+
args.add("--codegen", "incremental=" + incremental_dir)
585+
586+
# with sandboxing enabled, subsequent rustc invocations will crash,
587+
# as it doesn't expect the source files to have moved
588+
execution_requirements = {"no-sandbox": "1"}
589+
mnemonic = "RustIncr"
590+
else:
591+
execution_requirements = {}
592+
mnemonic = "Rustc"
593+
557594
ctx.actions.run(
558595
executable = ctx.executable._process_wrapper,
559596
inputs = compile_inputs,
560597
outputs = [crate_info.output],
561598
env = env,
562599
arguments = [args],
563-
mnemonic = "Rustc",
564-
progress_message = "Compiling Rust {} {}{} ({} files)".format(
600+
mnemonic = mnemonic,
601+
execution_requirements = execution_requirements,
602+
progress_message = "Compiling {} {} {}{} ({} files)".format(
603+
mnemonic,
565604
crate_info.type,
566605
ctx.label.name,
567606
formatted_version,
@@ -934,3 +973,75 @@ error_format = rule(
934973
implementation = _error_format_impl,
935974
build_setting = config.string(flag = True),
936975
)
976+
977+
def _incremental_base_impl(ctx):
978+
"""Implementation for the incremental_base() rule
979+
980+
Args:
981+
ctx (ctx): The rule's context object
982+
983+
Returns:
984+
BuildSettingInfo: an object with a `value` attribute containing the string.
985+
"""
986+
value = ctx.build_setting_value
987+
return BuildSettingInfo(value = value)
988+
989+
incremental_base = rule(
990+
build_setting = config.string(flag = True),
991+
implementation = _incremental_base_impl,
992+
doc = "Declares a command line argument that accepts an arbitrary string.",
993+
)
994+
995+
def _incremental_prefixes_impl(ctx):
996+
"""Implementation for the incremental_prefixes_flag() rule
997+
998+
Splits the provided string on commas, and then partitions prefixes starting
999+
with a hypen into the exclude list, returning a provider with the include
1000+
and exclude list. The hypens are stripped from the entries in the exclude list.
1001+
1002+
Args:
1003+
ctx (ctx): The rule's context object
1004+
1005+
Returns:
1006+
(IncrementalPrefixInfo): a list of prefixes to include and exclude
1007+
"""
1008+
values = ctx.build_setting_value.split(",")
1009+
include = []
1010+
exclude = []
1011+
for value in ctx.build_setting_value.split(","):
1012+
if not value:
1013+
continue
1014+
elif value.startswith("-"):
1015+
exclude.append(value[1:])
1016+
else:
1017+
include.append(value)
1018+
return IncrementalPrefixInfo(include = include, exclude = exclude)
1019+
1020+
incremental_prefixes = rule(
1021+
build_setting = config.string(flag = True),
1022+
implementation = _incremental_prefixes_impl,
1023+
doc = """Declares a command line argument for incremental prefixes.
1024+
1025+
See _incremental_prefixes_impl() for the details.
1026+
""",
1027+
)
1028+
1029+
def _want_incremental_compile(label, prefixes):
1030+
"""True if the provided prefixes indicate the target should be incrementally compiled.
1031+
1032+
Args:
1033+
label (Label): the label for a given target
1034+
prefixes (IncrementalPrefixInfo): prefixes to include and exclude
1035+
1036+
Returns:
1037+
bool
1038+
"""
1039+
label = str(label)
1040+
for prefix in prefixes.exclude:
1041+
if label.startswith(prefix):
1042+
return False
1043+
for prefix in prefixes.include:
1044+
if label.startswith(prefix):
1045+
return True
1046+
1047+
return False

0 commit comments

Comments
 (0)