Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions Xamarin.MacDev/AppleInstaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using Xamarin.MacDev.Models;

#nullable enable

namespace Xamarin.MacDev {

/// <summary>
/// Orchestrates Apple development environment setup. Checks the current
/// state via <see cref="EnvironmentChecker"/> and installs missing
/// components (Command Line Tools, Xcode first-launch packages, and
/// simulator runtimes).
/// </summary>
public class AppleInstaller {

static readonly string XcodeSelectPath = "/usr/bin/xcode-select";

readonly ICustomLogger log;

public AppleInstaller (ICustomLogger log)
{
this.log = log ?? throw new ArgumentNullException (nameof (log));
}

/// <summary>
/// Ensures the Apple development environment is ready.
/// When <paramref name="dryRun"/> is true, reports what would be
/// installed without making any changes.
/// </summary>
public EnvironmentCheckResult Install (IEnumerable<string>? requestedPlatforms = null, bool dryRun = false)
{
var checker = new EnvironmentChecker (log);

log.LogInfo ("Running initial environment check...");
var result = checker.Check ();

EnsureCommandLineTools (result.CommandLineTools, dryRun);

if (result.Xcode is not null)
EnsureFirstLaunch (checker, dryRun);
else
log.LogInfo ("No Xcode found — skipping first-launch check.");

if (requestedPlatforms is not null)
EnsureRuntimes (result, requestedPlatforms, dryRun);

if (!dryRun) {
log.LogInfo ("Running final environment check...");
result = checker.Check ();
}

log.LogInfo ("Install complete. Status: {0}.", result.Status);
return result;
}

void EnsureCommandLineTools (CommandLineToolsInfo clt, bool dryRun)
{
if (clt.IsInstalled) {
log.LogInfo ("Command Line Tools already installed (v{0}).", clt.Version);
return;
}

if (dryRun) {
log.LogInfo ("[DRY RUN] Would trigger Command Line Tools installation.");
return;
}

log.LogInfo ("Command Line Tools not found. Triggering installation...");
try {
var (exitCode, _, stderr) = ProcessUtils.Exec (XcodeSelectPath, "--install");
if (exitCode == 0)
log.LogInfo ("Command Line Tools installer triggered. Complete the dialog to continue.");
else
log.LogInfo ("xcode-select --install failed (exit {0}): {1}", exitCode, stderr.Trim ());
} catch (System.ComponentModel.Win32Exception ex) {
log.LogInfo ("Could not run xcode-select: {0}", ex.Message);
} catch (InvalidOperationException ex) {
log.LogInfo ("Could not run xcode-select: {0}", ex.Message);
}
}

void EnsureFirstLaunch (EnvironmentChecker checker, bool dryRun)
{
if (dryRun) {
log.LogInfo ("[DRY RUN] Would run xcodebuild -runFirstLaunch.");
return;
}

checker.RunFirstLaunch ();
}

void EnsureRuntimes (EnvironmentCheckResult result, IEnumerable<string> requestedPlatforms, bool dryRun)
{
var available = new HashSet<string> (StringComparer.OrdinalIgnoreCase);
foreach (var rt in result.Runtimes) {
if (!string.IsNullOrEmpty (rt.Platform))
available.Add (rt.Platform);
}

var runtimeService = new RuntimeService (log);

foreach (var platform in requestedPlatforms) {
if (available.Contains (platform)) {
log.LogInfo ("Runtime for '{0}' is already available.", platform);
continue;
}

if (dryRun) {
log.LogInfo ("[DRY RUN] Would download runtime for '{0}'.", platform);
continue;
}

log.LogInfo ("Downloading runtime for '{0}'...", platform);
var success = runtimeService.DownloadPlatform (platform);
if (!success)
log.LogInfo ("Failed to download runtime for '{0}'.", platform);
}
}
}
}
5 changes: 5 additions & 0 deletions Xamarin.MacDev/CommandLineTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ public CommandLineToolsInfo Check ()
}
} catch (System.ComponentModel.Win32Exception ex) {
log.LogInfo ("Could not run xcode-select: {0}", ex.Message);
} catch (InvalidOperationException ex) {
log.LogInfo ("Could not run xcode-select: {0}", ex.Message);
}
}

Expand Down Expand Up @@ -109,6 +111,9 @@ public CommandLineToolsInfo Check ()
} catch (System.ComponentModel.Win32Exception ex) {
log.LogInfo ("Could not run pkgutil: {0}", ex.Message);
return null;
} catch (InvalidOperationException ex) {
log.LogInfo ("Could not run pkgutil: {0}", ex.Message);
return null;
}
}

Expand Down
50 changes: 50 additions & 0 deletions tests/AppleInstallerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using NUnit.Framework;
using Xamarin.MacDev;

#nullable enable

namespace tests;

[TestFixture]
public class AppleInstallerTests {

[Test]
public void Constructor_ThrowsOnNullLogger ()
{
Assert.Throws<System.ArgumentNullException> (() => new AppleInstaller (null!));
}

[Test]
[Platform ("MacOsX")]
public void Install_DryRun_DoesNotThrow ()
{
var installer = new AppleInstaller (ConsoleLogger.Instance);
Assert.DoesNotThrow (() => installer.Install (dryRun: true));
}

[Test]
[Platform ("MacOsX")]
public void Install_DryRun_ReturnsValidResult ()
{
var installer = new AppleInstaller (ConsoleLogger.Instance);
var result = installer.Install (dryRun: true);
Assert.That (result, Is.Not.Null);
Assert.That (result.Status, Is.AnyOf (
Xamarin.MacDev.Models.EnvironmentStatus.Ok,
Xamarin.MacDev.Models.EnvironmentStatus.Partial,
Xamarin.MacDev.Models.EnvironmentStatus.Missing));
}

[Test]
[Platform ("MacOsX")]
public void Install_WithPlatforms_DryRun_DoesNotThrow ()
{
var installer = new AppleInstaller (ConsoleLogger.Instance);
Assert.DoesNotThrow (() => installer.Install (
requestedPlatforms: new [] { "iOS", "macOS" },
dryRun: true));
}
}