Arquitectura
Tres capas, una responsabilidad cada una — cómo está estructurado PCPM y por qué.
PCPM está estructurado como tres capas, cada una con una única responsabilidad, todas conectadas por inyección de dependencias. Esta página recorre las capas y explica el pago del diseño.
Las tres capas
┌─────────────────────────────────────────────────────────────────┐
│ pcpm.Cli Comandos Spectre.Console.Cli (uno por verbo) │
│ ───────── orquestación fina únicamente, sin lógica │
└────────────────────────┬────────────────────────────────────────┘
│ DI (Microsoft.Extensions.DependencyInjection)
┌────────────────────────┴────────────────────────────────────────┐
│ pcpm.Core Modelos de dominio, abstracciones, pura │
│ ──────── DependencyResolver, sin I/O │
└────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────┴────────────────────────────────────────┐
│ pcpm.Infrastructure Implementaciones: │
│ ────────────────── • NuGetFeed (HTTP crudo, no NuGet.Protocol) │
│ • PackageStore (direccionable por contenido)│
│ • HardlinkCreator (P/Invoke Win32) │
│ • CpmFileService, ProjectFileService, │
│ LockfileService (XML/JSON I/O) │
│ • PhysicalFileSystem, ProcessRunner │
└─────────────────────────────────────────────────────────────────┘
pcpm.Cli
La capa CLI es una clase de comando por verbo, más el bootstrap que
configura la DI. Cada comando recibe sus dependencias por inyección
de constructor. El flujo dotnet run … pcpm install es
Program → InstallCommand → (CpmFileService, ProjectFileService, ProjectDiscovery, NuGetFeed, PackageStore, LockfileService, DependencyResolver, ProcessRunner, IAnsiConsole).
La capa CLI no tiene lógica de negocio. Si te encuentras añadiendo
código de dominio a un comando, empújalo hacia abajo a pcpm.Core
(si es puro) o pcpm.Infrastructure (si necesita I/O).
pcpm.Core
La capa core tiene modelos de dominio, abstracciones y lógica pura.
La clase más importante aquí es
pcpm.Core.Services.DependencyResolver — el resolver BFS con unión
de restricciones descrito en
Resolución de dependencias.
pcpm.Core no tiene dependencias de I/O. Eso significa:
- Cada clase aquí es unit-testeable sin un directorio temporal, un sistema de ficheros falso, ni un stub de red.
- Cada dependencia es un parámetro de constructor, expresado como
interfaz (
INuGetFeed,IPackageStore,IFileSystem). - Si quieres cambiar una implementación, no tocas esta capa —
proporcionas un
INuGetFeeddistinto al contenedor de DI.
pcpm.Infrastructure
La capa de infraestructura es el único lugar que toca el disco o la red. Contiene:
NuGetFeed— cliente HTTP crudo para el protocolo NuGet v3. Sin dependencia deNuGet.Protocol. Elegimos HTTP crudo porqueNuGet.Protocolson 4MB de código transitivo y solo necesitamos tres endpoints.MultiFeedNuGetFeed— agregador que consulta múltiples feeds en paralelo y combina los resultados.PackageStore— el store direccionable por contenido. Hashea paquetes, deduplica por hash, expone un métodoMaterializeque hace hardlink de un paquete almacenado en~/.nuget/packages.HardlinkCreator— P/Invoke a la API de creación de links de la plataforma. Hardlinks NTFS en Windows, hardlinks en otros.CpmFileService,ProjectFileService,LockfileService— I/O XML y JSON para los ficheros de manifiesto.PhysicalFileSystem,ProcessRunner— envoltorios finos sobre la BCL que existen para quepcpm.Corepueda testearse sin el sistema de ficheros real o las APIs de proceso.
Por qué este layout
La división en tres capas no es nueva. Es un modelo bastante estándar de “dominio en el medio, infraestructura en el exterior”. El pago es concreto:
- Core es unit-testeable de forma aislada. Sin directorios temporales, sin stubs del sistema de ficheros, sin I/O asíncrono. Los tests del DependencyResolver corren en milisegundos.
- Infraestructura tiene una superficie pequeña y bien entendida. Todo el I/O vive en 8 clases, cada una con una interfaz clara. Si algo falla en rutas de código solo de Windows, la instrumentación de test es local.
- CLI es una orquestación de una-clase-por-verbo. Añadir un comando es mecánico: registrar una nueva clase en DI, escribir el comando, listo.
El pago mayor es la preparación para el futuro. Las abstracciones
en pcpm.Core.Abstractions son el contrato. Si quieres un store
distinto (¿en memoria? ¿respaldado por S3? ¿servido desde un
servidor de artefactos corporativo?), implementas IPackageStore
y lo inyectas. Al resolver no le importa de dónde vienen los bytes.
El store, en detalle
El store direccionable por contenido tiene tres operaciones:
- Put(bytes, id, version) → hash — almacena los bytes, devuelve el SHA-256. Idempotente: re-almacenar los mismos bytes es un no-op.
- Has(hash) → bool — ¿está este hash presente en el store?
- Materialize(hash, targetPath) → void — hace hardlink del
árbol de ficheros enraizado en
<hash>/extracted/<id>/<version>/entargetPath. En Windows esto esCreateHardLinkW. En POSIX,link(2). En cross-volume, fallback a copia recursiva.
Una ejecución de dotnet build no modifica el store. El store es
inmutable: cada entrada tiene un hash de contenido, y los bytes en
ese hash nunca cambian. La única forma de “eliminar” algo es borrar
el directorio (o, en el futuro, ejecutar pcpm store prune para
soltar los hashes que no referencia ningún lockfile).
Configuración de la DI
El contenedor de DI se configura en Program.cs:
services
.AddSingleton<IFileSystem, PhysicalFileSystem>()
.AddSingleton<IProcessRunner, ProcessRunner>()
.AddSingleton<ICpmFileService, CpmFileService>()
.AddSingleton<IProjectFileService, ProjectFileService>()
.AddSingleton<IProjectDiscovery, ProjectDiscoveryService>()
.AddSingleton<INuGetFeed, MultiFeedNuGetFeed>()
.AddSingleton<IPackageStore, PackageStore>()
.AddSingleton<IHardlinkCreator, HardlinkCreator>()
.AddSingleton<ILockfileService, LockfileService>()
.AddSingleton<IWorkspaceLocator, WorkspaceLocator>()
.AddSingleton<IDependencyResolver, DependencyResolver>()
.AddSingleton<IAnsiConsole>(_ => AnsiConsole.Console);
Todo es un singleton. PCPM es de vida corta: cada comando se ejecuta hasta completarse en un único proceso, y la vida útil es el proceso. Los singletons son la elección natural.
El compañero MSBuild
pcpm.MsBuild es un pequeño proyecto compañero — un paquete NuGet
que envía una tarea RelinkBin de MSBuild. Cuando instalas el
paquete en un proyecto, la tarea vuelve a hacer hardlink de la
salida del build al store tras finalizar dotnet build. Es una
pequeña ganancia en uso de disco para soluciones grandes, y una
ganancia aún mayor para las cachés de CI: el directorio bin/ no
crece con cada build.
Es opt-in. El paquete pcpm.MsBuild se publica por separado de
pcpm; instálalo una vez por workspace si lo quieres.