xamarin-macios/tests/xharness/AppBundleLocator.cs

189 строки
6.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.DotNet.XHarness.Common.Execution;
using Microsoft.DotNet.XHarness.Common.Logging;
using Microsoft.DotNet.XHarness.Common.Utilities;
using Microsoft.DotNet.XHarness.iOS.Shared;
using Microsoft.DotNet.XHarness.iOS.Shared.Utilities;
namespace Xharness {
public class AppBundleLocator : IAppBundleLocator {
readonly IProcessManager processManager;
readonly Func<ILog> getLog;
readonly string msBuildPath;
readonly string systemDotnetPath;
readonly string dotnetPath;
// Gets either the system .NET or DOTNET variable, depending on any global.json
// config file found in the specified directory or any containing directories.
readonly Dictionary<string, string> dotnet_executables = new Dictionary<string, string> ();
public AppBundleLocator (IProcessManager processManager, Func<ILog> getLog, string msBuildPath, string systemDotnetPath, string dotnetPath)
{
this.processManager = processManager;
this.getLog = getLog;
this.msBuildPath = msBuildPath;
this.systemDotnetPath = systemDotnetPath;
this.dotnetPath = dotnetPath;
}
public async Task<string> LocateAppBundle (XmlDocument projectFile, string projectFilePath, TestTarget target, string buildConfiguration)
{
string platform = string.Empty;
if (target != TestTarget.None)
platform = target.IsSimulator () ? "iPhoneSimulator" : "iPhone";
if (projectFile.IsDotNetProject ()) {
var properties = new Dictionary<string, string> {
{ "Configuration", buildConfiguration },
};
if (!string.IsNullOrEmpty (platform))
properties ["Platform"] = platform;
return await GetPropertyByMSBuildEvaluationAsync (projectFile, projectFilePath, "OutputPath", "_GenerateBundleName", properties);
} else {
return projectFile.GetOutputPath (platform, buildConfiguration).Replace ('\\', Path.DirectorySeparatorChar);
}
}
// Retrieves a property from an MSBuild project file by executing MSBuild and getting MSBuild to print the property.
// We do this by creating a custom MSBuild file which:
// * Will import the project file we're inspecting
// * Has a target that will print a given property
// and then executing MSBuild on this custom MSBuild file.
private async Task<string> GetPropertyByMSBuildEvaluationAsync (XmlDocument csproj, string projectPath, string evaluateProperty, string dependsOnTargets = "", Dictionary<string, string> properties = null)
{
var xml =
@"<Project DefaultTargets='WriteProperty' xmlns='http://schemas.microsoft.com/developer/msbuild/2003'>
<!-- Import the project we want to inspect -->
<Import Project='$(ProjectFile)' Condition=""'$(ProjectFile)' != ''"" />
<!-- Target to write out the property we want -->
<Target Name='WriteProperty' DependsOnTargets='%DEPENDSONTARGETS%'>
<PropertyGroup>
<_Properties>$(%PROPERTY%)</_Properties>
</PropertyGroup>
<Error Text='The ProjectFile variable must be set.' Condition=""'$(ProjectFile)' == ''"" />
<Error Text='The OutputFile variable must be set.' Condition=""'$(OutputFile)' == ''"" />
<WriteLinesToFile File='$(OutputFile)' Lines='$(_Properties)' Overwrite='true' />
</Target>
</Project>
";
var dir = Path.GetDirectoryName (projectPath);
var inspector = Path.Combine (dir, "PropertyInspector.csproj");
var output = Path.Combine (dir, "PropertyInspector.txt");
try {
File.WriteAllText (inspector, xml.Replace ("%PROPERTY%", evaluateProperty).Replace ("%DEPENDSONTARGETS%", dependsOnTargets));
using (var proc = new Process ()) {
var isDotNetProject = csproj.IsDotNetProject ();
proc.StartInfo.FileName = isDotNetProject ? GetDotNetExecutable (projectPath) : msBuildPath;
var args = new List<string> ();
if (isDotNetProject)
args.Add ("build");
args.Add ("/p:ProjectFile=" + projectPath);
args.Add ("/p:OutputFile=" + output);
foreach (var prop in properties)
args.Add ($"/p:{prop.Key}={prop.Value}");
args.Add (inspector);
var env = new Dictionary<string, string> {
{ "MSBUILD_EXE_PATH", null },
};
proc.StartInfo.Arguments = StringUtils.FormatArguments (args);
proc.StartInfo.WorkingDirectory = dir;
// Don't evaluate in parallel on multiple threads to avoid overloading the mac.
var acquired = await evaluate_semaphore.WaitAsync (TimeSpan.FromMinutes (5));
try {
var log = getLog () ?? new ConsoleLog ();
var memoryLog = new MemoryLog ();
var aggregated = Log.CreateAggregatedLog (memoryLog, log);
if (!acquired)
aggregated.WriteLine ("Unable to acquire lock to evaluate MSBuild property in 5 minutes; will try to evaluate anyway.");
var rv = await processManager.RunAsync (proc, aggregated, environmentVariables: env, timeout: TimeSpan.FromMinutes (5));
if (!rv.Succeeded) {
var msg = $"Unable to evaluate the property {evaluateProperty} in {projectPath}, build failed with exit code {rv.ExitCode}. Timed out: {rv.TimedOut}";
Console.WriteLine (msg + " Output: \n" + memoryLog.ToString ());
throw new Exception (msg);
}
} finally {
if (acquired)
evaluate_semaphore.Release ();
}
return File.ReadAllText (output).Trim ();
}
} finally {
File.Delete (inspector);
File.Delete (output);
}
}
SemaphoreSlim evaluate_semaphore = new SemaphoreSlim (1);
public string GetDotNetExecutable (string directory)
{
if (directory is null)
throw new ArgumentNullException (nameof (directory));
lock (dotnet_executables) {
if (dotnet_executables.TryGetValue (directory, out var value))
return value;
}
// Find the first global.json up the directory hierarchy (stopping at the root directory)
string global_json = null;
var dir = directory;
while (dir.Length > 2) {
global_json = Path.Combine (dir, "global.json");
if (File.Exists (global_json))
break;
dir = Path.GetDirectoryName (dir);
}
if (!File.Exists (global_json))
throw new Exception ($"Could not find any global.json file in {directory} or above");
// Parse the global.json we found, and figure out if it tells us to use .NET 3.1.100 / 5.X.XXX or not.
var contents = File.ReadAllBytes (global_json);
using var reader = JsonReaderWriterFactory.CreateJsonReader (contents, new XmlDictionaryReaderQuotas ());
var doc = new XmlDocument ();
doc.Load (reader);
var version = doc.SelectSingleNode ("/root/sdk").InnerText;
string executable;
switch (version [0]) {
case '3':
case '5':
executable = systemDotnetPath;
break;
default:
executable = dotnetPath;
break;
}
getLog ()?.WriteLine ($"Mapped .NET SDK version {version} to {executable} for {directory}");
lock (dotnet_executables) {
dotnet_executables [directory] = executable;
}
return executable;
}
}
}