dotnet / dotnet/macios

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

Open
#24,855 1 comment 0 reactions 0 assignees View on GitHub
enhancement msbuild
Dominant language
C#
Stars
2.9k
Forks
576
Avg merge
2d 12h
Merged PRs (30d)
123

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](https://github.com/xtool-org/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`:

```csharp
// XamarinTask.cs — already exists
protected static string GetExecutable(List 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:
```csharp
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:
```csharp
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:
```csharp
public string InstallNameToolPath { get; set; } = "";

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

### 2. Wire properties in `Xamarin.Shared.targets`

```xml




```

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:

```csharp
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:

```xml

true
~/.xtool/sdk/XcodeDefault.xctoolchain/usr/bin/clang
~/.xtool/sdk/XcodeDefault.xctoolchain/usr/bin/clang++
~/.xtool/sdk/XcodeDefault.xctoolchain/usr/bin/install_name_tool

```

## 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.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.