Skip to content

Commit

Permalink
Reference handling rework.
Browse files Browse the repository at this point in the history
References are now cached in a targets file. This improves build time and should (but actually doesn't) make the referenced assemblies show up in Visual Studio Solution Explorer.
  • Loading branch information
DarkDaskin committed Apr 11, 2024
1 parent c1d83a9 commit 2e1cb13
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 93 deletions.
56 changes: 0 additions & 56 deletions UnityModStudio.Build/Tasks/FindGameFiles.cs

This file was deleted.

85 changes: 85 additions & 0 deletions UnityModStudio.Build/Tasks/ResolveGameAssemblyReferences.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using UnityModStudio.Common;

namespace UnityModStudio.Build.Tasks;

public class ResolveGameAssemblyReferences : Task
{
[Required]
public string? GamePath { get; set; }

[Required]
public string? TargetFramework { get; set; }

public ITaskItem[] ExistingReferences { get; set; } = [];

[Output]
public string? Architecture { get; private set; }

[Output]
public ITaskItem[] ReferencesToAdd { get; private set; } = [];

[Output]
public ITaskItem[] ReferencesToUpdate { get; private set; } = [];

[Output]
public ITaskItem[] ReferencesToRemove { get; private set; } = [];

public override bool Execute()
{
if (!GameInformationResolver.TryGetGameInformation(GamePath, out var gameInformation, out var error))
{
Log.LogError(error);
return false;
}

Architecture = gameInformation.Architecture.ToString();

IEnumerable<FileInfo> assemblyFiles = gameInformation.GameAssemblyFiles;
if (!(TargetFramework?.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase) ?? false))
assemblyFiles = assemblyFiles.Concat(gameInformation.FrameworkAssemblyFiles);
var resolvedReferences = assemblyFiles.ToDictionary(file => Path.GetFileNameWithoutExtension(file.Name), file => file.FullName,
StringComparer.OrdinalIgnoreCase);

var referencesToUpdate = new List<ITaskItem>();
var referencesToRemove = new List<ITaskItem>();
foreach (var reference in ExistingReferences)
{
if (resolvedReferences.TryGetValue(reference.ItemSpec, out var path))
{
reference.SetMetadata("HintPath", path);
reference.SetMetadata("Private", "false");
referencesToUpdate.Add(reference);
}
else //if (string.Equals(reference.GetMetadata("IsImplicitlyDefined"), "true", StringComparison.OrdinalIgnoreCase))
referencesToRemove.Add(reference);
}

var existingReferenceNames = new HashSet<string>(ExistingReferences.Select(item => item.ItemSpec),
StringComparer.OrdinalIgnoreCase);
var referencesToAdd = new List<ITaskItem>();
foreach (var resolvedReference in resolvedReferences)
{
if (!existingReferenceNames.Contains(resolvedReference.Key))
{
var reference = new TaskItem(resolvedReference.Key);
reference.SetMetadata("HintPath", resolvedReference.Value);
reference.SetMetadata("Private", "false");
reference.SetMetadata("IsImplicitlyDefined", "true");
reference.SetMetadata("IsImplicitlyDefinedGameReference", "true");
referencesToAdd.Add(reference);
}
}

ReferencesToAdd = referencesToAdd.ToArray();
ReferencesToUpdate = referencesToUpdate.ToArray();
ReferencesToRemove = referencesToRemove.ToArray();

return true;
}
}
115 changes: 101 additions & 14 deletions UnityModStudio.Build/Tasks/UpdateProjectFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,33 @@ public class UpdateProjectFile : Task
private static readonly XNamespace MsbuildNamespace = "http://schemas.microsoft.com/developer/msbuild/2003";

[Required]
public ITaskItem? ProjectFile { get; set; }
public string? ProjectFile { get; set; }

[Required]
public ITaskItem[] Properties { get; set; } = Array.Empty<ITaskItem>();
public ITaskItem[] Properties { get; set; } = [];

public ITaskItem[] Items { get; set; } = [];

// Names have to be specified separately to correctly clean the file if Items is empty.
public ITaskItem[] ItemNames { get; set; } = [];

// NOTE: This will reformat the document, SaveOptions.DisableFormatting only preserves whitespace between elements.
// TODO: Preserve as much formatting as possible.
public override bool Execute()
{
if (string.IsNullOrWhiteSpace(ProjectFile?.ItemSpec))
if (string.IsNullOrWhiteSpace(ProjectFile))
{
Log.LogError("Project file is not specified.");
return false;
}

if (Properties.Length == 0)
if (Properties.Length == 0 && ItemNames.Length == 0)
{
Log.LogWarning("No properties to set.");
Log.LogWarning("No properties or items to set.");
return true;
}

var document = File.Exists(ProjectFile!.ItemSpec)
? XDocument.Load(ProjectFile.ItemSpec)
var document = File.Exists(ProjectFile)
? XDocument.Load(ProjectFile!)
: new XDocument(new XElement(MsbuildNamespace + "Project"));

// Default namespace may be implicit or explicit, determine that from the root element.
Expand All @@ -47,8 +51,18 @@ public override bool Execute()
return false;
}

SetProperties(document, ns);
SetItems(document, ns);

document.Save(ProjectFile!);

return true;
}

private void SetProperties(XDocument document, XNamespace ns)
{
var propertyGroupXName = ns + "PropertyGroup";
var propertyGroupElements = document.Root.Elements(propertyGroupXName).Where(HasNoCondition).ToList();
var propertyGroupElements = document.Root!.Elements(propertyGroupXName).Where(HasNoCondition).ToList();
var propertyNames = new HashSet<string>(Properties.Select(item => item.ItemSpec));
// New properties are appended to either the first property group which contains one of the existing properties,
// or the first existing property group.
Expand Down Expand Up @@ -92,14 +106,87 @@ public override bool Execute()

if (newPropertyGroupElement is { Document: null })
document.Root.Add(newPropertyGroupElement);

document.Save(ProjectFile.ItemSpec);
}

return true;
private void SetItems(XDocument document, XNamespace ns)
{
var itemsByName = Items.ToLookup(item => item.GetMetadata("ItemName"));

var itemGroupXName = ns + "ItemGroup";
var itemGroupElements = document.Root!.Elements(itemGroupXName).Where(HasNoCondition).ToList();

var hasLoggedInvalidItemAction = false;

foreach (var itemNameItem in ItemNames)
{
var itemName = itemNameItem.ItemSpec;

// Items in current group are appended to either the first item group which contains one of the existing items,
// or the first existing empty item group.
var currentItemGroupElement =
itemGroupElements.FirstOrDefault(
igElement => igElement.Elements().Where(HasNoCondition).Any(element => element.Name.LocalName == itemName)) ??
itemGroupElements.FirstOrDefault(igElement => igElement.IsEmpty);

// Remove existing items.
itemGroupElements.Elements().Where(HasNoCondition).Where(element => element.Name.LocalName == itemName).Remove();

// Remove empty item groups except the one which will be filled.
itemGroupElements.Where(igElement => igElement.IsEmpty).Skip(1).Remove();

// Conditional propertes are not considered as setting them might have unexpected results.
static bool HasNoCondition(XElement element) => element.Attribute("Condition") == null;
if (!itemsByName.Contains(itemName))
continue;

var items = itemsByName[itemName];
var itemXName = ns + itemName;

foreach (var item in items)
{
var itemActionString = item.GetMetadata("ItemAction");
var itemAction = ItemAction.Include;
if (!string.IsNullOrEmpty(itemActionString) && !Enum.TryParse(itemActionString, true, out itemAction))
{
if (!hasLoggedInvalidItemAction)
{
Log.LogWarning("One or more items have invalid ItemAction. Skipping.");
hasLoggedInvalidItemAction = true;
}
continue;
}

var itemElement = new XElement(itemXName, new XAttribute(itemAction.ToString(), item.ItemSpec));
currentItemGroupElement ??= new XElement(itemGroupXName);
currentItemGroupElement.Add(itemElement);

var includeMetadataString = item.GetMetadata("IncludeMetadata");
if (!string.IsNullOrEmpty(includeMetadataString))
{
var includedMetadataNames = new HashSet<string>(includeMetadataString.Split(';'), StringComparer.OrdinalIgnoreCase);
foreach (string metadataName in item.MetadataNames)
{
if (!includedMetadataNames.Contains(metadataName))
continue;

var metadataValue = item.GetMetadata(metadataName);
itemElement.Add(new XAttribute(metadataName, metadataValue));
}
}
}

if (currentItemGroupElement is { Document: null })
document.Root.Add(currentItemGroupElement);
}
}

// Conditional propertes and items are not considered as setting them might have unexpected results.
private static bool HasNoCondition(XElement element) => element.Attribute("Condition") == null;


private enum ItemAction
{
Include,
Update,
Remove,
}
}
}
Loading

0 comments on commit 2e1cb13

Please sign in to comment.