resharper/resharper-unity/src/Unity/UnityEditorIntegration/Packages/UnityPackageProjectResolution.cs (90 lines of code) (raw):
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using JetBrains.Application.Parts;
using JetBrains.ProjectModel;
using JetBrains.Util;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace JetBrains.ReSharper.Plugins.Unity.UnityEditorIntegration.Packages;
[SolutionComponent(Instantiation.ContainerAsyncAnyThreadSafe)]
public class UnityPackageProjectResolution
{
// - Starting with Unity 6000.1, the package folder changed
// Built-in Data/Resources/PackageManager/BuiltInPackages/package_id
// Registry Library/PackageCaches/package_id@fingerprint
// both can be read the same way from "projectResolution.json"
// however this file is prone to future changes, so we plan to add some our own json, written by the Rider package
private class Data(string? id, VirtualFileSystemPath resolvedPath, PackageSource packageSource)
{
public readonly string? ID = id;
public readonly VirtualFileSystemPath ResolvedPath = resolvedPath;
public readonly PackageSource Source = packageSource;
}
private readonly VirtualFileSystemPath myProjectResolutionPath;
private readonly ILogger myLogger;
private DateTime myLastModifiedTime;
private readonly Dictionary<string, Data> myPackages;
public UnityPackageProjectResolution(ISolution solution, ILogger logger)
{
myLogger = logger;
myProjectResolutionPath = solution.SolutionDirectory.Combine("Library").Combine("PackageManager").Combine("projectResolution.json");
myPackages = [];
myLastModifiedTime = DateTime.MinValue;
}
private void InvalidateCache()
{
myPackages.Clear();
try
{
myProjectResolutionPath.ReadStream(stream =>
{
using var rawReader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(rawReader);
var jsonObj = JObject.Load(jsonReader);
if (jsonObj.SelectToken("$.outputs") is not JObject outputs) return;
foreach (var property in outputs.Properties())
{
var id = property.Value["name"]?.ToString();
var resolvedPathToken = property.Value["resolvedPath"];
if (resolvedPathToken == null) continue;
var source = property.Value["source"]?.ToString();
// property.Name in the json looks like a composite key of name and version, where version can be file:path for local and tarbal package types
myPackages[property.Name] = new Data(id, VirtualFileSystemPath.TryParse(resolvedPathToken.ToString(), InteractionContext.Local), PackageSourceExtensions.ToPackageSource(source));
}
});
}
catch (Exception e)
{
myLogger.Error(e, $"Failed to build a cache on {myProjectResolutionPath}");
}
}
public List<PackageData>? GetPackages()
{
try
{
if (!myProjectResolutionPath.ExistsFile)
{
myLogger.Verbose("packageResolution.json does not exist");
return null;
}
myLogger.Info("Attempt to use projectResolution.json to determine packages");
var lastWriteTime = myProjectResolutionPath.FileModificationTimeUtc;
if (lastWriteTime > myLastModifiedTime)
{
InvalidateCache();
myLastModifiedTime = lastWriteTime;
}
var packages = new Dictionary<string, PackageData>();
foreach (var package in myPackages.Values)
{
var packageData = PackageData.GetFromFolder(package.ID, package.ResolvedPath, package.Source);
if (packageData != null)
{
packages[packageData.Id] = packageData;
}
}
// there should not be several versions of one package,
// but we wouldn't fail, even if there iss.
return [..packages.Values];
}
catch (Exception e)
{
myLogger.LogExceptionSilently(e);
return null;
}
}
}