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:
CompileNativeCode.cs — hardcodes arguments.Add("clang") then ExecuteAsync("xcrun", arguments)
LinkNativeCode.cs — hardcodes arguments.Add("clang++") then ExecuteAsync("xcrun", arguments), plus hardcodes "derq" the same way
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.
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.xipand run them on Linux and Windows — enabling Swift/SwiftPM iOS builds without Xcode. Ifdotnet/maciossupported 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:xcrun clangxcrun clang++xcrun actoolxcrun ibtoolxcrun derqxcrun install_name_tool/usr/bin/codesignxcruninternally callsxcodebuildto 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 causesxcodebuildto fail because it validates its canonical location.The Existing Pattern:
GetExecutable()Already Supports OverridesThe codebase already has the right abstraction in
XamarinTask.cs:actoolandibtoolalready use this pattern viaXcodeCompilerToolTask.ToolPath— when$(ACToolPath)or$(IBToolPath)are set, they bypassxcrunentirely and call the tool directly.What's Missing: Three Tasks Hardcode
xcrunThree task files skip the
GetExecutable()pattern and hardcodexcrun:CompileNativeCode.cs— hardcodesarguments.Add("clang")thenExecuteAsync("xcrun", arguments)LinkNativeCode.cs— hardcodesarguments.Add("clang++")thenExecuteAsync("xcrun", arguments), plus hardcodes"derq"the same wayInstallNameTool.cs— hardcodesarguments.Add("install_name_tool")thenExecuteAsync("xcrun", arguments)Additionally:
DetectSdkLocationsRequires XcodeDetectSdkLocationscallsAppleSdkSettings.Init()which validates a full Xcode.app installation (checksInfo.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
xcrunApply the existing
GetExecutable()pattern consistently:CompileNativeCode.cs— addClangPathproperty:LinkNativeCode.cs— addClangPlusPlusPathandDerqPathproperties:InstallNameTool.cs— addInstallNameToolPathproperty:2. Wire properties in
Xamarin.Shared.targetsPass them to the task invocations as attributes.
3. Add
SkipXcodeValidationtoDetectSdkLocationsAllow bypassing the Xcode.app validation when tool paths are provided directly:
When
true, skipAppleSdkSettings.Init()and theEnsureAppleSdkRoot()check, allowing the build to proceed with explicitly provided SDK paths.Complete Property Summary
ACToolPathIBToolPathStripPathClangPathClangPlusPlusPathDerqPathInstallNameToolPathCodesignPathSkipXcodeValidationfalseHow This Would Enable Cross-Platform Builds
With these changes, a developer could build a .NET iOS app using alternative toolchains:
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:
actoolon Linux — needs a cross-platform asset catalog compiler (or apps that avoid.xcassets)ibtoolon Linux — needs storyboard compilation alternative (or apps that avoid storyboards)codesignon Linux — tools like xtool have their own signing, would need a bridgeKey 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 existingGetExecutable()pattern consistent across all tool-invoking tasks.