Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 89 additions & 19 deletions swift/internal/actions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def _apply_action_configs(
additional_tools = []
inputs = []
transitive_inputs = []
unused_inputs = []

for action_config in swift_toolchain.action_configs:
# Skip the action config if it does not apply to the requested action.
Expand Down Expand Up @@ -101,12 +102,14 @@ def _apply_action_configs(
additional_tools.extend(action_inputs.additional_tools)
inputs.extend(action_inputs.inputs)
transitive_inputs.extend(action_inputs.transitive_inputs)
unused_inputs.extend(action_inputs.unused_inputs)

# Merge the action results into a single result that we return.
return ConfigResultInfo(
additional_tools = additional_tools,
inputs = inputs,
transitive_inputs = transitive_inputs,
unused_inputs = unused_inputs,
)

def is_action_enabled(action_name, swift_toolchain = None, toolchains = None):
Expand Down Expand Up @@ -227,23 +230,90 @@ def run_toolchain_action(
swift_toolchain = swift_toolchain,
)

actions.run(
arguments = [tool_executable_args, args],
env = tool_config.env,
exec_group = exec_group,
executable = executable,
toolchain = toolchain_type,
execution_requirements = execution_requirements,
inputs = depset(
action_inputs.inputs,
# Handle unused inputs for cache optimization. When unused_inputs is provided,
# we use Bazel's unused_inputs_list mechanism to exclude certain inputs from
# the action cache key calculation.
#
# According to Bazel docs, if unused_inputs_list file is in inputs, the inputs
# are trimmed BEFORE the action executes (not part of cache key).
# If it's in outputs, inputs are trimmed AFTER the action executes.
#
# We need the file to exist before the action runs, so we use actions.write()
# to create it first, then include it in the action's inputs.
unused_inputs = action_inputs.unused_inputs
if unused_inputs:
# Create a file listing all unused inputs (one path per line)
# Use module_name from prerequisites to ensure uniqueness across targets
module_name = getattr(prerequisites, "module_name", None)
if module_name:
unused_inputs_filename = "{}_{}_unused_inputs.txt".format(
module_name,
action_name,
)
else:
# Fallback: use output file path if available, otherwise action name
outputs = kwargs.get("outputs", [])
if outputs:
# Use the full path relative to output to ensure uniqueness
unused_inputs_filename = "{}_unused_inputs.txt".format(
outputs[0].short_path.replace("/", "_"),
)
else:
unused_inputs_filename = "{}_unused_inputs.txt".format(action_name)

unused_inputs_list_file = actions.declare_file(unused_inputs_filename)

# Write the unused inputs list file. This creates a separate FileWrite
# action that must complete before the compile action can run.
actions.write(
output = unused_inputs_list_file,
content = "\n".join([f.path for f in unused_inputs]),
)

# Include unused inputs in the inputs depset, but pass the list file
# to actions.run so Bazel knows not to use them for caching.
# The unused_inputs_list_file must be in inputs for pre-execution trimming.
all_inputs = depset(
action_inputs.inputs + unused_inputs + [unused_inputs_list_file],
transitive = action_inputs.transitive_inputs,
),
mnemonic = mnemonic if mnemonic else action_name,
resource_set = tool_config.resource_set,
tools = depset(
tools,
transitive = action_inputs.additional_tools,
),
use_default_shell_env = True,
**kwargs
)
)

actions.run(
arguments = [tool_executable_args, args],
env = tool_config.env,
exec_group = exec_group,
executable = executable,
toolchain = toolchain_type,
execution_requirements = execution_requirements,
inputs = all_inputs,
mnemonic = mnemonic if mnemonic else action_name,
resource_set = tool_config.resource_set,
tools = depset(
tools,
transitive = action_inputs.additional_tools,
),
unused_inputs_list = unused_inputs_list_file,
use_default_shell_env = True,
**kwargs
)
else:
actions.run(
arguments = [tool_executable_args, args],
env = tool_config.env,
exec_group = exec_group,
executable = executable,
toolchain = toolchain_type,
execution_requirements = execution_requirements,
inputs = depset(
action_inputs.inputs,
transitive = action_inputs.transitive_inputs,
),
mnemonic = mnemonic if mnemonic else action_name,
resource_set = tool_config.resource_set,
tools = depset(
tools,
transitive = action_inputs.additional_tools,
),
use_default_shell_env = True,
**kwargs
)
102 changes: 99 additions & 3 deletions swift/internal/compiling.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ load(
"SWIFT_FEATURE_THIN_LTO",
"SWIFT_FEATURE_USE_C_MODULES",
"SWIFT_FEATURE_USE_EXPLICIT_SWIFT_MODULE_MAP",
"SWIFT_FEATURE_USE_SWIFTINTERFACE_FOR_CACHING",
"SWIFT_FEATURE__NUM_THREADS_0_IN_SWIFTCOPTS",
"SWIFT_FEATURE__WMO_IN_SWIFTCOPTS",
)
Expand Down Expand Up @@ -123,7 +124,17 @@ def _explicit_swift_module_map_info(
feature_configuration,
target_name,
transitive_modules):
"""Returns the explicit Swift module map file and matching Swift inputs."""
"""Returns the explicit Swift module map file and matching Swift inputs.

Also returns a `unused_inputs` list. When
`swift.use_swiftinterface_for_caching` is enabled and every included module
has a swiftinterface, the returned `inputs` only carry the swiftinterface
files (participating in the action cache key) and the swiftmodule files are
moved into `unused_inputs` — sandboxed in but stripped from the cache key
via Bazel's `unused_inputs_list` mechanism. Modules lacking swiftinterface
(e.g., without `library_evolution`) fall back to swiftmodule going into
`inputs`, keeping correctness.
"""
if is_feature_enabled(
feature_configuration = feature_configuration,
feature_name = SWIFT_FEATURE_USE_EXPLICIT_SWIFT_MODULE_MAP,
Expand All @@ -142,21 +153,49 @@ def _explicit_swift_module_map_info(
if module.is_system
]
if not module_contexts:
return struct(file = None, inputs = [])
return struct(file = None, inputs = [], unused_inputs = [])

filename = "{}.swift-system-explicit-module-map.json".format(target_name)
else:
return struct(file = None, inputs = [])
return struct(file = None, inputs = [], unused_inputs = [])

explicit_swift_module_map_file = actions.declare_file(filename)
write_explicit_swift_module_map_file(
actions = actions,
explicit_swift_module_map_file = explicit_swift_module_map_file,
module_contexts = module_contexts,
)
use_swiftinterface_for_caching = is_feature_enabled(
feature_configuration = feature_configuration,
feature_name = SWIFT_FEATURE_USE_SWIFTINTERFACE_FOR_CACHING,
)
if use_swiftinterface_for_caching:
inputs = []
unused_inputs = []
for module in module_contexts:
swift_module = module.swift
if not swift_module:
continue
interface_file = (
swift_module.private_swiftinterface or
swift_module.swiftinterface
)
if interface_file:
inputs.append(interface_file)
if type(swift_module.swiftmodule) == "File":
unused_inputs.append(swift_module.swiftmodule)
elif type(swift_module.swiftmodule) == "File":
# Fallback: no interface, keep swiftmodule in cache-key inputs.
inputs.append(swift_module.swiftmodule)
return struct(
file = explicit_swift_module_map_file,
inputs = inputs,
unused_inputs = unused_inputs,
)
return struct(
file = explicit_swift_module_map_file,
inputs = transitive_swift_dependency_inputs(module_contexts),
unused_inputs = [],
)

def create_compilation_context(defines, srcs, transitive_modules):
Expand Down Expand Up @@ -311,6 +350,28 @@ def compile_module_interface(
# than the same `depset` being flattened and re-merged multiple times up
# the build graph.
transitive_modules = merged_swift_info.transitive_modules.to_list()

# Collect each transitive Swift dep's swiftinterface and swiftmodule as
# two parallel lists, so that when `swift.use_swiftinterface_for_caching`
# is enabled, configurators can route the swiftinterfaces into `inputs`
# (participating in the action cache key) and the swiftmodules into
# `unused_inputs` (still sandboxed for the compiler, but excluded from the
# cache key). `private_swiftinterface` is preferred over `swiftinterface`,
# matching the selection in `transitive_swift_dependency_inputs`.
transitive_swiftinterfaces = []
transitive_swiftmodules_only = []
for module in transitive_modules:
swift_module = module.swift
if not swift_module:
continue
interface_file = (
swift_module.private_swiftinterface or
swift_module.swiftinterface
)
if interface_file:
transitive_swiftinterfaces.append(interface_file)
if type(swift_module.swiftmodule) == "File":
transitive_swiftmodules_only.append(swift_module.swiftmodule)
transitive_swift_dependency_inputs_list = transitive_swift_dependency_inputs(
transitive_modules,
)
Expand Down Expand Up @@ -339,12 +400,19 @@ def compile_module_interface(
else:
indexstore_directory = None

# Determine if we should use swiftinterface files for caching
use_swiftinterface_for_caching = is_feature_enabled(
feature_configuration = feature_configuration,
feature_name = SWIFT_FEATURE_USE_SWIFTINTERFACE_FOR_CACHING,
)

prerequisites = struct(
additional_inputs = additional_inputs,
bin_dir = feature_configuration._bin_dir,
cc_compilation_context = merged_compilation_context,
explicit_swift_module_map_file = explicit_swift_module_map_info.file,
explicit_swift_module_map_inputs = explicit_swift_module_map_info.inputs,
explicit_swift_module_map_unused_inputs = explicit_swift_module_map_info.unused_inputs,
genfiles_dir = feature_configuration._genfiles_dir,
indexstore_directory = indexstore_directory,
is_swift = True,
Expand All @@ -355,6 +423,9 @@ def compile_module_interface(
target_label = feature_configuration._label,
transitive_modules = transitive_modules,
transitive_swift_dependency_inputs = transitive_swift_dependency_inputs_list,
transitive_swiftinterfaces = transitive_swiftinterfaces,
transitive_swiftmodules_only = transitive_swiftmodules_only,
use_swiftinterface_for_caching = use_swiftinterface_for_caching,
user_compile_flags = copts,
)

Expand Down Expand Up @@ -677,10 +748,24 @@ def compile(
)

defines_set = sets.make(defines)

# See the matching loop in `compile_module_interface` for the rationale.
# `private_swiftinterface` is preferred, matching
# `transitive_swift_dependency_inputs`.
transitive_swiftinterfaces = []
transitive_swiftmodules_only = []
for module in transitive_modules:
swift_module = module.swift
if not swift_module:
continue
interface_file = (
swift_module.private_swiftinterface or
swift_module.swiftinterface
)
if interface_file:
transitive_swiftinterfaces.append(interface_file)
if type(swift_module.swiftmodule) == "File":
transitive_swiftmodules_only.append(swift_module.swiftmodule)
if swift_module.defines:
defines_set = sets.union(
defines_set,
Expand Down Expand Up @@ -783,6 +868,13 @@ to use swift_common.compile(include_dev_srch_paths = ...) instead.\
upcoming_features, experimental_features = upcoming_and_experimental_features(
feature_configuration = feature_configuration,
)

# Determine if we should use swiftinterface files for caching
use_swiftinterface_for_caching = is_feature_enabled(
feature_configuration = feature_configuration,
feature_name = SWIFT_FEATURE_USE_SWIFTINTERFACE_FOR_CACHING,
)

prerequisites = struct(
additional_inputs = additional_inputs + toolchains.cc.all_files.to_list(),
always_include_headers = is_feature_enabled(
Expand All @@ -800,6 +892,7 @@ to use swift_common.compile(include_dev_srch_paths = ...) instead.\
experimental_features = experimental_features,
explicit_swift_module_map_file = explicit_swift_module_map_info.file,
explicit_swift_module_map_inputs = explicit_swift_module_map_info.inputs,
explicit_swift_module_map_unused_inputs = explicit_swift_module_map_info.unused_inputs,
genfiles_dir = feature_configuration._genfiles_dir,
include_dev_srch_paths = include_dev_srch_paths_value,
is_swift = True,
Expand All @@ -811,7 +904,10 @@ to use swift_common.compile(include_dev_srch_paths = ...) instead.\
target_label = feature_configuration._label,
transitive_modules = transitive_modules,
transitive_swift_dependency_inputs = transitive_swift_dependency_inputs_list,
transitive_swiftinterfaces = transitive_swiftinterfaces,
transitive_swiftmodules_only = transitive_swiftmodules_only,
upcoming_features = upcoming_features,
use_swiftinterface_for_caching = use_swiftinterface_for_caching,
user_compile_flags = copts,
workspace_name = workspace_name,
# Merge the compile outputs into the prerequisites.
Expand Down
15 changes: 15 additions & 0 deletions swift/internal/feature_names.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,18 @@ SWIFT_FEATURE_ENABLE_EMBEDDED = "swift.enable_embedded"
# Before swift 6.3 using macros lead to absolute paths in swiftmodule files
# even with -prefix-serialized-debugging-options
SWIFT_FEATURE__SUPPORTS_HERMETIC_SWIFTMODULE = "swift._supports_hermetic_swiftmodule"

# If enabled, the action cache key for Swift compilation will be based on
# `.swiftinterface` files instead of `.swiftmodule` files. This allows upstream
# libraries to make internal changes without triggering recompilation of
# downstream dependencies, as long as their public interface remains stable.
#
# This feature requires all dependencies to be built with `library_evolution`
# enabled (so that `.swiftinterface` files are generated). If a dependency does
# not have a `.swiftinterface` file, the build will fall back to using its
# `.swiftmodule` file for that specific dependency.
#
# NOTE: This feature uses Bazel's `unused_inputs_list` mechanism to exclude
# `.swiftmodule` files from the action cache key while still providing them as
# inputs to the Swift compiler.
SWIFT_FEATURE_USE_SWIFTINTERFACE_FOR_CACHING = "swift.use_swiftinterface_for_caching"
11 changes: 10 additions & 1 deletion swift/toolchains/config/action_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ def _config_result_init(
*,
additional_tools = [],
inputs = [],
transitive_inputs = []):
transitive_inputs = [],
unused_inputs = []):
"""Validates and initializes an action configurator result.

Args:
Expand All @@ -132,6 +133,12 @@ def _config_result_init(
being configured.
transitive_inputs: A list of `depset`s of `File`s that should be passed
as inputs to the action being configured.
unused_inputs: A list of `File`s that should be passed as inputs to the
action but should NOT affect the action cache key. These files are
written to an `unused_inputs_list` file that is passed to the action.
This is useful for files that are needed by the compiler but whose
content changes should not trigger recompilation (e.g., swiftmodule
files when using swiftinterface for caching).

Returns:
A new config result that can be returned from a configurator.
Expand All @@ -140,6 +147,7 @@ def _config_result_init(
"additional_tools": additional_tools,
"inputs": inputs,
"transitive_inputs": transitive_inputs,
"unused_inputs": unused_inputs,
}

def add_arg(arg_name_or_value, value = None, format = None):
Expand Down Expand Up @@ -194,6 +202,7 @@ ConfigResultInfo, _config_result_init_unchecked = provider(
"additional_tools", # List[depset[File]]
"inputs", # list[File]
"transitive_inputs", # List[depset[File]]
"unused_inputs", # list[File] - inputs that don't affect cache key
],
init = _config_result_init,
)
Loading