Skip to content

Installing a package with a circular dependency hangs forever (UI install / non-slim restore); restore works because it's slim #758

Description

@SCastanedaMunoz

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.21.0.4) depends on Chartboost.CSharp.Logging.Unity 1.1.0

Steps:

  1. In a Unity project, open the NuGet window (NuGet → Manage NuGet Packages) with nuget.org as a source.
  2. Search for Chartboost.CSharp.Utilities.Unity (or Chartboost.CSharp.Logging.Unity) and click Install.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions