Skip to content
Draft
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
2 changes: 1 addition & 1 deletion foreign_cc/boost_build.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def _create_configure_script(configureParameters):

return [
"cd $INSTALLDIR",
"##copy_dir_contents_to_dir## $$EXT_BUILD_ROOT$$/{}/. .".format(root),
"##copy_dir_contents_to_dir## $$EXT_BUILD_ROOT$$/{}/. . True".format(root),
"chmod -R +w .",
"##enable_tracing##",
"./bootstrap.sh {}".format(" ".join(ctx.attr.bootstrap_options)),
Expand Down
21 changes: 18 additions & 3 deletions foreign_cc/built_tools/meson_tool_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,28 @@ def _find_meson_main():
if not rlocation:
raise RuntimeError("REAL_MESON is not set")

path = runfiles.Create().Rlocation(rlocation)
r = runfiles.Create()
path = r.Rlocation(rlocation)
if path and os.path.isfile(path):
return path

raise RuntimeError(
"Failed to locate meson main from REAL_MESON={!r}".format(rlocation)
# Provide detailed diagnostics for runfiles resolution failures
diag = ["Failed to locate meson main from REAL_MESON={!r}".format(rlocation)]
diag.append(" Rlocation returned: {!r}".format(path))
diag.append(" RUNFILES_DIR={!r}".format(os.environ.get("RUNFILES_DIR")))
diag.append(
" RUNFILES_MANIFEST_FILE={!r}".format(os.environ.get("RUNFILES_MANIFEST_FILE"))
)
rf_dir = os.environ.get("RUNFILES_DIR", "")
candidate = os.path.join(rf_dir, rlocation) if rf_dir else None
diag.append(
" candidate path: {!r} exists={}".format(
candidate, os.path.isfile(candidate) if candidate else "N/A"
)
)
mf = os.environ.get("RUNFILES_MANIFEST_FILE", "")
diag.append(" manifest exists={}".format(os.path.isfile(mf) if mf else "N/A"))
raise RuntimeError("\n".join(diag))


def main():
Expand Down
2 changes: 1 addition & 1 deletion foreign_cc/built_tools/private/built_tools_framework.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def built_tool_rule_impl(ctx, script_lines, out_dir, mnemonic, additional_tools
"##rm_rf## $$BUILD_TMPDIR$$",
"##mkdirs## $$INSTALLDIR$$",
"##mkdirs## $$BUILD_TMPDIR$$",
"##copy_dir_contents_to_dir## ./{} $$BUILD_TMPDIR$$".format(root),
"##copy_dir_contents_to_dir## ./{} $$BUILD_TMPDIR$$ True".format(root),
"cd \"$$BUILD_TMPDIR$$\"",
]

Expand Down
4 changes: 4 additions & 0 deletions foreign_cc/meson.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@ def meson_with_requirements(name, requirements, **kwargs):
"REAL_MESON": "$(rlocationpath @meson_src//:meson.py)",
},
path = "$(execpath :meson_tool_for_{})".format(name),
staged_path = select({
"@platforms//os:windows": "bin/meson_tool_for_{}.exe".format(name),
"//conditions:default": "bin/meson_tool_for_{}".format(name),
}),
target = ":meson_tool_for_{}".format(name),
tools = ["@meson_src//:meson.py"],
)
Expand Down
4 changes: 2 additions & 2 deletions foreign_cc/private/configure_script.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def create_configure_script(
root_path = "$$EXT_BUILD_ROOT$$/{}".format(root)
configure_path = "{}/{}".format(root_path, configure_command)
if configure_in_place:
script.append("##copy_dir_contents_to_dir## $$EXT_BUILD_ROOT$$/{} $$BUILD_TMPDIR$$".format(root))
script.append("##copy_dir_contents_to_dir## $$EXT_BUILD_ROOT$$/{} $$BUILD_TMPDIR$$ True".format(root))
root_path = "$$BUILD_TMPDIR$$"
configure_path = "{}/{}".format(root_path, configure_command)

Expand Down Expand Up @@ -98,7 +98,7 @@ def create_configure_script(

script.extend(make_commands)
script.append("##disable_tracing##")
script.append("##copy_dir_contents_to_dir## $$BUILD_TMPDIR$$/$$INSTALL_PREFIX$$ $$INSTALLDIR$$\n")
script.append("##copy_dir_contents_to_dir## $$BUILD_TMPDIR$$/$$INSTALL_PREFIX$$ $$INSTALLDIR$$ True\n")

return script

Expand Down
75 changes: 58 additions & 17 deletions foreign_cc/private/framework.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -858,16 +858,43 @@ def _copy_deps_and_tools(files):

if files.tools_files:
lines.append("##mkdirs## $$EXT_BUILD_DEPS$$/bin")
lines.append("##mkdirs## $$EXT_BUILD_DEPS$$/tools")
for tool in files.tools_files:
tool_prefix = "$EXT_BUILD_ROOT/"
tool = tool[len(tool_prefix):] if tool.startswith(tool_prefix) else tool
tool_runfiles = "{}.runfiles".format(tool)
tool_runfiles_manifest = "{}.runfiles_manifest".format(tool)
tool_exe_runfiles_manifest = "{}.exe.runfiles_manifest".format(tool)
lines.append("##symlink_to_dir## $$EXT_BUILD_ROOT$$/{} $$EXT_BUILD_DEPS$$/bin/ False".format(tool))
lines.append("##symlink_to_dir## $$EXT_BUILD_ROOT$$/{} $$EXT_BUILD_DEPS$$/bin/ False".format(tool_runfiles))
lines.append("##symlink_to_dir## $$EXT_BUILD_ROOT$$/{} $$EXT_BUILD_DEPS$$/bin/ False".format(tool_runfiles_manifest))
lines.append("##symlink_to_dir## $$EXT_BUILD_ROOT$$/{} $$EXT_BUILD_DEPS$$/bin/ False".format(tool_exe_runfiles_manifest))
tool_path = _file_path(tool).strip()
if not tool_path:
continue
if getattr(tool, "is_directory", False):
lines.append("##copy_dir_contents_to_dir## \"$$EXT_BUILD_ROOT$$/{}\" \"$$EXT_BUILD_DEPS$$/tools/{}\" True".format(
tool_path,
paths.basename(tool_path),
))
else:
lines.append("##copy_file_to_dir## \"$$EXT_BUILD_ROOT$$/{}\" \"$$EXT_BUILD_DEPS$$/bin\"".format(tool_path))

for tool in files.tools_runfiles:
staged_runfiles = "{}.runfiles".format(tool.staged_path)
source_runfiles = "$$EXT_BUILD_ROOT$$/{}.runfiles".format(tool.invoke_path)

# Bulk-copy the entire runfiles tree instead of copying files
# individually. Individual cp commands have high fork+exec overhead
# (~15s for ~2500 files on Linux, far worse on Windows).
lines.append("##copy_dir_contents_to_dir## \"{}\" \"{}\" False".format(
source_runfiles,
staged_runfiles,
))

# Tell the Python runfiles library where to find the staged tree.
# This must happen here (not in env_prelude) so that $EXT_BUILD_DEPS
# has already been aliased to the short path on Windows — using the
# long exec-root path would exceed MAX_PATH.
#
# Do NOT set RUNFILES_MANIFEST_FILE: the manifest contains absolute
# paths from the original build machine that are not valid in the
# staged tree. _repo_mapping is inside the .runfiles tree and is
# already covered by the bulk copy.
lines.append("export RUNFILES_DIR=\"{staged_runfiles}\"".format(
staged_runfiles = staged_runfiles,
))

for ext_dir in files.ext_build_dirs:
lines.append("##symlink_to_dir## $$EXT_BUILD_ROOT$$/{} $$EXT_BUILD_DEPS$$ True".format(_file_path(ext_dir)))
Expand Down Expand Up @@ -1081,9 +1108,10 @@ InputFiles = provider(
),
libs = "Library files built by Bazel. Will be copied into $EXT_BUILD_DEPS/lib.",
tools_files = (
"Files and directories with tools needed for configuration/building " +
"to be copied into the bin folder, which is added to the PATH"
"Tool runtime files and directories that should be staged under " +
"$EXT_BUILD_DEPS for foreign builds."
),
tools_runfiles = "Structured runfiles trees that should be staged next to staged tools.",
ext_build_dirs = (
"Directories with libraries, built by framework function. " +
"This directories should be copied into $EXT_BUILD_DEPS/lib-name as is, with all contents."
Expand Down Expand Up @@ -1133,14 +1161,23 @@ def _define_inputs(attrs):
# but filter out repeating directories
ext_build_dirs = uniq_list_keep_order(ext_build_dirs)

tools = []
tools_files = []
tools_runfiles = []
input_files = []
for tool in attrs.tools_data:
tools.append(tool.path)
if tool.target:
for file_list in tool.target.files.to_list():
tools_files += _list(file_list)
if tool.target and getattr(tool, "stage_runtime", False):
tools_files += tool.runtime_files.to_list()
runfiles_files = tool.runfiles_files.to_list()
runfiles_manifest = tool.runfiles_manifest
repo_mapping_manifest = tool.repo_mapping_manifest
if runfiles_files or runfiles_manifest or repo_mapping_manifest:
tools_runfiles.append(struct(
files = runfiles_files,
invoke_path = tool.invoke_path,
runfiles_manifest = runfiles_manifest,
repo_mapping_manifest = repo_mapping_manifest,
staged_path = tool.path,
))
Comment on lines +1168 to +1180
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_define_inputs() now only stages tool artifacts into tools_files when tool.stage_runtime is true. This drops non-staged tools (e.g. the ninja wrapper) from $EXT_BUILD_DEPS/bin and therefore from PATH, which can break builds that rely on PATH discovery (Meson’s CMake fallback is one example). Consider restoring the previous behavior for non-staged tools (symlink/copy the tool launcher into $EXT_BUILD_DEPS/bin) while keeping the new runtime-closure staging for staged tools.

Suggested change
if tool.target and getattr(tool, "stage_runtime", False):
tools_files += tool.runtime_files.to_list()
runfiles_files = tool.runfiles_files.to_list()
runfiles_manifest = tool.runfiles_manifest
repo_mapping_manifest = tool.repo_mapping_manifest
if runfiles_files or runfiles_manifest or repo_mapping_manifest:
tools_runfiles.append(struct(
files = runfiles_files,
invoke_path = tool.invoke_path,
runfiles_manifest = runfiles_manifest,
repo_mapping_manifest = repo_mapping_manifest,
staged_path = tool.path,
))
if tool.target:
if getattr(tool, "stage_runtime", False):
tools_files += tool.runtime_files.to_list()
runfiles_files = tool.runfiles_files.to_list()
runfiles_manifest = tool.runfiles_manifest
repo_mapping_manifest = tool.repo_mapping_manifest
if runfiles_files or runfiles_manifest or repo_mapping_manifest:
tools_runfiles.append(struct(
files = runfiles_files,
invoke_path = tool.invoke_path,
runfiles_manifest = runfiles_manifest,
repo_mapping_manifest = repo_mapping_manifest,
staged_path = tool.path,
))
else:
executable = tool.target.files_to_run.executable
if executable:
tools_files.append(executable)

Copilot uses AI. Check for mistakes.

# TODO: Remove, `additional_tools` is deprecated.
for tool in attrs.additional_tools:
Expand All @@ -1159,13 +1196,17 @@ def _define_inputs(attrs):
headers = bazel_headers,
include_dirs = bazel_system_includes,
libs = bazel_libs,
tools_files = tools,
tools_files = tools_files,
tools_runfiles = tools_runfiles,
deps_compilation_info = cc_info_merged.compilation_context,
deps_linking_info = cc_info_merged.linking_context,
ext_build_dirs = ext_build_dirs,
declared_inputs = filter_containing_dirs_from_inputs(attrs.lib_source.files.to_list()) +
bazel_libs +
tools_files +
[runfile for tool in tools_runfiles for runfile in tool.files] +
[tool.runfiles_manifest for tool in tools_runfiles if tool.runfiles_manifest] +
[tool.repo_mapping_manifest for tool in tools_runfiles if tool.repo_mapping_manifest] +
input_files +
cc_info_merged.compilation_context.headers.to_list() + _collect_libs(cc_info_merged.linking_context) +
ext_build_dirs,
Expand Down
19 changes: 19 additions & 0 deletions foreign_cc/private/framework/toolchains/commands.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,28 @@ PLATFORM_COMMANDS = {
doc = "Source directory, immediate children of which are copied",
),
_argument_info(name = "target", data_type = type(""), doc = "Target directory"),
_argument_info(
name = "flatten_timestamps",
data_type = type(""),
doc = "If True, flatten all file timestamps to the source directory mtime (prevents autotools reruns)",
),
],
doc = "Copies contents of the directory to target directory",
),
"copy_file": _command_info(
arguments = [
_argument_info(name = "source", data_type = type(""), doc = "Source file"),
_argument_info(name = "target", data_type = type(""), doc = "Target file"),
],
doc = "Copies a file to an exact target path",
),
"copy_file_to_dir": _command_info(
arguments = [
_argument_info(name = "source", data_type = type(""), doc = "Source file"),
_argument_info(name = "target", data_type = type(""), doc = "Target directory"),
],
doc = "Copies a file into the target directory",
),
"define_absolute_paths": _command_info(
arguments = [
_argument_info(name = "dir_", data_type = type(""), doc = "Directory where to replace"),
Expand Down
66 changes: 63 additions & 3 deletions foreign_cc/private/framework/toolchains/freebsd_commands.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ https://docs.bazel.build/versions/main/install-compile-source.html#bootstrap-uni

load(":commands.bzl", "FunctionAndCallInfo")

def _strip_outer_quotes(val):
"""Remove one layer of surrounding double-quotes if present.

The ##command## arg1 arg2 directive parser (split_arguments) keeps quoted
tokens together but includes the quotes in the value. Platform command
functions already add their own quotes, so we strip the outer layer to
avoid double-quoting.
"""
if len(val) >= 2 and val[0] == '"' and val[-1] == '"':
return val[1:-1]
return val

def shebang():
return "#!/usr/bin/env bash"

Expand Down Expand Up @@ -48,7 +60,7 @@ def disable_tracing():
return "set +x"

def mkdirs(path):
return "mkdir -p \"{path}\"".format(path = path)
return "mkdir -p \"{path}\"".format(path = _strip_outer_quotes(path))

def rm_rf(path):
return "rm -rf \"{path}\"".format(path = path)
Expand Down Expand Up @@ -94,17 +106,63 @@ fi
""",
)

def copy_dir_contents_to_dir(source, target):
def copy_dir_contents_to_dir(source, target, flatten_timestamps):
"""Copy directory contents to target, optionally flattening timestamps.

Args:
source: Source directory whose contents are copied.
target: Target directory.
flatten_timestamps: If "True", set all file timestamps to the source
directory mtime (prevents autotools regeneration).

Returns:
str: Shell command string.
"""
source = _strip_outer_quotes(source)
target = _strip_outer_quotes(target)

# Beause FreeBSD `cp` doesn't have `--no-target-directory`, we have to
# do something more complex for this environment.
return """\
if flatten_timestamps == "True":
return """\
if [[ -d "{source}" ]]; then
cp -L -R "{source}"/. "{target}"
else
cp -L -R "{source}" "{target}"
fi
find "{target}" -type f -exec touch -r "{source}" "{{}}" \\;
""".format(
source = source,
target = target,
)

# Without flatten_timestamps we skip -L (dereference symlinks) and
# tolerate "File exists" errors. Runfiles trees contain repo-mapping
# symlinks (apparent → canonical) that create duplicate destination
# paths; cp errors on the second copy but the file is already present.
return """\
if [[ -d "{source}" ]]; then
cp -R "{source}"/. "{target}" 2>&1 | grep -v "File exists" >&2 || true
else
cp -R "{source}" "{target}" 2>&1 | grep -v "File exists" >&2 || true
fi
Comment on lines +144 to +148
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When flatten_timestamps is "False", the cp ... | grep ... || true pipeline will mask all copy failures (because || true forces success even if cp exits non-zero under pipefail). This can hide real errors (missing source dir, permissions, IO errors) and proceed with an incomplete runfiles tree. Recommend preserving cp’s exit status and only ignoring the specific duplicate-path "File exists" case (e.g., capture stderr, filter it, and fail if any remaining errors or if cp failed for other reasons).

Suggested change
if [[ -d "{source}" ]]; then
cp -R "{source}"/. "{target}" 2>&1 | grep -v "File exists" >&2 || true
else
cp -R "{source}" "{target}" 2>&1 | grep -v "File exists" >&2 || true
fi
_copy_dir_contents_stderr="$(mktemp)"
_copy_dir_contents_status=0
if [[ -d "{source}" ]]; then
cp -R "{source}"/. "{target}" 2>"${{_copy_dir_contents_stderr}}" || _copy_dir_contents_status=$?
else
cp -R "{source}" "{target}" 2>"${{_copy_dir_contents_stderr}}" || _copy_dir_contents_status=$?
fi
if grep -qv "File exists" "${{_copy_dir_contents_stderr}}"; then
grep -v "File exists" "${{_copy_dir_contents_stderr}}" >&2
rm -f "${{_copy_dir_contents_stderr}}"
exit "${{_copy_dir_contents_status:-1}}"
fi
if [[ "${{_copy_dir_contents_status}}" -ne 0 ]] && ! grep -q "File exists" "${{_copy_dir_contents_stderr}}"; then
rm -f "${{_copy_dir_contents_stderr}}"
exit "${{_copy_dir_contents_status}}"
fi
rm -f "${{_copy_dir_contents_stderr}}"

Copilot uses AI. Check for mistakes.
""".format(
source = source,
target = target,
)

def copy_file_to_dir(source, target):
source = _strip_outer_quotes(source)
target = _strip_outer_quotes(target)
return """cp -L "{source}" "{target}/" && touch -r "{source}" "{target}/$(basename "{source}")" """.format(
source = source,
target = target,
)

def copy_file(source, target):
source = _strip_outer_quotes(source)
target = _strip_outer_quotes(target)
return """cp -L "{source}" "{target}" && touch -r "{source}" "{target}" """.format(
source = source,
target = target,
)
Expand Down Expand Up @@ -295,6 +353,8 @@ commands = struct(
children_to_path = children_to_path,
cleanup_function = cleanup_function,
copy_dir_contents_to_dir = copy_dir_contents_to_dir,
copy_file_to_dir = copy_file_to_dir,
copy_file = copy_file,
define_absolute_paths = define_absolute_paths,
define_function = define_function,
define_sandbox_paths = define_sandbox_paths,
Expand Down
Loading
Loading