Skip to content

[Exploration] Add MSBuild properties to override native tool paths (clang, install_name_tool, etc.) — enabling builds without Xcode #24855

@Redth

Description

@Redth

Summary

This is an exploratory investigation into whether .NET iOS builds could work without a full Xcode installation — specifically by allowing developers to override the paths to key native tools (clang, clang++, install_name_tool, etc.) via MSBuild properties.

The motivation comes from tools like XTool which can extract the iOS SDK, clang/lld toolchain, and signing infrastructure from Xcode.xip and run them on Linux and Windows — enabling Swift/SwiftPM iOS builds without Xcode. If dotnet/macios supported overriding tool paths, it could open the door to cross-platform .NET iOS builds.

Investigation Findings

The xcrun Bottleneck

.NET iOS builds invoke all Apple tools via xcrun:

Tool Invocation Purpose
clang xcrun clang Compile native code (.mm, .s files)
clang++ xcrun clang++ Link final executable with iOS frameworks
actool xcrun actool Compile .xcassets (icons, images)
ibtool xcrun ibtool Compile storyboards
derq xcrun derq Process entitlements
install_name_tool xcrun install_name_tool Fix dylib install names
codesign /usr/bin/codesign Sign binaries

xcrun internally calls xcodebuild to locate tools and validate the developer directory. This creates a hard dependency on a full Xcode installation — even symlinking a complete Xcode.app to another path causes xcodebuild to fail because it validates its canonical location.

The Existing Pattern: GetExecutable() Already Supports Overrides

The codebase already has the right abstraction in XamarinTask.cs:

// XamarinTask.cs — already exists
protected static string GetExecutable(List<string> arguments, string toolName, string? toolPathOverride)
{
    if (string.IsNullOrEmpty(toolPathOverride)) {
        arguments.Insert(0, toolName);
        return "xcrun";  // default: use xcrun to resolve tool
    }
    return toolPathOverride;  // override: call tool directly, bypassing xcrun
}

actool and ibtool already use this pattern via XcodeCompilerToolTask.ToolPath — when $(ACToolPath) or $(IBToolPath) are set, they bypass xcrun entirely and call the tool directly.

What's Missing: Three Tasks Hardcode xcrun

Three task files skip the GetExecutable() pattern and hardcode xcrun:

  1. CompileNativeCode.cs — hardcodes arguments.Add("clang") then ExecuteAsync("xcrun", arguments)
  2. LinkNativeCode.cs — hardcodes arguments.Add("clang++") then ExecuteAsync("xcrun", arguments), plus hardcodes "derq" the same way
  3. InstallNameTool.cs — hardcodes arguments.Add("install_name_tool") then ExecuteAsync("xcrun", arguments)

Additionally: DetectSdkLocations Requires Xcode

DetectSdkLocations calls AppleSdkSettings.Init() which validates a full Xcode.app installation (checks Info.plist, version.plist, platform directories, etc.). There's no way to bypass this validation when tool paths are provided directly.

Proposed Changes (~50 lines across 6 files)

1. Add tool path override properties to tasks that hardcode xcrun

Apply the existing GetExecutable() pattern consistently:

CompileNativeCode.cs — add ClangPath property:

public string ClangPath { get; set; } = "";

// Replace: arguments.Add("clang"); ... ExecuteAsync("xcrun", arguments)
// With:
var executable = GetExecutable(arguments, "clang", ClangPath);
processes[i] = ExecuteAsync(executable, arguments);

LinkNativeCode.cs — add ClangPlusPlusPath and DerqPath properties:

public string ClangPlusPlusPath { get; set; } = "";
public string DerqPath { get; set; } = "";

// Linking — replace hardcoded xcrun with:
var executable = GetExecutable(arguments, "clang++", ClangPlusPlusPath);

// Entitlements — replace hardcoded xcrun with:
var executable = GetExecutable(arguments, "derq", DerqPath);

InstallNameTool.cs — add InstallNameToolPath property:

public string InstallNameToolPath { get; set; } = "";

var executable = GetExecutable(arguments, "install_name_tool", InstallNameToolPath);

2. Wire properties in Xamarin.Shared.targets

<PropertyGroup>
  <ClangPath Condition="'$(ClangPath)' == ''"></ClangPath>
  <ClangPlusPlusPath Condition="'$(ClangPlusPlusPath)' == ''"></ClangPlusPlusPath>
  <DerqPath Condition="'$(DerqPath)' == ''"></DerqPath>
  <InstallNameToolPath Condition="'$(InstallNameToolPath)' == ''"></InstallNameToolPath>
</PropertyGroup>

Pass them to the task invocations as attributes.

3. Add SkipXcodeValidation to DetectSdkLocations

Allow bypassing the Xcode.app validation when tool paths are provided directly:

public bool SkipXcodeValidation { get; set; }

When true, skip AppleSdkSettings.Init() and the EnsureAppleSdkRoot() check, allowing the build to proceed with explicitly provided SDK paths.

Complete Property Summary

Property Default Purpose Status
ACToolPath (xcrun actool) Path to actool binary ✅ Already exists
IBToolPath (xcrun ibtool) Path to ibtool binary ✅ Already exists
StripPath (xcrun strip) Path to strip binary ✅ Already exists
ClangPath (xcrun clang) Path to clang binary New
ClangPlusPlusPath (xcrun clang++) Path to clang++ binary New
DerqPath (xcrun derq) Path to derq binary New
InstallNameToolPath (xcrun install_name_tool) Path to install_name_tool New
CodesignPath (/usr/bin/codesign) Path to codesign binary New
SkipXcodeValidation false Skip Xcode.app validation New

How This Would Enable Cross-Platform Builds

With these changes, a developer could build a .NET iOS app using alternative toolchains:

<PropertyGroup>
  <SkipXcodeValidation>true</SkipXcodeValidation>
  <ClangPath>~/.xtool/sdk/XcodeDefault.xctoolchain/usr/bin/clang</ClangPath>
  <ClangPlusPlusPath>~/.xtool/sdk/XcodeDefault.xctoolchain/usr/bin/clang++</ClangPlusPlusPath>
  <InstallNameToolPath>~/.xtool/sdk/XcodeDefault.xctoolchain/usr/bin/install_name_tool</InstallNameToolPath>
</PropertyGroup>

What This Doesn't Solve (Future Work)

These changes are intentionally minimal — they make tool paths overridable without changing default behavior. Remaining challenges for full cross-platform builds include:

  • actool on Linux — needs a cross-platform asset catalog compiler (or apps that avoid .xcassets)
  • ibtool on Linux — needs storyboard compilation alternative (or apps that avoid storyboards)
  • codesign on Linux — tools like xtool have their own signing, would need a bridge
  • iOS workload installation on Linux — separate issue from the build toolchain
  • Cross-compilation sysroot — the extracted iOS SDK would need to be provided as a sysroot flag

Key Observation

The proposed changes are backward-compatible — when none of the new properties are set, behavior is identical to today (everything goes through xcrun). This is a low-risk change that simply makes the existing GetExecutable() pattern consistent across all tool-invoking tasks.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementThe issue or pull request is an enhancementmsbuildIssues affecting our msbuild tasks/targets

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions