Skip to content

Commit fae114c

Browse files
authored
feat: Add //rust/settings:lto (#3104)
Fixes #3045 This PR adds a new build setting `//rust/settings:lto=(off|thin|fat)` which changes how we specify the following flags: * [`lto`](https://doc.rust-lang.org/rustc/codegen-options/index.html#lto) * [`embed-bitcode`](https://doc.rust-lang.org/rustc/codegen-options/index.html#embed-bitcode) * [`linker-plugin-lto`](https://doc.rust-lang.org/rustc/codegen-options/index.html#linker-plugin-lto) The way we invoke the flags was based on how Cargo does it today ([code](https://github.com/rust-lang/cargo/blob/769f622e12db0001431d8ae36d1093fb8727c5d9/src/cargo/core/compiler/lto.rs#L4)) and based on suggestions from the [Rust docs](https://doc.rust-lang.org/rustc/codegen-options/index.html#embed-bitcode). When LTO is not enabled, we will specify `-Cembed-bitcode=no` which tells `rustc` to skip embedding LLVM bitcode and should speed up builds. Similarly when LTO is enabled we specify `-Clinker-plugin-lto` which will cause `rustc` to skip generating objects files entirely, and instead replace them with LLVM bitcode*. *only when building an `rlib`, when building other crate types we continue generating object files. I added unit tests to make sure we pass the flags correctly, as well as some docs describing the new setting. Please let me know if I should add more!
1 parent 4652fb5 commit fae114c

File tree

6 files changed

+270
-0
lines changed

6 files changed

+270
-0
lines changed

rust/private/lto.bzl

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""A module defining Rust link time optimization (lto) rules"""
2+
3+
load("//rust/private:utils.bzl", "is_exec_configuration")
4+
5+
_LTO_MODES = [
6+
# Default. No mode has been explicitly set, rustc will do "thin local" LTO
7+
# between the codegen units of a single crate.
8+
"unspecified",
9+
# LTO has been explicitly turned "off".
10+
"off",
11+
# Perform "thin" LTO. This is similar to "fat" but takes significantly less
12+
# time to run, but provides similar performance improvements.
13+
#
14+
# See: <http://blog.llvm.org/2016/06/thinlto-scalable-and-incremental-lto.html>
15+
"thin",
16+
# Perform "fat"/full LTO.
17+
"fat",
18+
]
19+
20+
RustLtoInfo = provider(
21+
doc = "A provider describing the link time optimization setting.",
22+
fields = {"mode": "string: The LTO mode specified via a build setting."},
23+
)
24+
25+
def _rust_lto_flag_impl(ctx):
26+
value = ctx.build_setting_value
27+
28+
if value not in _LTO_MODES:
29+
msg = "{NAME} build setting allowed to take values [{EXPECTED}], but was set to: {ACTUAL}".format(
30+
NAME = ctx.label,
31+
VALUES = ", ".join(["'{}'".format(m) for m in _LTO_MODES]),
32+
ACTUAL = value,
33+
)
34+
fail(msg)
35+
36+
return RustLtoInfo(mode = value)
37+
38+
rust_lto_flag = rule(
39+
doc = "A build setting which specifies the link time optimization mode used when building Rust code. Allowed values are: ".format(_LTO_MODES),
40+
implementation = _rust_lto_flag_impl,
41+
build_setting = config.string(flag = True),
42+
)
43+
44+
def _determine_lto_object_format(ctx, toolchain, crate_info):
45+
"""Determines if we should run LTO and what bitcode should get included in a built artifact.
46+
47+
Args:
48+
ctx (ctx): The calling rule's context object.
49+
toolchain (rust_toolchain): The current target's `rust_toolchain`.
50+
crate_info (CrateInfo): The CrateInfo provider of the target crate.
51+
52+
Returns:
53+
string: Returns one of only_object, only_bitcode, object_and_bitcode.
54+
"""
55+
56+
# Even if LTO is enabled don't use it for actions being built in the exec
57+
# configuration, e.g. build scripts and proc-macros. This mimics Cargo.
58+
if is_exec_configuration(ctx):
59+
return "only_object"
60+
61+
mode = toolchain._lto.mode
62+
63+
if mode in ["off", "unspecified"]:
64+
return "only_object"
65+
66+
perform_linking = crate_info.type in ["bin", "staticlib", "cdylib"]
67+
68+
# is_linkable = crate_info.type in ["lib", "rlib", "dylib", "proc-macro"]
69+
is_dynamic = crate_info.type in ["dylib", "cdylib", "proc-macro"]
70+
needs_object = perform_linking or is_dynamic
71+
72+
# At this point we know LTO is enabled, otherwise we would have returned above.
73+
74+
if not needs_object:
75+
# If we're building an 'rlib' and LTO is enabled, then we can skip
76+
# generating object files entirely.
77+
return "only_bitcode"
78+
elif crate_info.type == "dylib":
79+
# If we're a dylib and we're running LTO, then only emit object code
80+
# because 'rustc' doesn't currently support LTO with dylibs.
81+
return "only_object"
82+
else:
83+
return "object_and_bitcode"
84+
85+
def construct_lto_arguments(ctx, toolchain, crate_info):
86+
"""Returns a list of 'rustc' flags to configure link time optimization.
87+
88+
Args:
89+
ctx (ctx): The calling rule's context object.
90+
toolchain (rust_toolchain): The current target's `rust_toolchain`.
91+
crate_info (CrateInfo): The CrateInfo provider of the target crate.
92+
93+
Returns:
94+
list: A list of strings that are valid flags for 'rustc'.
95+
"""
96+
mode = toolchain._lto.mode
97+
format = _determine_lto_object_format(ctx, toolchain, crate_info)
98+
99+
args = []
100+
101+
if mode in ["thin", "fat", "off"] and not is_exec_configuration(ctx):
102+
args.append("lto={}".format(mode))
103+
104+
if format in ["unspecified", "object_and_bitcode"]:
105+
# Embedding LLVM bitcode in object files is `rustc's` default.
106+
args.extend([])
107+
elif format in ["off", "only_object"]:
108+
args.extend(["embed-bitcode=no"])
109+
elif format == "only_bitcode":
110+
args.extend(["linker-plugin-lto"])
111+
else:
112+
fail("unrecognized LTO object format {}".format(format))
113+
114+
return ["-C{}".format(arg) for arg in args]

rust/private/rustc.bzl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ load(
2424
)
2525
load("//rust/private:common.bzl", "rust_common")
2626
load("//rust/private:compat.bzl", "abs")
27+
load("//rust/private:lto.bzl", "construct_lto_arguments")
2728
load("//rust/private:providers.bzl", "RustcOutputDiagnosticsInfo", _BuildInfo = "BuildInfo")
2829
load("//rust/private:stamp.bzl", "is_stamping_enabled")
2930
load(
@@ -998,6 +999,7 @@ def construct_arguments(
998999
data_paths = depset(direct = getattr(attr, "data", []), transitive = [crate_info.compile_data_targets]).to_list()
9991000

10001001
add_edition_flags(rustc_flags, crate_info)
1002+
_add_lto_flags(ctx, toolchain, rustc_flags, crate_info)
10011003

10021004
# Link!
10031005
if ("link" in emit and crate_info.type not in ["rlib", "lib"]) or add_flags_for_binary:
@@ -1583,6 +1585,18 @@ def _collect_nonstatic_linker_inputs(cc_info):
15831585
))
15841586
return shared_linker_inputs
15851587

1588+
def _add_lto_flags(ctx, toolchain, args, crate):
1589+
"""Adds flags to an Args object to configure LTO for 'rustc'.
1590+
1591+
Args:
1592+
ctx (ctx): The calling rule's context object.
1593+
toolchain (rust_toolchain): The current target's `rust_toolchain`.
1594+
args (Args): A reference to an Args object
1595+
crate (CrateInfo): A CrateInfo provider
1596+
"""
1597+
lto_args = construct_lto_arguments(ctx, toolchain, crate)
1598+
args.add_all(lto_args)
1599+
15861600
def establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_configuration, interface_library):
15871601
"""If the produced crate is suitable yield a CcInfo to allow for interop with cc rules
15881602

rust/settings/BUILD.bazel

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ load(
1414
"per_crate_rustc_flag",
1515
"rustc_output_diagnostics",
1616
)
17+
load("//rust/private:lto.bzl", "rust_lto_flag")
1718
load("//rust/private:unpretty.bzl", "rust_unpretty_flag")
1819
load(":incompatible.bzl", "incompatible_flag")
1920

@@ -48,6 +49,12 @@ rust_unpretty_flag(
4849
visibility = ["//visibility:public"],
4950
)
5051

52+
rust_lto_flag(
53+
name = "lto",
54+
build_setting_default = "unspecified",
55+
visibility = ["//visibility:public"],
56+
)
57+
5158
# A flag controlling whether to rename first-party crates such that their names
5259
# encode the Bazel package and target name, instead of just the target name.
5360
#

rust/toolchain.bzl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
44
load("//rust/platform:triple.bzl", "triple")
55
load("//rust/private:common.bzl", "rust_common")
6+
load("//rust/private:lto.bzl", "RustLtoInfo")
67
load("//rust/private:rust_analyzer.bzl", _rust_analyzer_toolchain = "rust_analyzer_toolchain")
78
load(
89
"//rust/private:rustfmt.bzl",
@@ -517,6 +518,7 @@ def _rust_toolchain_impl(ctx):
517518
third_party_dir = ctx.attr._third_party_dir[BuildSettingInfo].value
518519
pipelined_compilation = ctx.attr._pipelined_compilation[BuildSettingInfo].value
519520
no_std = ctx.attr._no_std[BuildSettingInfo].value
521+
lto = ctx.attr._lto[RustLtoInfo]
520522

521523
experimental_use_global_allocator = ctx.attr._experimental_use_global_allocator[BuildSettingInfo].value
522524
if _experimental_use_cc_common_link(ctx):
@@ -701,6 +703,7 @@ def _rust_toolchain_impl(ctx):
701703
_toolchain_generated_sysroot = ctx.attr._toolchain_generated_sysroot[BuildSettingInfo].value,
702704
_incompatible_do_not_include_data_in_compile_data = ctx.attr._incompatible_do_not_include_data_in_compile_data[IncompatibleFlagInfo].enabled,
703705
_no_std = no_std,
706+
_lto = lto,
704707
)
705708
return [
706709
toolchain,
@@ -891,6 +894,10 @@ rust_toolchain = rule(
891894
default = Label("//rust/settings:incompatible_do_not_include_data_in_compile_data"),
892895
doc = "Label to a boolean build setting that controls whether to include data files in compile_data.",
893896
),
897+
"_lto": attr.label(
898+
providers = [RustLtoInfo],
899+
default = Label("//rust/settings:lto"),
900+
),
894901
"_no_std": attr.label(
895902
default = Label("//rust/settings:no_std"),
896903
),

test/unit/lto/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
load(":lto_test_suite.bzl", "lto_test_suite")
2+
3+
lto_test_suite(
4+
name = "lto_test_suite",
5+
)

test/unit/lto/lto_test_suite.bzl

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""Starlark tests for `//rust/settings/lto`"""
2+
3+
load("@bazel_skylib//lib:unittest.bzl", "analysistest")
4+
load("@bazel_skylib//rules:write_file.bzl", "write_file")
5+
load("//rust:defs.bzl", "rust_library")
6+
load(
7+
"//test/unit:common.bzl",
8+
"assert_action_mnemonic",
9+
"assert_argv_contains",
10+
"assert_argv_contains_not",
11+
"assert_argv_contains_prefix_not",
12+
)
13+
14+
def _lto_test_impl(ctx, lto_setting, embed_bitcode, linker_plugin):
15+
env = analysistest.begin(ctx)
16+
target = analysistest.target_under_test(env)
17+
18+
action = target.actions[0]
19+
assert_action_mnemonic(env, action, "Rustc")
20+
21+
# Check if LTO is enabled.
22+
if lto_setting:
23+
assert_argv_contains(env, action, "-Clto={}".format(lto_setting))
24+
else:
25+
assert_argv_contains_prefix_not(env, action, "-Clto")
26+
27+
# Check if we should embed bitcode.
28+
if embed_bitcode:
29+
assert_argv_contains(env, action, "-Cembed-bitcode={}".format(embed_bitcode))
30+
else:
31+
assert_argv_contains_prefix_not(env, action, "-Cembed-bitcode")
32+
33+
# Check if we should use linker plugin LTO.
34+
if linker_plugin:
35+
assert_argv_contains(env, action, "-Clinker-plugin-lto")
36+
else:
37+
assert_argv_contains_not(env, action, "-Clinker-plugin-lto")
38+
39+
return analysistest.end(env)
40+
41+
def _lto_level_default(ctx):
42+
return _lto_test_impl(ctx, None, "no", False)
43+
44+
_lto_level_default_test = analysistest.make(
45+
_lto_level_default,
46+
config_settings = {},
47+
)
48+
49+
def _lto_level_off(ctx):
50+
return _lto_test_impl(ctx, "off", "no", False)
51+
52+
_lto_level_off_test = analysistest.make(
53+
_lto_level_off,
54+
config_settings = {str(Label("//rust/settings:lto")): "off"},
55+
)
56+
57+
def _lto_level_thin(ctx):
58+
return _lto_test_impl(ctx, "thin", None, True)
59+
60+
_lto_level_thin_test = analysistest.make(
61+
_lto_level_thin,
62+
config_settings = {str(Label("//rust/settings:lto")): "thin"},
63+
)
64+
65+
def _lto_level_fat(ctx):
66+
return _lto_test_impl(ctx, "fat", None, True)
67+
68+
_lto_level_fat_test = analysistest.make(
69+
_lto_level_fat,
70+
config_settings = {str(Label("//rust/settings:lto")): "fat"},
71+
)
72+
73+
def lto_test_suite(name):
74+
"""Entry-point macro called from the BUILD file.
75+
76+
Args:
77+
name (str): The name of the test suite.
78+
"""
79+
write_file(
80+
name = "crate_lib",
81+
out = "lib.rs",
82+
content = [
83+
"#[allow(dead_code)]",
84+
"fn add() {}",
85+
"",
86+
],
87+
)
88+
89+
rust_library(
90+
name = "lib",
91+
srcs = [":lib.rs"],
92+
edition = "2021",
93+
)
94+
95+
_lto_level_default_test(
96+
name = "lto_level_default_test",
97+
target_under_test = ":lib",
98+
)
99+
100+
_lto_level_off_test(
101+
name = "lto_level_off_test",
102+
target_under_test = ":lib",
103+
)
104+
105+
_lto_level_thin_test(
106+
name = "lto_level_thin_test",
107+
target_under_test = ":lib",
108+
)
109+
110+
_lto_level_fat_test(
111+
name = "lto_level_fat_test",
112+
target_under_test = ":lib",
113+
)
114+
115+
native.test_suite(
116+
name = name,
117+
tests = [
118+
":lto_level_default_test",
119+
":lto_level_off_test",
120+
":lto_level_thin_test",
121+
":lto_level_fat_test",
122+
],
123+
)

0 commit comments

Comments
 (0)