es

Resolución de dependencias

BFS con unión de restricciones, detección de conflictos, y cómo PCPM elige la versión correcta.

El resolver de PCPM es deliberadamente pequeño: lógica pura, sin I/O, sin heurísticas. Recorre el grafo de dependencias con BFS, acumula restricciones por paquete, y elige la versión más alta que satisface la unión de todas ellas.

El algoritmo

Dado un workspace con N proyectos y un conjunto de dependencias directas, el resolver:

  1. Empieza una worklist con todas las entradas <PackageReference /> directas.
  2. Saca un paquete de la worklist. Si aún no está en el conjunto resuelto, consulta el feed por sus versiones. (La capa de feed es intercambiable, pero por defecto es NuGetFeed, que habla HTTP crudo con nuget.org.)
  3. Para cada versión candidata, recorre las dependencias de su .nuspec y las añade a la worklist con el rango de versión que requieren.
  4. Cuando el mismo paquete es requerido por varios caminos, acumula el rango. La versión elegida es la más alta que satisface todos los rangos acumulados.
  5. Si ninguna versión satisface el rango acumulado, la resolución falla con un ResolutionConflict y pcpm install sale con 1.

El conjunto es O(V + E) en el tamaño del grafo resuelto, con caché que hace las re-ejecuciones gratis. También es puro: dado el mismo workspace y los mismos feeds, obtienes el mismo lockfile, en cada máquina, en cada commit.

Unión de restricciones

La parte interesante es el paso 4. Supón que apps/web quiere Microsoft.Extensions.Logging >= 8.0.0, y que libs/contracts quiere Microsoft.Extensions.Logging >= 7.0.1. La unión es >= 7.0.1 — ambas restricciones se satisfacen eligiendo la versión más alta disponible. Si ambos proyectos están contentos con [8.0.0, 9.0.0), PCPM elige la 8.x más alta.

Si apps/web dice >= 8.0.0 y libs/contracts dice < 8.0.0, la unión está vacía. Eso es un conflicto, y PCPM lo reporta explícitamente con los dos proyectos fuente y los rangos de versión que discrepan.

Parseo de rangos

PCPM delega el parseo de rangos de versión a NuGet.Versioning, la misma librería que usa dotnet restore en sí. Eso significa:

  • [1.0.0, 2.0.0) — mínimo inclusivo, máximo exclusivo
  • 1.0.* — flotante, resuelto a la versión coincidente más alta
  • 1.0.0-beta.* — flotante sobre una etiqueta pre-release
  • * — cualquier versión

Es la misma sintaxis que escribes en tus ficheros .nuspec; PCPM no introduce una nueva. El comportamiento es el mismo que el de dotnet restore, incluida la semántica de pre-release, así que no te sorprenden las actualizaciones.

Versiones flotantes

Por defecto, PCPM fija los rangos flotantes. Un <PackageReference Include="X" Version="1.0.*" /> en CPM se resuelve a la versión estable coincidente más alta en el momento de pcpm install, y esa versión se escribe en pcpm.lock como cadena fija. Las ejecuciones siguientes no re-resuelven.

Para optar por el comportamiento de “elige siempre la última”, pon "lockfile": { "floating": "follow" } en pcpm.json. Con este ajuste, pcpm install re-consultará el feed por rangos flotantes en cada ejecución. No recomendamos esto para CI — pcpm ci ignora el ajuste y siempre usa el lockfile como fuente de verdad.

Reporte de conflictos

Cuando PCPM no puede satisfacer la unión, imprime un ResolutionConflict por consola y sale con un estado no-cero. El mensaje tiene la forma:

× Resolution conflict: Microsoft.Extensions.Logging

  Required by:
    apps/web  (>= 8.0.0)
    libs/contracts  (< 8.0.0)

  No version satisfies both. Either pin one project to a specific
  version, or refactor so the constraint disappears.

La solución es casi siempre una de:

  • Añade una entrada <PackageVersion /> en Directory.Packages.props que fije el paquete a una versión que ambos proyectos puedan usar.
  • Actualiza uno de los proyectos a un major más nuevo.
  • Usa pcpm why <pkg> para ver qué paquetes están arrastrando la restricción transitivamente.

pcpm why recorre el lockfile e imprime la cadena de dependientes que trajo un paquete al grafo resuelto. Es una herramienta esencial de depuración cuando un bump transitivo te sorprende.

Lógica pura, fácil de testear

El resolver está en pcpm.Core.Services.DependencyResolver. No tiene dependencias de I/O: toma un feed y un workspace, devuelve un grafo resuelto. Los tests unitarios cubren:

  • Camino feliz del BFS.
  • Resolución transitiva con una cadena multi-salto.
  • Detección de conflictos con dos rangos incompatibles.
  • Resolución de versiones flotantes.

Este es el pago del diseño: como el resolver es puro, las abstracciones que define (el feed, el workspace) son el contrato. ¿Quieres un feed distinto? Implementa INuGetFeed. ¿Quieres testear contra un feed falso? Sustitúyelo por un stub. La suite de tests hace exactamente esto, y es la razón por la que el resolver está tan seguro de sí mismo.