diff options
Diffstat (limited to 'Packages/com.vrchat.core.vpm-resolver/Editor/Resolver')
4 files changed, 580 insertions, 0 deletions
diff --git a/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs new file mode 100644 index 0000000..e3a23f9 --- /dev/null +++ b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Serilog; +using Serilog.Sinks.Unity3D; +using UnityEditor; +using UnityEngine; +using VRC.PackageManagement.Core; +using VRC.PackageManagement.Core.Types; +using VRC.PackageManagement.Core.Types.Packages; +using Version = VRC.PackageManagement.Core.Types.VPMVersion.Version; + +namespace VRC.PackageManagement.Resolver +{ + + [InitializeOnLoad] + public class Resolver + { + private const string _projectLoadedKey = "PROJECT_LOADED"; + + private static string _projectDir; + public static string ProjectDir + { + get + { + if (_projectDir != null) + { + return _projectDir; + } + + try + { + _projectDir = new DirectoryInfo(Assembly.GetExecutingAssembly().Location).Parent.Parent.Parent + .FullName; + return _projectDir; + } + catch (Exception) + { + return ""; + } + } + } + + static Resolver() + { + SetupLogging(); + if (!SessionState.GetBool(_projectLoadedKey, false)) + { +#pragma warning disable 4014 + CheckResolveNeeded(); +#pragma warning restore 4014 + } + } + + private static void SetupLogging() + { + VRCLibLogger.SetLoggerDirectly( + new LoggerConfiguration() + .MinimumLevel.Information() + .WriteTo.Unity3D() + .CreateLogger() + ); + } + + private static async Task CheckResolveNeeded() + { + SessionState.SetBool(_projectLoadedKey, true); + + //Wait for project to finish compiling + while (EditorApplication.isCompiling || EditorApplication.isUpdating) + { + await Task.Delay(250); + } + + try + { + + if (string.IsNullOrWhiteSpace(ProjectDir)) + { + return; + } + + if (VPMProjectManifest.ResolveIsNeeded(ProjectDir)) + { + Debug.Log($"Resolve needed."); + var result = EditorUtility.DisplayDialog("VRChat Package Management", + $"This project requires some VRChat Packages which are not in the project yet.\n\nPress OK to download and install them.", + "OK", "Show Me What's Missing"); + if (result) + { + ResolveStatic(ProjectDir); + } + else + { + ResolverWindow.ShowWindow(); + } + } + } + catch (Exception) + { + // Unity says we can't open windows from this function so it throws an exception but also works fine. + } + } + + public static bool VPMManifestExists() + { + return VPMProjectManifest.Exists(ProjectDir, out _); + } + + public static void CreateManifest() + { + VPMProjectManifest.Load(ProjectDir); + ResolverWindow.Refresh().ConfigureAwait(false); + } + + public static void ResolveManifest() + { + ResolveStatic(ProjectDir); + } + + public static void ResolveStatic(string dir) + { + // Todo: calculate and show actual progress + EditorUtility.DisplayProgressBar($"Getting all VRChat Packages", "Downloading and Installing...", 0.5f); + VPMProjectManifest.Resolve(ProjectDir); + EditorUtility.ClearProgressBar(); + ForceRefresh(); + } + + public static List<string> GetAllVersionsOf(string id) + { + var project = new UnityProject(ProjectDir); + + var versions = new List<string>(); + foreach (var provider in Repos.GetAll) + { + var packagesWithVersions = provider.GetAllWithVersions(); + + foreach (var packageVersionList in packagesWithVersions) + { + foreach (var package in packageVersionList.Value.VersionsDescending) + { + if (package.Id != id) + continue; + if (Version.TryParse(package.Version, out var result)) + { + if (!versions.Contains(package.Version)) + versions.Add(package.Version); + } + } + } + } + + // Sort packages in project to the top + var sorted = from entry in versions orderby project.VPMProvider.HasPackage(entry) descending select entry; + + return sorted.ToList<string>(); + } + + public static List<string> GetAffectedPackageList(IVRCPackage package) + { + List<string> list = new List<string>(); + + var project = new UnityProject(ProjectDir); + + if (Repos.GetAllDependencies(package, out Dictionary<string, string> dependencies, null)) + { + foreach (KeyValuePair<string, string> item in dependencies) + { + project.VPMProvider.Refresh(); + if (project.VPMProvider.GetPackage(item.Key, item.Value) == null) + { + IVRCPackage d = Repos.GetPackageWithVersionMatch(item.Key, item.Value); + if (d != null) + { + list.Add(d.Id + " " + d.Version + "\n"); + } + } + } + + return list; + } + + return null; + } + + public static void ForceRefresh () + { + UnityEditor.PackageManager.Client.Resolve(); + AssetDatabase.Refresh(); + } + + } +}
\ No newline at end of file diff --git a/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs.meta b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs.meta new file mode 100644 index 0000000..a540340 --- /dev/null +++ b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/Resolver.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f872e3586f8b4f06bab3c9facd14f6e6 +timeCreated: 1659048476
\ No newline at end of file diff --git a/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs new file mode 100644 index 0000000..264aca6 --- /dev/null +++ b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; +using VRC.PackageManagement.Core; +using VRC.PackageManagement.Core.Types; +using VRC.PackageManagement.Core.Types.Packages; +using Version = VRC.PackageManagement.Core.Types.VPMVersion.Version; + +namespace VRC.PackageManagement.Resolver +{ + public class ResolverWindow : EditorWindow + { + // VisualElements + private static VisualElement _rootView; + private static Button _refreshButton; + private static Button _createButton; + private static Button _resolveButton; + private static Box _manifestInfo; + private static Label _manifestLabel; + private static Label _manifestInfoText; + private static VisualElement _manifestPackageList; + private static bool _isUpdating; + private static Color _colorPositive = Color.green; + private static Color _colorNegative = new Color(1, 0.3f, 0.3f); + + const string HAS_REFRESHED_KEY = "VRC.PackageManagement.Resolver.Refreshed"; + + private static bool IsUpdating + { + get => _isUpdating; + set + { + _isUpdating = value; + _refreshButton.SetEnabled(!value); + _refreshButton.text = value ? "Refreshing..." : "Refresh"; + _manifestLabel.text = value ? "Refreshing packages ..." : "Required Packages"; + } + } + + + [MenuItem("VRChat SDK/Utilities/Package Resolver")] + public static void ShowWindow() + { + ResolverWindow wnd = GetWindow<ResolverWindow>(); + wnd.titleContent = new GUIContent("Package Resolver"); + } + + public static async Task Refresh() + { + if (_rootView == null || string.IsNullOrWhiteSpace(Resolver.ProjectDir)) return; + + IsUpdating = true; + _manifestPackageList.Clear(); + + // check for vpm dependencies + if (!Resolver.VPMManifestExists()) + { + _manifestInfoText.style.display = DisplayStyle.Flex; + _manifestInfoText.text = "No VPM Manifest"; + _manifestInfoText.style.color = _colorNegative; + } + else + { + _manifestInfoText.style.display = DisplayStyle.None; + } + + var manifest = VPMProjectManifest.Load(Resolver.ProjectDir); + var project = await Task.Run(() => new UnityProject(Resolver.ProjectDir)); + + // Here is where we detect if all dependencies are installed + var allDependencies = manifest.locked != null && manifest.locked.Count > 0 + ? manifest.locked + : manifest.dependencies; + + var depList = await Task.Run(() => + { + var results = new Dictionary<(string id, string version), (IVRCPackage package, List<string> allVersions)>(); + foreach (var pair in allDependencies) + { + var id = pair.Key; + var version = pair.Value.version; + var package = project.VPMProvider.GetPackage(id, version); + results.Add((id, version), (package, Resolver.GetAllVersionsOf(id))); + } + + var legacyPackages = project.VPMProvider.GetLegacyPackages(); + + results = results.Where(i => !legacyPackages.Contains(i.Key.id)).ToDictionary(i => i.Key, i => i.Value); + + return results; + }); + + foreach (var dep in depList) + { + + _manifestPackageList.Add( + CreateDependencyRow( + dep.Key.id, + dep.Key.version, + project, + dep.Value.package, + dep.Value.allVersions + ) + ); + } + + IsUpdating = false; + } + + /// <summary> + /// Unity calls the CreateGUI method automatically when the window needs to display + /// </summary> + private void CreateGUI() + { + ScrollView scrollView = new ScrollView() + { + horizontalScrollerVisibility = ScrollerVisibility.Hidden, + }; + rootVisualElement.Add(scrollView); + + _rootView = scrollView; + _rootView.name = "root-view"; + _rootView.styleSheets.Add((StyleSheet)Resources.Load("ResolverWindowStyle")); + + // Main Container + var container = new Box() + { + name = "buttons" + }; + _rootView.Add(container); + + // Create Button + if (!Resolver.VPMManifestExists()) + { + _createButton = new Button(Resolver.CreateManifest) + { + text = "Create", + name = "create-button-base" + }; + container.Add(_createButton); + } + else + { + _resolveButton = new Button(Resolver.ResolveManifest) + { + text = "Resolve All", + name = "resolve-button-base" + }; + container.Add(_resolveButton); + } + + // Manifest Info + _manifestInfo = new Box() + { + name = "manifest-info", + }; + _manifestLabel = (new Label("Required Packages") { name = "manifest-header" }); + _manifestInfo.Add(_manifestLabel); + _manifestInfoText = new Label(); + _manifestInfo.Add(_manifestInfoText); + _manifestPackageList = new ScrollView() + { + verticalScrollerVisibility = ScrollerVisibility.Hidden, + }; + _manifestPackageList.style.flexDirection = FlexDirection.Column; + _manifestPackageList.style.alignItems = Align.Stretch; + _manifestInfo.Add(_manifestPackageList); + + _rootView.Add(_manifestInfo); + + // Refresh Button + var refreshBox = new Box(); + _refreshButton = new Button(() => + { + // When manually refreshing - ensure package manager is also up to date + Resolver.ForceRefresh(); + Refresh().ConfigureAwait(false); + }) + { + text = "Refresh", + name = "refresh-button-base" + }; + refreshBox.Add(_refreshButton); + _rootView.Add(refreshBox); + + // Refresh on open + // Sometimes unity can get into a bad state where calling package manager refresh will endlessly reload assemblies + // That in turn means that a Full refresh will be called every single time assemblies are loaded + // Which locks up the editor in an endless loop + // This condition ensures that the UPM resolve only happens on first launch + // We also call it after installing packages or hitting Refresh manually + if (!SessionState.GetBool(HAS_REFRESHED_KEY, false)) + { + SessionState.SetBool(HAS_REFRESHED_KEY, true); + Resolver.ForceRefresh(); + } + + rootVisualElement.schedule.Execute(() => Refresh().ConfigureAwait(false)).ExecuteLater(100); + } + + private static VisualElement CreateDependencyRow(string id, string version, UnityProject project, IVRCPackage package, List <string> allVersions) + { + bool havePackage = package != null; + + // Table Row + VisualElement row = new Box { name = "package-row" }; + VisualElement column1 = new Box { name = "package-box" }; + VisualElement column2 = new Box { name = "package-box" }; + VisualElement column3 = new Box { name = "package-box" }; + VisualElement column4 = new Box { name = "package-box" }; + + column1.style.minWidth = 200; + column1.style.width = new StyleLength(new Length(40, LengthUnit.Percent)); + column2.style.minWidth = 100; + column2.style.width = new StyleLength(new Length(19f, LengthUnit.Percent)); + column3.style.minWidth = 100; + column3.style.width = new StyleLength(new Length(19f, LengthUnit.Percent)); + column4.style.minWidth = 100; + column4.style.width = new StyleLength(new Length(19f, LengthUnit.Percent)); + + row.Add(column1); + row.Add(column2); + row.Add(column3); + row.Add(column4); + + // Package Name + Status + column1.style.alignItems = Align.FlexStart; + if (havePackage) + { + column1.style.flexDirection = FlexDirection.Column; + var titleRow = new VisualElement(); + titleRow.style.unityFontStyleAndWeight = FontStyle.Bold; + titleRow.Add(new Label(package.Title)); + column1.Add(titleRow); + } + TextElement text = new TextElement { text = $"{id} {version} " }; + + column1.Add(text); + + if (!havePackage) + { + TextElement missingText = new TextElement { text = "MISSING" }; + missingText.style.color = _colorNegative; + column2.Add(missingText); + } + + // Version Popup + var currVersion = Mathf.Max(0, havePackage ? allVersions.IndexOf(package.Version) : 0); + var popupField = new PopupField<string>(allVersions, 0) + { + value = allVersions[currVersion], + style = { flexGrow = 1} + }; + + column3.Add(popupField); + + // Button + + Button updateButton = new Button() { text = "Update" }; + if (havePackage) + RefreshUpdateButton(updateButton, version, allVersions[0]); + else + RefreshMissingButton(updateButton); + + updateButton.clicked += (() => + { + IVRCPackage package = Repos.GetPackageWithVersionMatch(id, popupField.value); + + // Check and warn on Dependencies if Updating or Downgrading + if (Version.TryParse(version, out var currentVersion) && + Version.TryParse(popupField.value, out var newVersion)) + { + Dictionary<string, string> dependencies = new Dictionary<string, string>(); + StringBuilder dialogMsg = new StringBuilder(); + List<string> affectedPackages = Resolver.GetAffectedPackageList(package); + for (int v = 0; v < affectedPackages.Count; v++) + { + dialogMsg.Append(affectedPackages[v]); + } + + if (affectedPackages.Count > 1) + { + dialogMsg.Insert(0, "This will update multiple packages:\n\n"); + dialogMsg.AppendLine("\nAre you sure?"); + if (EditorUtility.DisplayDialog("Package Has Dependencies", dialogMsg.ToString(), "OK", "Cancel")) + OnUpdatePackageClicked(project, package); + } + else + { + OnUpdatePackageClicked(project, package); + } + } + + }); + column4.Add(updateButton); + + popupField.RegisterCallback<ChangeEvent<string>>((evt) => + { + if (havePackage) + RefreshUpdateButton(updateButton, version, evt.newValue); + else + RefreshMissingButton(updateButton); + }); + + return row; + } + + private static void RefreshUpdateButton(Button button, string currentVersion, string highestAvailableVersion) + { + if (currentVersion == highestAvailableVersion) + { + button.style.display = DisplayStyle.None; + } + else + { + button.style.display = (_isUpdating ? DisplayStyle.None : DisplayStyle.Flex); + if (Version.TryParse(currentVersion, out var currentVersionObject) && + Version.TryParse(highestAvailableVersion, out var highestAvailableVersionObject)) + { + if (currentVersionObject < highestAvailableVersionObject) + { + SetButtonColor(button, _colorPositive); + button.text = "Update"; + } + else + { + SetButtonColor(button, _colorNegative); + button.text = "Downgrade"; + } + } + } + } + + private static void RefreshMissingButton(Button button) + { + button.text = "Resolve"; + SetButtonColor(button, Color.white); + } + + private static void SetButtonColor(Button button, Color color) + { + button.style.color = color; + color.a = 0.25f; + button.style.borderRightColor = + button.style.borderLeftColor = + button.style.borderTopColor = + button.style.borderBottomColor = + color; + } + + private static async void OnUpdatePackageClicked(UnityProject project, IVRCPackage package) + { + _isUpdating = true; + await Refresh(); + await Task.Delay(500); + project.UpdateVPMPackage(package); + _isUpdating = false; + await Refresh(); + Resolver.ForceRefresh(); + } + + } +}
\ No newline at end of file diff --git a/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs.meta b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs.meta new file mode 100644 index 0000000..fbfb6ef --- /dev/null +++ b/Packages/com.vrchat.core.vpm-resolver/Editor/Resolver/ResolverWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 32d2636186ee0834fa1dc2287750dd32 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: |
