xamarin-macios/tests/xharness/ProjectFileExtensions.cs

218 строки
8.5 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using Microsoft.DotNet.XHarness.iOS.Shared.Utilities;
using Xamarin.Utils;
#nullable enable
namespace Xharness {
public static class EvolvedProjectFileExtensions {
// Evaluates a text and replaces '$(Variable)' with the property value specified in the 'properties' dictionary.
// Contrary to what MSBuild does, if the variable can't be found in the dictionary, it's not replaced with
// an empty string, instead the variable reference stays as-is.
public static string EvaluateAsMSBuildText (this string text, Dictionary<string, string> properties)
{
if (text.Length < 4)
return text;
if (text.IndexOf ('$') == -1)
return text;
var sb = new StringBuilder ();
for (var i = 0; i < text.Length; i++) {
if (text [i] != '$' || i + 2 >= text.Length) {
sb.Append (text [i]);
continue;
}
if (text [i + 1] != '(') {
sb.Append ('$');
sb.Append (text [i + 1]);
continue;
}
var endParenthesis = text.IndexOf (')', i + 2);
if (endParenthesis == -1)
continue;
var variable = text.Substring (i + 2, endParenthesis - i - 2);
if (properties.TryGetValue (variable, out var value)) {
sb.Append (value);
} else {
// Put back the variable reference
sb.Append ('$');
sb.Append ('(');
sb.Append (variable);
sb.Append (')');
}
i = endParenthesis;
}
return sb.ToString ();
}
// Collect all top-level properties in a csproj. They're evaluated as they're found, so that multiple properties concatenating
// strings work correctly. The 'forcedProperties' dictionary contains properties that are hardcoded and should be returned as
// the dictionary says.
public static Dictionary<string, string> CollectAndEvaluateTopLevelProperties (this XmlDocument doc, Dictionary<string, string> forcedProperties)
{
var collectedProperties = new Dictionary<string, string> (forcedProperties);
var properties = doc.SelectNodes ("/Project/*[local-name() = 'PropertyGroup' and not(@Condition)]//*");
foreach (XmlNode prop in properties) {
if (!forcedProperties.ContainsKey (prop.Name))
collectedProperties [prop.Name] = prop.InnerText.EvaluateAsMSBuildText (collectedProperties);
}
return collectedProperties;
}
static IEnumerable<XmlNode> SelectElementNodes (this XmlNode node, string name)
{
foreach (XmlNode? child in node.ChildNodes) {
if (child == null)
continue;
if (child.NodeType == XmlNodeType.Element && string.Equals (child.Name, name, StringComparison.OrdinalIgnoreCase))
yield return child;
if (!child.HasChildNodes)
continue;
foreach (XmlNode descendent in child.SelectElementNodes (name))
yield return descendent;
}
}
// This is an evolved version of https://github.com/dotnet/xharness/blob/b2297d610df1ae15fc7ba8bd8c9bc0a7192aaefa/src/Microsoft.DotNet.XHarness.iOS.Shared/Utilities/ProjectFileExtensions.cs#L1168
public static void ResolveAllPaths (this XmlDocument csproj, string project_path, Dictionary<string, string>? variableSubstitution = null)
{
var dir = Path.GetDirectoryName (project_path)!;
var nodes_with_paths = new string []
{
"AssemblyOriginatorKeyFile",
"CodesignEntitlements",
"TestLibrariesDirectory",
"HintPath",
"RootTestsDirectory",
};
var attributes_with_paths = new []
{
new { Element = "None", Attribute = "Include", SkipLogicalName = false, },
new { Element = "Compile", Attribute = "Include", SkipLogicalName = false, },
new { Element = "Compile", Attribute = "Exclude", SkipLogicalName = false, },
new { Element = "ProjectReference", Attribute = "Include", SkipLogicalName = true, },
new { Element = "InterfaceDefinition", Attribute = "Include", SkipLogicalName = false, },
new { Element = "BundleResource", Attribute = "Include", SkipLogicalName = false, },
new { Element = "EmbeddedResource", Attribute = "Include", SkipLogicalName = false, },
new { Element = "ImageAsset", Attribute = "Include", SkipLogicalName = false, },
new { Element = "GeneratedTestInput", Attribute = "Include", SkipLogicalName = false, },
new { Element = "GeneratedTestOutput", Attribute = "Include", SkipLogicalName = false, },
new { Element = "TestLibrariesInput", Attribute = "Include", SkipLogicalName = false, },
new { Element = "TestLibrariesOutput", Attribute = "Include", SkipLogicalName = false, },
new { Element = "Content", Attribute = "Include", SkipLogicalName = false, },
new { Element = "ObjcBindingApiDefinition", Attribute = "Include", SkipLogicalName = false, },
new { Element = "ObjcBindingCoreSource", Attribute = "Include", SkipLogicalName = false, },
new { Element = "ObjcBindingNativeLibrary", Attribute = "Include", SkipLogicalName = false, },
new { Element = "ObjcBindingNativeFramework", Attribute = "Include", SkipLogicalName = false, },
new { Element = "Import", Attribute = "Project", SkipLogicalName = true, },
new { Element = "FilesToCopy", Attribute = "Include", SkipLogicalName = false, },
new { Element = "FilesToCopyFoo", Attribute = "Include", SkipLogicalName = false, },
new { Element = "FilesToCopyFooBar", Attribute = "Include", SkipLogicalName = false, },
new { Element = "FilesToCopyEncryptedXml", Attribute = "Include", SkipLogicalName = false, },
new { Element = "FilesToCopyCryptographyPkcs", Attribute = "Include", SkipLogicalName = false, },
new { Element = "FilesToCopyResources", Attribute = "Include", SkipLogicalName = false, },
new { Element = "FilesToCopyXMLFiles", Attribute = "Include", SkipLogicalName = false, },
new { Element = "FilesToCopyChannels", Attribute = "Include", SkipLogicalName = false, },
new { Element = "CustomMetalSmeltingInput", Attribute = "Include", SkipLogicalName = false, },
new { Element = "Metal", Attribute = "Include", SkipLogicalName = false, },
new { Element = "NativeReference", Attribute = "Include", SkipLogicalName = false, },
};
var nodes_with_variables = new string []
{
"MtouchExtraArgs",
};
Func<string, string>? convert = null;
convert = (input) => {
if (input.Contains (';')) {
var split = input.Split (new char [] { ';' }, StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < split.Length; i++)
split [i] = convert!.Invoke (split [i]);
return string.Join (";", split);
}
if (variableSubstitution != null)
input = input.EvaluateAsMSBuildText (variableSubstitution);
var makeFullPath = input [0] != '$';
if (makeFullPath) {
input = input.Replace ('\\', '/'); // make unix-style
input = Path.GetFullPath (Path.Combine (dir, input));
input = input.Replace ('/', '\\'); // make windows-style again
}
return input;
};
foreach (var key in nodes_with_paths) {
var nodes = csproj.SelectElementNodes (key);
foreach (var node in nodes)
node.InnerText = convert (node.InnerText);
}
foreach (var key in nodes_with_variables) {
var nodes = csproj.SelectElementNodes (key);
foreach (var node in nodes)
node.InnerText = node.InnerText.Replace ("${ProjectDir}", StringUtils.Quote (Path.GetDirectoryName (project_path)));
}
foreach (var kvp in attributes_with_paths) {
var element = kvp.Element;
var attrib = kvp.Attribute;
var skipLogicalName = kvp.SkipLogicalName;
var nodes = csproj.SelectElementNodes (element);
foreach (XmlNode node in nodes) {
var a = node.Attributes [attrib];
if (a == null)
continue;
// Fix any default LogicalName values (but don't change existing ones).
if (!skipLogicalName) {
var ln = node.SelectElementNodes ("LogicalName")?.SingleOrDefault ();
var links = node.SelectElementNodes ("Link");
if (ln == null && !links.Any ()) {
ln = csproj.CreateElement ("LogicalName", csproj.GetNamespace ());
node.AppendChild (ln);
string logicalName = a.Value;
switch (element) {
case "BundleResource":
if (logicalName.StartsWith ("Resources\\", StringComparison.Ordinal))
logicalName = logicalName.Substring ("Resources\\".Length);
break;
default:
break;
}
ln.InnerText = logicalName;
}
}
a.Value = convert (a.Value);
}
}
}
}
}