Skip to content

Commit 4221c8a

Browse files
committed
Symlink all transitive crates into a single directory on Windows
Transitive crate dependencies are passed to rustc by specifying the folder they live in as a library search path (an `-Ldependency=path` flag). Given that bazel builds all libraries into their own folders, this means passing potentially hundreds of paths to rustc which can lead to overflowing the rather short limit for command arguments on Windows. For this reason, this patch creates symlinks to all the transitive crates into a single directory that it then sets as the `-Ldependency=` flag to rustc. This dramatically shortens the command arguments length when calling rustc. Note that we only do that on Windows since it introduces some prep actions that need to do IO and there is no need to regress other platforms that support much longer commane args. I opted for declaring these files as inputs and creating the symlinks via `ctx.actions.symlink` but another option would have been to do that in the process wrapper instead. For context, here is the size returned by `getconf ARG_MAX` on my following machines: ``` macos (12 x86_64): 1,048,576 linux (arch x86_64): 2,097,152 // obviously somewhat lower on 32-bit but I don't have a machine handy windows (10 x86_64): 32,000 ```
1 parent 6630fd5 commit 4221c8a

File tree

9 files changed

+242
-57
lines changed

9 files changed

+242
-57
lines changed

rust/private/clippy.bzl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def _clippy_aspect_impl(target, ctx):
7373
remove_transitive_libs_from_dep_info = toolchain._incompatible_remove_transitive_libs_from_dep_info,
7474
)
7575

76-
compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs = collect_inputs(
76+
compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, transitive_crates_dir = collect_inputs(
7777
ctx,
7878
ctx.rule.file,
7979
ctx.rule.files,
@@ -103,6 +103,7 @@ def _clippy_aspect_impl(target, ctx):
103103
build_env_files = build_env_files,
104104
build_flags_files = build_flags_files,
105105
emit = ["dep-info", "metadata"],
106+
transitive_crates_dir = transitive_crates_dir,
106107
)
107108

108109
if crate_info.is_test:

rust/private/rustc.bzl

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ def collect_inputs(
374374
- (File): An optional path to a generated environment file from a `cargo_build_script` target
375375
- (list): All direct and transitive build flags from the current build info
376376
- (list[File]): Linkstamp outputs.
377+
- (File): Optional - the output directory containing all transitive crates that this crate depends on.
377378
"""
378379
linker_script = getattr(file, "linker_script") if hasattr(file, "linker_script") else None
379380

@@ -449,10 +450,19 @@ def collect_inputs(
449450
# If stamping is enabled include the volatile status info file
450451
stamp_info = [ctx.version_file] if stamp else []
451452

453+
# Windows is known to have a small limit to the length the command args can have (32k characters).
454+
# To prevent the arguments to rustc from overflowing this length, rather than adding each transitive dep as a single library
455+
# search path flag, symlink all transitive deps into a single directory that we can pass as a single path flag.
456+
if toolchain.os == "windows":
457+
transitive_crates_dir, transitive_crates_links = symlink_transitive_crates(ctx, crate_info, dep_info)
458+
else:
459+
transitive_crates_dir, transitive_crates_links = None, []
460+
452461
compile_inputs = depset(
453462
linkstamp_outs + stamp_info,
454463
transitive = [
455464
nolinkstamp_compile_inputs,
465+
depset(transitive_crates_links),
456466
],
457467
)
458468

@@ -462,7 +472,7 @@ def collect_inputs(
462472
build_env_files = [f for f in build_env_files] + [build_env_file]
463473
compile_inputs = depset(build_env_files, transitive = [compile_inputs])
464474

465-
return compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs
475+
return compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, transitive_crates_dir
466476

467477
def construct_arguments(
468478
ctx,
@@ -481,6 +491,7 @@ def construct_arguments(
481491
build_env_files,
482492
build_flags_files,
483493
emit = ["dep-info", "link"],
494+
transitive_crates_dir = None,
484495
force_all_deps_direct = False,
485496
force_link = False,
486497
stamp = False,
@@ -504,6 +515,7 @@ def construct_arguments(
504515
build_env_files (list): Files containing rustc environment variables, for instance from `cargo_build_script` actions.
505516
build_flags_files (list): The output files of a `cargo_build_script` actions containing rustc build flags
506517
emit (list): Values for the --emit flag to rustc.
518+
transitive_crates_dir (File, optional): The output directory containing all transitive crates that this crate depends on.
507519
force_all_deps_direct (bool, optional): Whether to pass the transitive rlibs with --extern
508520
to the commandline as opposed to -L.
509521
force_link (bool, optional): Whether to add link flags to the command regardless of `emit`.
@@ -650,7 +662,7 @@ def construct_arguments(
650662
_add_native_link_flags(rustc_flags, dep_info, linkstamp_outs, crate_info.type, toolchain, cc_toolchain, feature_configuration)
651663

652664
# These always need to be added, even if not linking this crate.
653-
add_crate_link_flags(rustc_flags, dep_info, force_all_deps_direct)
665+
add_crate_link_flags(rustc_flags, dep_info, transitive_crates_dir, force_all_deps_direct)
654666

655667
needs_extern_proc_macro_flag = "proc-macro" in [crate_info.type, crate_info.wrapped_crate_type] and \
656668
crate_info.edition != "2015"
@@ -739,7 +751,7 @@ def rustc_compile_action(
739751
# Determine if the build is currently running with --stamp
740752
stamp = is_stamping_enabled(attr)
741753

742-
compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs = collect_inputs(
754+
compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, transitive_crates_dir = collect_inputs(
743755
ctx = ctx,
744756
file = ctx.file,
745757
files = ctx.files,
@@ -769,6 +781,7 @@ def rustc_compile_action(
769781
out_dir = out_dir,
770782
build_env_files = build_env_files,
771783
build_flags_files = build_flags_files,
784+
transitive_crates_dir = transitive_crates_dir,
772785
force_all_deps_direct = force_all_deps_direct,
773786
stamp = stamp,
774787
)
@@ -1040,12 +1053,54 @@ def _get_dir_names(files):
10401053
dirs[f.dirname] = None
10411054
return dirs.keys()
10421055

1043-
def add_crate_link_flags(args, dep_info, force_all_deps_direct = False):
1056+
def symlink_transitive_crates(ctx, crate_info, dep_info):
1057+
"""Collect and symlink the transitive crates into a single directory.
1058+
1059+
The reason for this is so that we can pass a single entry for the library search path, which greatly shortens
1060+
the length of the command line args that we pass to `rustc` and and prevents it from overflowing the limit set
1061+
by the host platform (which is really only a concern on Windows).
1062+
1063+
Args:
1064+
ctx (ctx): The rule's context object
1065+
crate_info (CrateInfo): The CrateInfo provider of the target crate
1066+
dep_info (DepInfo): The current target's dependency info
1067+
1068+
Returns:
1069+
tuple: A tuple of the following items
1070+
- (File): Optional - the output directory containing all transitive crates that this crate depends on.
1071+
- (list): The list of transitive crates files that should be used as input to the build action.
1072+
"""
1073+
deps = dep_info.transitive_crates.to_list()
1074+
if not deps:
1075+
return None, []
1076+
1077+
output_dirname = crate_info.output.basename + ".transitive_crates"
1078+
links = []
1079+
1080+
# Keep a list of the crates that were currently added so that we can uniquify them.
1081+
names = {}
1082+
1083+
for dep in deps:
1084+
name = dep.output.basename
1085+
if name in names:
1086+
continue
1087+
1088+
link = ctx.actions.declare_file(output_dirname + "/" + name)
1089+
ctx.actions.symlink(output = link, target_file = dep.output, is_executable = True)
1090+
1091+
names[name] = True
1092+
links.append(link)
1093+
1094+
return links[0].dirname, links
1095+
1096+
def add_crate_link_flags(args, dep_info, transitive_crates_dir = None, force_all_deps_direct = False):
10441097
"""Adds link flags to an Args object reference
10451098
10461099
Args:
10471100
args (Args): An arguments object reference
10481101
dep_info (DepInfo): The current target's dependency info
1102+
transitive_crates_dir (File, optional): An output directory containing all transitive crates that this crate depends on.
1103+
If this argument is set, only it will be added as a `-Ldependency=` flag, otherwise all rlibs will be set individually.
10491104
force_all_deps_direct (bool, optional): Whether to pass the transitive rlibs with --extern
10501105
to the commandline as opposed to -L.
10511106
"""
@@ -1064,12 +1119,17 @@ def add_crate_link_flags(args, dep_info, force_all_deps_direct = False):
10641119
else:
10651120
# nb. Direct crates are linked via --extern regardless of their crate_type
10661121
args.add_all(dep_info.direct_crates, map_each = _crate_to_link_flag)
1067-
args.add_all(
1068-
dep_info.transitive_crates,
1069-
map_each = _get_crate_dirname,
1070-
uniquify = True,
1071-
format_each = "-Ldependency=%s",
1072-
)
1122+
1123+
# If transitive rlibs have been collected into this single directory, only set this directory
1124+
if transitive_crates_dir:
1125+
args.add("-Ldependency={}".format(transitive_crates_dir))
1126+
else:
1127+
args.add_all(
1128+
dep_info.transitive_crates,
1129+
map_each = _get_crate_dirname,
1130+
uniquify = True,
1131+
format_each = "-Ldependency=%s",
1132+
)
10731133

10741134
def _crate_to_link_flag(crate):
10751135
"""A helper macro used by `add_crate_link_flags` for adding crate link flags to a Arg object
@@ -1091,11 +1151,10 @@ def _crate_to_link_flag(crate):
10911151
return ["--extern={}={}".format(name, crate_info.output.path)]
10921152

10931153
def _get_crate_dirname(crate):
1094-
"""A helper macro used by `add_crate_link_flags` for getting the directory name of the current crate's output path
1154+
"""A helper macro used by `add_crate_link_flags` for getting the directory name of the current crate's output path.
10951155
10961156
Args:
10971157
crate (CrateInfo): A CrateInfo provider from the current rule
1098-
10991158
Returns:
11001159
str: The directory name of the the output File that will be produced.
11011160
"""

rust/private/rustdoc.bzl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def rustdoc_compile_action(
7979
remove_transitive_libs_from_dep_info = toolchain._incompatible_remove_transitive_libs_from_dep_info,
8080
)
8181

82-
compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs = collect_inputs(
82+
compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, transitive_crates_dir = collect_inputs(
8383
ctx = ctx,
8484
file = ctx.file,
8585
files = ctx.files,
@@ -115,6 +115,7 @@ def rustdoc_compile_action(
115115
build_env_files = build_env_files,
116116
build_flags_files = build_flags_files,
117117
emit = [],
118+
transitive_crates_dir = transitive_crates_dir,
118119
remap_path_prefix = None,
119120
force_link = True,
120121
)

test/unit/common.bzl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ def assert_action_mnemonic(env, action, mnemonic):
6262
),
6363
)
6464

65+
def find_actions_with_mnemonic(env, target, mnemonic):
66+
"""Find all the actions in the given target that have the given mnemonic.
67+
68+
Args:
69+
env: env from analysistest.begin(ctx).
70+
target: the target current being analyzed.
71+
mnemonic (str): The action mnemonic we're filtering on.
72+
73+
Returns:
74+
list: The actions that have the given mnemonic.
75+
"""
76+
actions = []
77+
for action in target.actions:
78+
if action.mnemonic == mnemonic:
79+
actions.append(action)
80+
return actions
81+
6582
def _startswith(list, prefix):
6683
if len(list) < len(prefix):
6784
return False

0 commit comments

Comments
 (0)