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:
- Empieza una worklist con todas las entradas
<PackageReference />directas. - 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.) - Para cada versión candidata, recorre las dependencias de su
.nuspecy las añade a la worklist con el rango de versión que requieren. - 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.
- Si ninguna versión satisface el rango acumulado, la resolución
falla con un
ResolutionConflictypcpm installsale 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 exclusivo1.0.*— flotante, resuelto a la versión coincidente más alta1.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 />enDirectory.Packages.propsque 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.