Description
Installing a package whose dependency graph contains a cycle hangs NuGetForUnity indefinitely. The "Installing…" progress bar flips back and forth between the two mutually-dependent packages and never completes; the only way out is to kill Unity.
This happens on the install path (the "Install" button in the NuGet window, or any InstallIdentifier call that resolves dependencies). Running Restore on the same set of packages works fine.
Root cause
In NugetPackageInstaller.Install(INugetPackage, …) the installer recurses into all of a package's dependencies before the package is registered as installed via InstalledPackagesManager.AddPackageToInstalled(package) at the very end of the method. The only re-entry guard during recursion is InstalledPackagesManager.TryGetById(...), which cannot detect a package that is still being installed higher up the recursion stack.
So for a legal dependency cycle (A → B → A), neither A nor B is ever marked installed while their dependencies are being resolved, the guard never fires, and the recursion never terminates:
Install(A) // A not installed yet
└─ Install(B) // B not installed yet
└─ Install(A) // A STILL not installed (mid-recursion) → recurse again
└─ Install(B) // … forever
There is no visited / in-progress set to break the cycle.
Why restore works but install hangs
PackageRestorer.Restore installs the flat, already-pinned list from packages.config with slimRestore (default true), and Install skips the entire dependency-recursion block when isSlimRestoreInstall is true. So restore never traverses the cycle. The UI "Install" button calls InstallIdentifier(package) with isSlimRestoreInstall = false, which takes the recursive path that loops.
Reproduction
Circular dependencies are legal on NuGet, so any cyclic graph triggers this. A minimal, publicly-available 2-package cycle on nuget.org:
Chartboost.CSharp.Logging.Unity 1.1.0 depends on Chartboost.CSharp.Utilities.Unity 1.0.2
Chartboost.CSharp.Utilities.Unity (1.0.2–1.0.4) depends on Chartboost.CSharp.Logging.Unity 1.1.0
Steps:
- In a Unity project, open the NuGet window (NuGet → Manage NuGet Packages) with
nuget.org as a source.
- Search for
Chartboost.CSharp.Utilities.Unity (or Chartboost.CSharp.Logging.Unity) and click Install.
- The install never completes — the progress bar flips between
Chartboost.CSharp.Utilities.Unity and Chartboost.CSharp.Logging.Unity indefinitely.
By contrast, adding both to packages.config and running Restore completes normally.
Expected behavior
Installing a package with a circular dependency should complete and install every package in the cycle exactly once, rather than recursing forever.
Suggested fix
Track in-progress installs in a set threaded through the recursion and short-circuit re-entry when a package is already on the install stack (its own call completes the installation as the stack unwinds). Happy to open a PR with this change.
Environment
- NuGet Package:
Chartboost.CSharp.Utilities.Unity 1.0.4 / Chartboost.CSharp.Logging.Unity 1.1.0 (any package with a cyclic transitive graph)
- NuGetForUnity Version: current
master (commit acc1c7b); the recursive-install logic is unchanged in v4.5.0, so released versions are affected too
- Unity Version: Applicable to any Unity Version
- Operating System: Platform Independent
Description
Installing a package whose dependency graph contains a cycle hangs NuGetForUnity indefinitely. The "Installing…" progress bar flips back and forth between the two mutually-dependent packages and never completes; the only way out is to kill Unity.
This happens on the install path (the "Install" button in the NuGet window, or any
InstallIdentifiercall that resolves dependencies). Running Restore on the same set of packages works fine.Root cause
In
NugetPackageInstaller.Install(INugetPackage, …)the installer recurses into all of a package's dependencies before the package is registered as installed viaInstalledPackagesManager.AddPackageToInstalled(package)at the very end of the method. The only re-entry guard during recursion isInstalledPackagesManager.TryGetById(...), which cannot detect a package that is still being installed higher up the recursion stack.So for a legal dependency cycle (
A → B → A), neitherAnorBis ever marked installed while their dependencies are being resolved, the guard never fires, and the recursion never terminates:There is no visited / in-progress set to break the cycle.
Why
restoreworks butinstallhangsPackageRestorer.Restoreinstalls the flat, already-pinned list frompackages.configwithslimRestore(defaulttrue), andInstallskips the entire dependency-recursion block whenisSlimRestoreInstallis true. So restore never traverses the cycle. The UI "Install" button callsInstallIdentifier(package)withisSlimRestoreInstall = false, which takes the recursive path that loops.Reproduction
Circular dependencies are legal on NuGet, so any cyclic graph triggers this. A minimal, publicly-available 2-package cycle on nuget.org:
Chartboost.CSharp.Logging.Unity1.1.0depends onChartboost.CSharp.Utilities.Unity1.0.2Chartboost.CSharp.Utilities.Unity(1.0.2–1.0.4) depends onChartboost.CSharp.Logging.Unity1.1.0Steps:
nuget.orgas a source.Chartboost.CSharp.Utilities.Unity(orChartboost.CSharp.Logging.Unity) and click Install.Chartboost.CSharp.Utilities.UnityandChartboost.CSharp.Logging.Unityindefinitely.By contrast, adding both to
packages.configand running Restore completes normally.Expected behavior
Installing a package with a circular dependency should complete and install every package in the cycle exactly once, rather than recursing forever.
Suggested fix
Track in-progress installs in a set threaded through the recursion and short-circuit re-entry when a package is already on the install stack (its own call completes the installation as the stack unwinds). Happy to open a PR with this change.
Environment
Chartboost.CSharp.Utilities.Unity1.0.4 /Chartboost.CSharp.Logging.Unity1.1.0 (any package with a cyclic transitive graph)master(commitacc1c7b); the recursive-install logic is unchanged inv4.5.0, so released versions are affected too