diff --git a/test/internal/custom_toolchain/BUILD b/test/internal/custom_toolchain/BUILD new file mode 100644 index 000000000..d0aa56b15 --- /dev/null +++ b/test/internal/custom_toolchain/BUILD @@ -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"], +) diff --git a/test/internal/custom_toolchain/custom_toolchain_test.sh b/test/internal/custom_toolchain/custom_toolchain_test.sh new file mode 100755 index 000000000..94146f21b --- /dev/null +++ b/test/internal/custom_toolchain/custom_toolchain_test.sh @@ -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 \ No newline at end of file diff --git a/test/internal/custom_toolchain/test_swiftc.sh b/test/internal/custom_toolchain/test_swiftc.sh new file mode 100644 index 000000000..ef1872d99 --- /dev/null +++ b/test/internal/custom_toolchain/test_swiftc.sh @@ -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 diff --git a/xcodeproj/internal/custom_toolchain.bzl b/xcodeproj/internal/custom_toolchain.bzl new file mode 100644 index 000000000..39b4c378f --- /dev/null +++ b/xcodeproj/internal/custom_toolchain.bzl @@ -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", + ), + ), + }, +) diff --git a/xcodeproj/internal/providers.bzl b/xcodeproj/internal/providers.bzl index 397c7e330..4908e1c3b 100644 --- a/xcodeproj/internal/providers.bzl +++ b/xcodeproj/internal/providers.bzl @@ -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", + }, +) diff --git a/xcodeproj/internal/templates/custom_toolchain_override.sh b/xcodeproj/internal/templates/custom_toolchain_override.sh new file mode 100644 index 000000000..f8e1a62b8 --- /dev/null +++ b/xcodeproj/internal/templates/custom_toolchain_override.sh @@ -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" + + diff --git a/xcodeproj/internal/templates/custom_toolchain_symlink.sh b/xcodeproj/internal/templates/custom_toolchain_symlink.sh new file mode 100644 index 000000000..611a61bc9 --- /dev/null +++ b/xcodeproj/internal/templates/custom_toolchain_symlink.sh @@ -0,0 +1,85 @@ +#!/bin/bash +set -euo pipefail + +# Define constants within the script +TOOLCHAIN_NAME_BASE="%toolchain_name_base%" +TOOLCHAIN_DIR="%toolchain_dir%" +XCODE_VERSION="%xcode_version%" + +# Get Xcode version and default toolchain path +DEFAULT_TOOLCHAIN=$(xcrun --find clang | sed 's|/usr/bin/clang$||') +XCODE_RAW_VERSION=$(xcodebuild -version | head -n 1) + +TOOL_NAMES_FILE=$(mktemp) +echo "%tool_names_list%" > "$TOOL_NAMES_FILE" + +HOME_TOOLCHAIN_NAME="BazelRulesXcodeProj${XCODE_VERSION}" +USER_TOOLCHAIN_PATH="/Users/$(id -un)/Library/Developer/Toolchains/${HOME_TOOLCHAIN_NAME}.xctoolchain" +BUILT_TOOLCHAIN_PATH="$PWD/$TOOLCHAIN_DIR" + +mkdir -p "$TOOLCHAIN_DIR" + +# Process all files from the default toolchain +find "$DEFAULT_TOOLCHAIN" -type f -o -type l | while read -r file; do + rel_path="${file#"$DEFAULT_TOOLCHAIN/"}" + base_name=$(basename "$rel_path") + + # Skip ToolchainInfo.plist as we'll create our own + if [[ "$rel_path" == "ToolchainInfo.plist" ]]; then + continue + fi + + # Check if this file is in the list of tools to be overridden + should_skip=0 + for tool_name in $(cat "$TOOL_NAMES_FILE"); do + if [[ "$base_name" == "$tool_name" ]]; then + # Skip creating a symlink for overridden tools + should_skip=1 + break + fi + done + + if [[ $should_skip -eq 1 ]]; then + continue + fi + + # Ensure parent directory exists + mkdir -p "$TOOLCHAIN_DIR/$(dirname "$rel_path")" + + # Create symlink to the original file + ln -sf "$file" "$TOOLCHAIN_DIR/$rel_path" +done + +# Generate the ToolchainInfo.plist directly with Xcode version information +cat > "$TOOLCHAIN_DIR/ToolchainInfo.plist" << EOF + + + + + Aliases + + ${HOME_TOOLCHAIN_NAME} + + CFBundleIdentifier + com.rules_xcodeproj.BazelRulesXcodeProj.${XCODE_VERSION} + CompatibilityVersion + 2 + CompatibilityVersionDisplayString + ${XCODE_RAW_VERSION} + DisplayName + ${HOME_TOOLCHAIN_NAME} + ReportProblemURL + https://github.com/MobileNativeFoundation/rules_xcodeproj + ShortDisplayName + ${HOME_TOOLCHAIN_NAME} + Version + 0.1.0 + + +EOF + +mkdir -p "$(dirname "$USER_TOOLCHAIN_PATH")" +if [[ -e "$USER_TOOLCHAIN_PATH" || -L "$USER_TOOLCHAIN_PATH" ]]; then + rm -rf "$USER_TOOLCHAIN_PATH" +fi +ln -sf "$BUILT_TOOLCHAIN_PATH" "$USER_TOOLCHAIN_PATH"