-
Notifications
You must be signed in to change notification settings - Fork 559
Description
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— 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: 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:
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 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.