From 4e1ef38782fa2944229aa037738a2a4d6a6a8ff3 Mon Sep 17 00:00:00 2001 From: Karim Alweheshy Date: Fri, 21 Mar 2025 10:28:57 +0100 Subject: [PATCH 1/4] Add custom toolchain rule and tests Signed-off-by: Karim Alweheshy --- test/internal/custom_toolchain/BUILD | 27 +++++ .../custom_toolchain/custom_toolchain_test.sh | 46 ++++++++ test/internal/custom_toolchain/test_swiftc.sh | 11 ++ xcodeproj/internal/custom_toolchain.bzl | 105 ++++++++++++++++++ .../templates/custom_toolchain_symlink.sh | 86 ++++++++++++++ 5 files changed, 275 insertions(+) create mode 100644 test/internal/custom_toolchain/BUILD create mode 100755 test/internal/custom_toolchain/custom_toolchain_test.sh create mode 100644 test/internal/custom_toolchain/test_swiftc.sh create mode 100644 xcodeproj/internal/custom_toolchain.bzl create mode 100644 xcodeproj/internal/templates/custom_toolchain_symlink.sh diff --git a/test/internal/custom_toolchain/BUILD b/test/internal/custom_toolchain/BUILD new file mode 100644 index 000000000..62c6ce4c7 --- /dev/null +++ b/test/internal/custom_toolchain/BUILD @@ -0,0 +1,27 @@ +# 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 = { + # Key is a label target, value is the tool name + ":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..e4bf9cb2b --- /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 "BazelRulesXcodeProj" "$TOOLCHAIN_DIR/ToolchainInfo.plist"; then + echo "ERROR: ToolchainInfo.plist doesn't contain BazelRulesXcodeProj" + 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..a357576ba --- /dev/null +++ b/xcodeproj/internal/custom_toolchain.bzl @@ -0,0 +1,105 @@ +"""Implementation of the `custom_toolchain` rule.""" + +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: + # This will result in analysis cache misses, but it's better than + # failing + return raw_version + + 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_dir = ctx.actions.declare_directory( + toolchain_name_base + "{}".format(xcode_version) + ".xctoolchain", + ) + + resolved_overrides = {} + override_files = [] + + for tool_target, tool_name in ctx.attr.overrides.items(): + files = tool_target.files.to_list() + if not files: + fail("ERROR: Override for '{}' does not produce any files!".format(tool_name)) + + if len(files) > 1: + fail("ERROR: Override for '{}' produces multiple files ({}). Each override must have exactly one file.".format( + tool_name, + len(files), + )) + + override_file = files[0] + override_files.append(override_file) + resolved_overrides[tool_name] = override_file.path + + overrides_list = " ".join(["{}={}".format(k, v) for k, v in resolved_overrides.items()]) + + script_file = ctx.actions.declare_file(toolchain_name_base + "_setup.sh") + + ctx.actions.expand_template( + template = ctx.file._symlink_template, + output = script_file, + is_executable = True, + substitutions = { + "%overrides_list%": overrides_list, + "%toolchain_dir%": toolchain_dir.path, + "%toolchain_name_base%": toolchain_name_base, + "%xcode_version%": xcode_version, + }, + ) + + ctx.actions.run_shell( + outputs = [toolchain_dir], + inputs = override_files, + tools = [script_file], + mnemonic = "CreateCustomToolchain", + command = script_file.path, + execution_requirements = { + "local": "1", + "no-cache": "1", + "no-sandbox": "1", + "requires-darwin": "1", + }, + use_default_shell_env = True, + ) + + # Create runfiles with the override files and script file + runfiles = ctx.runfiles(files = override_files + [script_file]) + + return [DefaultInfo( + files = depset([toolchain_dir]), + runfiles = runfiles, + )] + +custom_toolchain = rule( + implementation = _custom_toolchain_impl, + attrs = { + "overrides": attr.label_keyed_string_dict( + allow_files = True, + mandatory = False, + default = {}, + ), + "toolchain_name": attr.string(mandatory = True), + "_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/templates/custom_toolchain_symlink.sh b/xcodeproj/internal/templates/custom_toolchain_symlink.sh new file mode 100644 index 000000000..5f27577ca --- /dev/null +++ b/xcodeproj/internal/templates/custom_toolchain_symlink.sh @@ -0,0 +1,86 @@ +#!/bin/bash +set -e + +# 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) + +# Define toolchain names +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 + base_name="$(basename "$file")" + rel_path="${file#"$DEFAULT_TOOLCHAIN/"}" + + # Skip ToolchainInfo.plist as we'll create our own + if [[ "$rel_path" == "ToolchainInfo.plist" ]]; then + continue + fi + + # Ensure parent directory exists + mkdir -p "$TOOLCHAIN_DIR/$(dirname "$rel_path")" + + # Process overrides + override_found=false + while IFS='=' read -r key value; do + if [[ "$key" == "$base_name" ]]; then + value="$PWD/$value" + cp "$value" "$TOOLCHAIN_DIR/$rel_path" + # Make executable if original is executable + if [[ -x "$file" ]]; then + chmod +x "$TOOLCHAIN_DIR/$rel_path" + fi + override_found=true + break + fi + done <<< "%overrides_list%" + + # If no override found, symlink the original + if [[ "$override_found" == "false" ]]; then + ln -sf "$file" "$TOOLCHAIN_DIR/$rel_path" + fi +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" From 2d78cae914d610764ab927cd94176aa07a1da372 Mon Sep 17 00:00:00 2001 From: Karim Alweheshy Date: Sat, 22 Mar 2025 21:19:38 +0100 Subject: [PATCH 2/4] improve toolchain --- test/internal/custom_toolchain/BUILD | 1 - .../custom_toolchain/custom_toolchain_test.sh | 4 +- xcodeproj/internal/custom_toolchain.bzl | 58 ++++++++++++------- xcodeproj/internal/providers.bzl | 8 +++ .../templates/custom_toolchain_symlink.sh | 43 +++++++++----- 5 files changed, 78 insertions(+), 36 deletions(-) diff --git a/test/internal/custom_toolchain/BUILD b/test/internal/custom_toolchain/BUILD index 62c6ce4c7..d0aa56b15 100644 --- a/test/internal/custom_toolchain/BUILD +++ b/test/internal/custom_toolchain/BUILD @@ -12,7 +12,6 @@ filegroup( custom_toolchain( name = "test_toolchain", overrides = { - # Key is a label target, value is the tool name ":test_swiftc": "swiftc", }, toolchain_name = "TestCustomToolchain", diff --git a/test/internal/custom_toolchain/custom_toolchain_test.sh b/test/internal/custom_toolchain/custom_toolchain_test.sh index e4bf9cb2b..94146f21b 100755 --- a/test/internal/custom_toolchain/custom_toolchain_test.sh +++ b/test/internal/custom_toolchain/custom_toolchain_test.sh @@ -20,8 +20,8 @@ if [[ ! -f "$TOOLCHAIN_DIR/ToolchainInfo.plist" ]]; then fi # Check for correct identifiers in the plist -if ! grep -q "BazelRulesXcodeProj" "$TOOLCHAIN_DIR/ToolchainInfo.plist"; then - echo "ERROR: ToolchainInfo.plist doesn't contain BazelRulesXcodeProj" +if ! grep -q "TestCustomToolchain" "$TOOLCHAIN_DIR/ToolchainInfo.plist"; then + echo "ERROR: ToolchainInfo.plist doesn't contain TestCustomToolchain" exit 1 fi diff --git a/xcodeproj/internal/custom_toolchain.bzl b/xcodeproj/internal/custom_toolchain.bzl index a357576ba..0aa91b4e0 100644 --- a/xcodeproj/internal/custom_toolchain.bzl +++ b/xcodeproj/internal/custom_toolchain.bzl @@ -1,5 +1,7 @@ """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: @@ -21,31 +23,39 @@ def _custom_toolchain_impl(ctx): ) toolchain_name_base = ctx.attr.toolchain_name - toolchain_dir = ctx.actions.declare_directory( - toolchain_name_base + "{}".format(xcode_version) + ".xctoolchain", - ) + toolchain_id = "com.rules_xcodeproj.{}.{}".format(toolchain_name_base, xcode_version) + full_toolchain_name = "{}{}".format(toolchain_name_base, xcode_version) + toolchain_dir = ctx.actions.declare_directory(full_toolchain_name + ".xctoolchain") resolved_overrides = {} override_files = [] - for tool_target, tool_name in ctx.attr.overrides.items(): - files = tool_target.files.to_list() + # 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 for '{}' does not produce any files!".format(tool_name)) + fail("ERROR: Override stub does not produce any files!") if len(files) > 1: - fail("ERROR: Override for '{}' produces multiple files ({}). Each override must have exactly one file.".format( - tool_name, + fail("ERROR: Override stub produces multiple files ({}). Each stub must have exactly one file.".format( len(files), )) - override_file = files[0] - override_files.append(override_file) - resolved_overrides[tool_name] = override_file.path + 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 overrides_list = " ".join(["{}={}".format(k, v) for k, v in resolved_overrides.items()]) - script_file = ctx.actions.declare_file(toolchain_name_base + "_setup.sh") + script_file = ctx.actions.declare_file(full_toolchain_name + "_setup.sh") ctx.actions.expand_template( template = ctx.file._symlink_template, @@ -54,7 +64,8 @@ def _custom_toolchain_impl(ctx): substitutions = { "%overrides_list%": overrides_list, "%toolchain_dir%": toolchain_dir.path, - "%toolchain_name_base%": toolchain_name_base, + "%toolchain_id%": toolchain_id, + "%toolchain_name_base%": full_toolchain_name, "%xcode_version%": xcode_version, }, ) @@ -74,21 +85,28 @@ def _custom_toolchain_impl(ctx): use_default_shell_env = True, ) - # Create runfiles with the override files and script file runfiles = ctx.runfiles(files = override_files + [script_file]) - return [DefaultInfo( - files = depset([toolchain_dir]), - runfiles = runfiles, - )] + toolchain_provider = ToolchainInfo( + name = full_toolchain_name, + identifier = toolchain_id, + ) + + return [ + DefaultInfo( + files = depset([toolchain_dir]), + runfiles = runfiles, + ), + toolchain_provider, + ] custom_toolchain = rule( implementation = _custom_toolchain_impl, attrs = { "overrides": attr.label_keyed_string_dict( allow_files = True, - mandatory = False, - default = {}, + 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), "_symlink_template": attr.label( 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_symlink.sh b/xcodeproj/internal/templates/custom_toolchain_symlink.sh index 5f27577ca..54720525a 100644 --- a/xcodeproj/internal/templates/custom_toolchain_symlink.sh +++ b/xcodeproj/internal/templates/custom_toolchain_symlink.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -euo pipefail # Define constants within the script TOOLCHAIN_NAME_BASE="%toolchain_name_base%" @@ -17,6 +17,10 @@ BUILT_TOOLCHAIN_PATH="$PWD/$TOOLCHAIN_DIR" mkdir -p "$TOOLCHAIN_DIR" +# Parse overrides into a file for safer processing +OVERRIDES_FILE=$(mktemp) +echo "%overrides_list%" > "$OVERRIDES_FILE" + # Process all files from the default toolchain find "$DEFAULT_TOOLCHAIN" -type f -o -type l | while read -r file; do base_name="$(basename "$file")" @@ -30,27 +34,40 @@ find "$DEFAULT_TOOLCHAIN" -type f -o -type l | while read -r file; do # Ensure parent directory exists mkdir -p "$TOOLCHAIN_DIR/$(dirname "$rel_path")" - # Process overrides + # Check if this file has an override override_found=false - while IFS='=' read -r key value; do - if [[ "$key" == "$base_name" ]]; then - value="$PWD/$value" - cp "$value" "$TOOLCHAIN_DIR/$rel_path" - # Make executable if original is executable - if [[ -x "$file" ]]; then - chmod +x "$TOOLCHAIN_DIR/$rel_path" - fi + override_value="" + + for override in $(cat "$OVERRIDES_FILE"); do + KEY="${override%%=*}" + VALUE="${override#*=}" + + if [[ "$KEY" == "$base_name" ]]; then + override_value="$VALUE" override_found=true break fi - done <<< "%overrides_list%" + done - # If no override found, symlink the original - if [[ "$override_found" == "false" ]]; then + # Apply the override or create symlink + if [[ "$override_found" == "true" ]]; then + # Make path absolute + override_path="$PWD/$override_value" + cp "$override_path" "$TOOLCHAIN_DIR/$rel_path" + + # Make executable if original is executable + if [[ -x "$file" ]]; then + chmod +x "$TOOLCHAIN_DIR/$rel_path" + fi + else + # If no override found, symlink the original ln -sf "$file" "$TOOLCHAIN_DIR/$rel_path" fi done +# Clean up +rm -f "$OVERRIDES_FILE" + # Generate the ToolchainInfo.plist directly with Xcode version information cat > "$TOOLCHAIN_DIR/ToolchainInfo.plist" << EOF From 854e37eae6cc9550a5c74da2e804240a2bc6dfce Mon Sep 17 00:00:00 2001 From: Karim Alweheshy Date: Sun, 23 Mar 2025 21:19:06 +0100 Subject: [PATCH 3/4] make rule incremental to avoid expensive calculation --- xcodeproj/internal/custom_toolchain.bzl | 75 +++++++++++++++---- .../templates/custom_toolchain_override.sh | 64 ++++++++++++++++ .../templates/custom_toolchain_symlink.sh | 66 ++++++---------- 3 files changed, 146 insertions(+), 59 deletions(-) create mode 100644 xcodeproj/internal/templates/custom_toolchain_override.sh diff --git a/xcodeproj/internal/custom_toolchain.bzl b/xcodeproj/internal/custom_toolchain.bzl index 0aa91b4e0..39b4c378f 100644 --- a/xcodeproj/internal/custom_toolchain.bzl +++ b/xcodeproj/internal/custom_toolchain.bzl @@ -10,10 +10,10 @@ def _get_xcode_product_version(*, xcode_config): """) version_components = raw_version.split(".") - if len(version_components) < 4: - # This will result in analysis cache misses, but it's better than - # failing - return raw_version + if len(version_components) != 4: + fail("""\ +`xcode_config.xcode_version` returned an unexpected number of components: {} +""".format(len(version_components))) return version_components[3] @@ -25,7 +25,10 @@ def _custom_toolchain_impl(ctx): 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) - toolchain_dir = ctx.actions.declare_directory(full_toolchain_name + ".xctoolchain") + + # 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 = [] @@ -53,29 +56,36 @@ def _custom_toolchain_impl(ctx): 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()]) - script_file = ctx.actions.declare_file(full_toolchain_name + "_setup.sh") + 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 = script_file, + output = symlink_script_file, is_executable = True, substitutions = { - "%overrides_list%": overrides_list, - "%toolchain_dir%": toolchain_dir.path, + "%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 = [toolchain_dir], - inputs = override_files, - tools = [script_file], - mnemonic = "CreateCustomToolchain", - command = script_file.path, + outputs = [symlink_toolchain_dir], + tools = [symlink_script_file], + mnemonic = "CreateSymlinkToolchain", + command = symlink_script_file.path, execution_requirements = { "local": "1", "no-cache": "1", @@ -85,7 +95,36 @@ def _custom_toolchain_impl(ctx): use_default_shell_env = True, ) - runfiles = ctx.runfiles(files = override_files + [script_file]) + 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, @@ -94,7 +133,7 @@ def _custom_toolchain_impl(ctx): return [ DefaultInfo( - files = depset([toolchain_dir]), + files = depset([final_toolchain_dir if override_files else symlink_toolchain_dir]), runfiles = runfiles, ), toolchain_provider, @@ -109,6 +148,10 @@ custom_toolchain = rule( 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"), diff --git a/xcodeproj/internal/templates/custom_toolchain_override.sh b/xcodeproj/internal/templates/custom_toolchain_override.sh new file mode 100644 index 000000000..0dd3da469 --- /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" -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 index 54720525a..e8fc32230 100644 --- a/xcodeproj/internal/templates/custom_toolchain_symlink.sh +++ b/xcodeproj/internal/templates/custom_toolchain_symlink.sh @@ -6,67 +6,50 @@ TOOLCHAIN_NAME_BASE="%toolchain_name_base%" TOOLCHAIN_DIR="%toolchain_dir%" XCODE_VERSION="%xcode_version%" +# Store the list of tools that will be overridden +TOOL_NAMES_FILE=$(mktemp) +echo "%tool_names_list%" > "$TOOL_NAMES_FILE" + # 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) -# Define toolchain names +# Define toolchain names for reference only 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" -# Parse overrides into a file for safer processing -OVERRIDES_FILE=$(mktemp) -echo "%overrides_list%" > "$OVERRIDES_FILE" - # Process all files from the default toolchain find "$DEFAULT_TOOLCHAIN" -type f -o -type l | while read -r file; do - base_name="$(basename "$file")" 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 - # Ensure parent directory exists - mkdir -p "$TOOLCHAIN_DIR/$(dirname "$rel_path")" - - # Check if this file has an override - override_found=false - override_value="" - - for override in $(cat "$OVERRIDES_FILE"); do - KEY="${override%%=*}" - VALUE="${override#*=}" - - if [[ "$KEY" == "$base_name" ]]; then - override_value="$VALUE" - override_found=true + # 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 + echo "Skipping symlink for tool to be overridden: $base_name" + should_skip=1 break fi done - # Apply the override or create symlink - if [[ "$override_found" == "true" ]]; then - # Make path absolute - override_path="$PWD/$override_value" - cp "$override_path" "$TOOLCHAIN_DIR/$rel_path" - - # Make executable if original is executable - if [[ -x "$file" ]]; then - chmod +x "$TOOLCHAIN_DIR/$rel_path" - fi - else - # If no override found, symlink the original - ln -sf "$file" "$TOOLCHAIN_DIR/$rel_path" + if [[ $should_skip -eq 1 ]]; then + continue fi -done -# Clean up -rm -f "$OVERRIDES_FILE" + # 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 @@ -96,8 +79,5 @@ cat > "$TOOLCHAIN_DIR/ToolchainInfo.plist" << EOF 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" +# Clean up +rm -f "$TOOL_NAMES_FILE" From 93e2f56cbba9dbadc23000554f04ab2c287bdc5f Mon Sep 17 00:00:00 2001 From: Karim Alweheshy Date: Sun, 23 Mar 2025 23:45:15 +0100 Subject: [PATCH 4/4] fix copying the overrides correctly --- .../templates/custom_toolchain_override.sh | 2 +- .../templates/custom_toolchain_symlink.sh | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/xcodeproj/internal/templates/custom_toolchain_override.sh b/xcodeproj/internal/templates/custom_toolchain_override.sh index 0dd3da469..f8e1a62b8 100644 --- a/xcodeproj/internal/templates/custom_toolchain_override.sh +++ b/xcodeproj/internal/templates/custom_toolchain_override.sh @@ -30,7 +30,7 @@ for tool_name in $(cat "$TOOL_NAMES_FILE"); do continue fi - find "$DEFAULT_TOOLCHAIN" -name "$tool_name" | while read -r default_tool_path; do + 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" diff --git a/xcodeproj/internal/templates/custom_toolchain_symlink.sh b/xcodeproj/internal/templates/custom_toolchain_symlink.sh index e8fc32230..611a61bc9 100644 --- a/xcodeproj/internal/templates/custom_toolchain_symlink.sh +++ b/xcodeproj/internal/templates/custom_toolchain_symlink.sh @@ -6,16 +6,16 @@ TOOLCHAIN_NAME_BASE="%toolchain_name_base%" TOOLCHAIN_DIR="%toolchain_dir%" XCODE_VERSION="%xcode_version%" -# Store the list of tools that will be overridden -TOOL_NAMES_FILE=$(mktemp) -echo "%tool_names_list%" > "$TOOL_NAMES_FILE" - # 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) -# Define toolchain names for reference only +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" @@ -34,7 +34,6 @@ find "$DEFAULT_TOOLCHAIN" -type f -o -type l | while read -r file; do for tool_name in $(cat "$TOOL_NAMES_FILE"); do if [[ "$base_name" == "$tool_name" ]]; then # Skip creating a symlink for overridden tools - echo "Skipping symlink for tool to be overridden: $base_name" should_skip=1 break fi @@ -79,5 +78,8 @@ cat > "$TOOLCHAIN_DIR/ToolchainInfo.plist" << EOF EOF -# Clean up -rm -f "$TOOL_NAMES_FILE" +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"