Skip to content

Add custom toolchain rule #3139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
26 changes: 26 additions & 0 deletions test/internal/custom_toolchain/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# buildifier: disable=bzl-visibility
load("//xcodeproj/internal:custom_toolchain.bzl", "custom_toolchain")

# Example swiftc override for testing
filegroup(
name = "test_swiftc",
srcs = ["test_swiftc.sh"],
visibility = ["//visibility:public"],
)

# Test target for custom_toolchain
custom_toolchain(
name = "test_toolchain",
overrides = {
":test_swiftc": "swiftc",
},
toolchain_name = "TestCustomToolchain",
)

# Add a simple test rule that depends on the toolchain
sh_test(
name = "custom_toolchain_test",
srcs = ["custom_toolchain_test.sh"],
args = ["$(location :test_toolchain)"],
data = [":test_toolchain"],
)
46 changes: 46 additions & 0 deletions test/internal/custom_toolchain/custom_toolchain_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/bash

set -euo pipefail

# The first argument should be the path to the toolchain directory
TOOLCHAIN_DIR="$1"

echo "Verifying toolchain at: $TOOLCHAIN_DIR"

# Check that the toolchain directory exists
if [[ ! -d "$TOOLCHAIN_DIR" ]]; then
echo "ERROR: Toolchain directory does not exist: $TOOLCHAIN_DIR"
exit 1
fi

# Check that ToolchainInfo.plist exists
if [[ ! -f "$TOOLCHAIN_DIR/ToolchainInfo.plist" ]]; then
echo "ERROR: ToolchainInfo.plist not found in toolchain"
exit 1
fi

# Check for correct identifiers in the plist
if ! grep -q "TestCustomToolchain" "$TOOLCHAIN_DIR/ToolchainInfo.plist"; then
echo "ERROR: ToolchainInfo.plist doesn't contain TestCustomToolchain"
exit 1
fi

# Check that our custom swiftc is properly linked/copied
if [[ ! -f "$TOOLCHAIN_DIR/usr/bin/swiftc" ]]; then
echo "ERROR: swiftc not found in toolchain"
exit 1
fi

# Ensure swiftc is executable
if [[ ! -x "$TOOLCHAIN_DIR/usr/bin/swiftc" ]]; then
echo "ERROR: swiftc is not executable"
exit 1
fi

# Test if the swiftc actually runs
if ! "$TOOLCHAIN_DIR/usr/bin/swiftc" --version > /dev/null 2>&1; then
echo "WARN: swiftc doesn't run correctly, but this is expected in tests"
fi

echo "Custom toolchain validation successful!"
exit 0
11 changes: 11 additions & 0 deletions test/internal/custom_toolchain/test_swiftc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash

# This is a test script that simulates a custom Swift compiler
# It will be used as an override in the custom toolchain

# Print inputs for debugging
echo "Custom swiftc called with args: $@" >&2

# In a real override, you would do something meaningful with the args
# For testing, just exit successfully
exit 0
166 changes: 166 additions & 0 deletions xcodeproj/internal/custom_toolchain.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Implementation of the `custom_toolchain` rule."""

load("//xcodeproj/internal:providers.bzl", "ToolchainInfo")

def _get_xcode_product_version(*, xcode_config):
raw_version = str(xcode_config.xcode_version())
if not raw_version:
fail("""\
`xcode_config.xcode_version` was not set. This is a bazel bug. Try again.
""")

version_components = raw_version.split(".")
if len(version_components) != 4:
fail("""\
`xcode_config.xcode_version` returned an unexpected number of components: {}
""".format(len(version_components)))

return version_components[3]

def _custom_toolchain_impl(ctx):
xcode_version = _get_xcode_product_version(
xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig],
)

toolchain_name_base = ctx.attr.toolchain_name
toolchain_id = "com.rules_xcodeproj.{}.{}".format(toolchain_name_base, xcode_version)
full_toolchain_name = "{}{}".format(toolchain_name_base, xcode_version)

# Create two directories - one for symlinks, one for the final overridden toolchain
symlink_toolchain_dir = ctx.actions.declare_directory(full_toolchain_name + ".symlink.xctoolchain")
final_toolchain_dir = ctx.actions.declare_directory(full_toolchain_name + ".xctoolchain")

resolved_overrides = {}
override_files = []

# Process tools from comma-separated list
for stub_target, tools_str in ctx.attr.overrides.items():
files = stub_target.files.to_list()
if not files:
fail("ERROR: Override stub does not produce any files!")

if len(files) > 1:
fail("ERROR: Override stub produces multiple files ({}). Each stub must have exactly one file.".format(
len(files),
))

stub_file = files[0]
if stub_file not in override_files:
override_files.append(stub_file)

# Split comma-separated list of tool names
tool_names = [name.strip() for name in tools_str.split(",")]

# Add an entry for each tool name
for tool_name in tool_names:
if tool_name: # Skip empty names
resolved_overrides[tool_name] = stub_file.path

# Instead of passing the full map of overrides, just pass the tool names
# This way, changes to the stubs don't trigger a rebuild
tool_names_list = " ".join(resolved_overrides.keys())

overrides_list = " ".join(["{}={}".format(k, v) for k, v in resolved_overrides.items()])

symlink_script_file = ctx.actions.declare_file(full_toolchain_name + "_symlink.sh")
override_script_file = ctx.actions.declare_file(full_toolchain_name + "_override.sh")
override_marker = ctx.actions.declare_file(full_toolchain_name + ".override.marker")

# Create symlink script
ctx.actions.expand_template(
template = ctx.file._symlink_template,
output = symlink_script_file,
is_executable = True,
substitutions = {
"%tool_names_list%": tool_names_list,
"%toolchain_dir%": symlink_toolchain_dir.path,
"%toolchain_id%": toolchain_id,
"%toolchain_name_base%": full_toolchain_name,
"%xcode_version%": xcode_version,
},
)

# First run the symlinking script to set up the toolchain
ctx.actions.run_shell(
outputs = [symlink_toolchain_dir],
tools = [symlink_script_file],
mnemonic = "CreateSymlinkToolchain",
command = symlink_script_file.path,
execution_requirements = {
"local": "1",
"no-cache": "1",
"no-sandbox": "1",
"requires-darwin": "1",
},
use_default_shell_env = True,
)

if override_files:
ctx.actions.expand_template(
template = ctx.file._override_template,
output = override_script_file,
is_executable = True,
substitutions = {
"%final_toolchain_dir%": final_toolchain_dir.path,
"%marker_file%": override_marker.path,
"%overrides_list%": overrides_list,
"%symlink_toolchain_dir%": symlink_toolchain_dir.path,
"%tool_names_list%": tool_names_list,
},
)

ctx.actions.run_shell(
inputs = override_files + [symlink_toolchain_dir],
outputs = [final_toolchain_dir, override_marker],
tools = [override_script_file],
mnemonic = "ApplyCustomToolchainOverrides",
command = override_script_file.path,
execution_requirements = {
"local": "1",
"no-cache": "1",
"no-sandbox": "1",
"requires-darwin": "1",
},
use_default_shell_env = True,
)

runfiles = ctx.runfiles(files = override_files + [symlink_script_file, override_script_file, override_marker])

toolchain_provider = ToolchainInfo(
name = full_toolchain_name,
identifier = toolchain_id,
)

return [
DefaultInfo(
files = depset([final_toolchain_dir if override_files else symlink_toolchain_dir]),
runfiles = runfiles,
),
toolchain_provider,
]

custom_toolchain = rule(
implementation = _custom_toolchain_impl,
attrs = {
"overrides": attr.label_keyed_string_dict(
allow_files = True,
mandatory = True,
doc = "Map from stub target to comma-separated list of tool names that should use that stub",
),
"toolchain_name": attr.string(mandatory = True),
"_override_template": attr.label(
allow_single_file = True,
default = Label("//xcodeproj/internal/templates:custom_toolchain_override.sh"),
),
"_symlink_template": attr.label(
allow_single_file = True,
default = Label("//xcodeproj/internal/templates:custom_toolchain_symlink.sh"),
),
"_xcode_config": attr.label(
default = configuration_field(
name = "xcode_config_label",
fragment = "apple",
),
),
},
)
8 changes: 8 additions & 0 deletions xcodeproj/internal/providers.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ XcodeProjRunnerOutputInfo = provider(
"runner": "The xcodeproj runner.",
},
)

ToolchainInfo = provider(
doc = "Information about the custom toolchain",
fields = {
"identifier": "The bundle identifier of the toolchain",
"name": "The full name of the toolchain",
},
)
64 changes: 64 additions & 0 deletions xcodeproj/internal/templates/custom_toolchain_override.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/bin/bash
set -euo pipefail

SYMLINK_TOOLCHAIN_DIR="%symlink_toolchain_dir%"
FINAL_TOOLCHAIN_DIR="%final_toolchain_dir%"
MARKER_FILE="%marker_file%"

# Get the default toolchain path
DEFAULT_TOOLCHAIN=$(xcrun --find clang | sed 's|/usr/bin/clang$||')

OVERRIDES_FILE=$(mktemp)
echo "%overrides_list%" > "$OVERRIDES_FILE"

TOOL_NAMES_FILE=$(mktemp)
echo "%tool_names_list%" > "$TOOL_NAMES_FILE"

for tool_name in $(cat "$TOOL_NAMES_FILE"); do
VALUE=""
for override in $(cat "$OVERRIDES_FILE"); do
KEY="${override%%=*}"
if [[ "$KEY" == "$tool_name" ]]; then
VALUE="${override#*=}"
break
fi
done

if [[ -z "$VALUE" ]]; then
echo "Error: No override found for tool: $tool_name"
echo "ERROR: No override found for tool: $tool_name" >> "$MARKER_FILE"
continue
fi

find "$DEFAULT_TOOLCHAIN/usr/bin" -name "$tool_name" | while read -r default_tool_path; do
rel_path="${default_tool_path#"$DEFAULT_TOOLCHAIN/"}"
target_file="$FINAL_TOOLCHAIN_DIR/$rel_path"

mkdir -p "$(dirname "$target_file")"

override_path="$PWD/$VALUE"
cp "$override_path" "$target_file"

echo "Copied $override_path to $target_file (rel_path: $rel_path)" >> "$MARKER_FILE"
done
done

# Clean up temporary files
rm -f "$OVERRIDES_FILE"
rm -f "$TOOL_NAMES_FILE"

# Copy the symlink toolchain to the final toolchain directory
mkdir -p "$FINAL_TOOLCHAIN_DIR"
cp -RP "$SYMLINK_TOOLCHAIN_DIR/"* "$FINAL_TOOLCHAIN_DIR/"

# Create a symlink to the toolchain in the user's Library directory
HOME_TOOLCHAIN_NAME=$(basename "$FINAL_TOOLCHAIN_DIR")
USER_TOOLCHAIN_PATH="/Users/$(id -un)/Library/Developer/Toolchains/$HOME_TOOLCHAIN_NAME"
mkdir -p "$(dirname "$USER_TOOLCHAIN_PATH")"
if [[ -e "$USER_TOOLCHAIN_PATH" || -L "$USER_TOOLCHAIN_PATH" ]]; then
rm -rf "$USER_TOOLCHAIN_PATH"
fi
ln -sf "$PWD/$FINAL_TOOLCHAIN_DIR" "$USER_TOOLCHAIN_PATH"
echo "Created symlink: $USER_TOOLCHAIN_PATH -> $PWD/$FINAL_TOOLCHAIN_DIR" >> "$MARKER_FILE"


Loading