es

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 INuGetFeed distinto 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 de NuGet.Protocol. Elegimos HTTP crudo porque NuGet.Protocol son 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étodo Materialize que 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 que pcpm.Core pueda 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:

  1. Put(bytes, id, version) → hash — almacena los bytes, devuelve el SHA-256. Idempotente: re-almacenar los mismos bytes es un no-op.
  2. Has(hash) → bool — ¿está este hash presente en el store?
  3. Materialize(hash, targetPath) → void — hace hardlink del árbol de ficheros enraizado en <hash>/extracted/<id>/<version>/ en targetPath. En Windows esto es CreateHardLinkW. 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.