Skip to content

Commit 6939bda

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 6939bda

File tree

9 files changed

+243
-57
lines changed

9 files changed

+243
-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: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,8 @@ 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.
378+
Only used on Windows. If it's None, all transitive crate search path will be specified individually.
377379
"""
378380
linker_script = getattr(file, "linker_script") if hasattr(file, "linker_script") else None
379381

@@ -449,10 +451,19 @@ def collect_inputs(
449451
# If stamping is enabled include the volatile status info file
450452
stamp_info = [ctx.version_file] if stamp else []
451453

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

@@ -462,7 +473,7 @@ def collect_inputs(
462473
build_env_files = [f for f in build_env_files] + [build_env_file]
463474
compile_inputs = depset(build_env_files, transitive = [compile_inputs])
464475

465-
return compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs
476+
return compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, transitive_crates_dir
466477

467478
def construct_arguments(
468479
ctx,
@@ -481,6 +492,7 @@ def construct_arguments(
481492
build_env_files,
482493
build_flags_files,
483494
emit = ["dep-info", "link"],
495+
transitive_crates_dir = None,
484496
force_all_deps_direct = False,
485497
force_link = False,
486498
stamp = False,
@@ -504,6 +516,7 @@ def construct_arguments(
504516
build_env_files (list): Files containing rustc environment variables, for instance from `cargo_build_script` actions.
505517
build_flags_files (list): The output files of a `cargo_build_script` actions containing rustc build flags
506518
emit (list): Values for the --emit flag to rustc.
519+
transitive_crates_dir (File, optional): The output directory containing all transitive crates that this crate depends on.
507520
force_all_deps_direct (bool, optional): Whether to pass the transitive rlibs with --extern
508521
to the commandline as opposed to -L.
509522
force_link (bool, optional): Whether to add link flags to the command regardless of `emit`.
@@ -650,7 +663,7 @@ def construct_arguments(
650663
_add_native_link_flags(rustc_flags, dep_info, linkstamp_outs, crate_info.type, toolchain, cc_toolchain, feature_configuration)
651664

652665
# 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)
666+
add_crate_link_flags(rustc_flags, dep_info, transitive_crates_dir, force_all_deps_direct)
654667

655668
needs_extern_proc_macro_flag = "proc-macro" in [crate_info.type, crate_info.wrapped_crate_type] and \
656669
crate_info.edition != "2015"
@@ -739,7 +752,7 @@ def rustc_compile_action(
739752
# Determine if the build is currently running with --stamp
740753
stamp = is_stamping_enabled(attr)
741754

742-
compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs = collect_inputs(
755+
compile_inputs, out_dir, build_env_files, build_flags_files, linkstamp_outs, transitive_crates_dir = collect_inputs(
743756
ctx = ctx,
744757
file = ctx.file,
745758
files = ctx.files,
@@ -769,6 +782,7 @@ def rustc_compile_action(
769782
out_dir = out_dir,
770783
build_env_files = build_env_files,
771784
build_flags_files = build_flags_files,
785+
transitive_crates_dir = transitive_crates_dir,
772786
force_all_deps_direct = force_all_deps_direct,
773787
stamp = stamp,
774788
)
@@ -1040,12 +1054,54 @@ def _get_dir_names(files):
10401054
dirs[f.dirname] = None
10411055
return dirs.keys()
10421056

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

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

10931154
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
1155+
"""A helper macro used by `add_crate_link_flags` for getting the directory name of the current crate's output path.
10951156
10961157
Args:
10971158
crate (CrateInfo): A CrateInfo provider from the current rule
1098-
10991159
Returns:
11001160
str: The directory name of the the output File that will be produced.
11011161
"""

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)