Files
oki/cli/ARCHITECTURE.md

199 lines
9.5 KiB
Markdown

Architecture de la ligne de commande *oki*
==========================================
La commande `oki` constitue le point d'entrée avec le gestionnaire de paquet.
Exécutée dans le répertoire d'un projet, elle permet de gérer les dépendances tout en incluant quelques raccourcis comme la génération d'un *Makefile*.
Composants
----------
Il est bien évidemment question de paquets et de leurs métadonnées. Ils sont modélisés dans `package`.
Ces paquets sont présents dans une collection de dépôts à la fois distants et locaux, dans `repository`.
La configuration de ces dépôts fait partie de `config`, le téléchargement et le désarchivage des paquets dans `io`.
La lecture et la représentation des versions et des contraintes a lieu dans le `semver`. La résolution des dépendances a lieu quant à elle dans `solver`.
Pour terminer, la gestion des commandes de l'utilisateur a lieu dans `cli`.
Compilation
-----------
Le projet nécessite C++ 20 et sa compilation est décrite à l'aide d'un [*Makefile*](Makefile).
Certaines dépendances header-only peuvent être téléchargées à l'aide du script `configurate.sh`, avec l'argument `-d`.
Le script shell `make-in-vdn.sh` peut être utilisé pour compiler dans une machine virtuelle VDN dans le réseau *demo*.
Installation d'un paquet
------------------------
```mermaid
sequenceDiagram
User->>CLI: oki install linked-list
CLI->>Repo: Demande les informations du paquet
alt paquet existant
Repo-->>CLI: Répond avec les informations du paquet
CLI->>Repo: Demande le téléchargement d'une version
Repo-->>CLI: Récupère l'archive de la version
CLI->>CLI: Désarchive dans le projet de l'utilisateur
CLI-->>User: Succès
else paquet inexistant
Repo-->>CLI: Répond qu'il n'existe pas
CLI-->>User: Erreur
end
```
Classes principales
-------------------
```mermaid
classDiagram
class Extractor {
-destination: fs::path
+Archive(destination: fs::path)
+extract(archive: fs::path)
}
RequestException <.. HttpRequest : throws
APIException <.. RemoteRepository : throws
class HttpRequest {
-curl: CURL
-url: string
+HttpRequest(url: string_view)
+get() string
+download(fs::path path)
}
class RequestException {
}
class APIException {
}
class TmpFile {
-filename: char*
+TmpFile()
+getFilename() char*
}
Version <|-- PackageVersion
class Package {
-shortName: string
-description: string
+getShortName() string
+getDescription() string
+getVersions() string
}
Package "1" --> "*" PackageVersion
class Version {
-identifier: string
+getIdentifier() string
}
class PackageVersion {
-publishedDate: string
-downloadUrl: string
+getIdentifier() string
}
Repository <|.. LocalRepository
Repository <|.. RemoteRepository
HttpRequest <.. RemoteRepository
class Repository {
+listPackages() vector~Package~
+showPackage() optional~Package~
+download(version : PackageVersion)
}
<<interface>> Repository
class LocalRepository {
-root: fs::path
+LocalRepository(root : fs::path)
+createIfNotExists()
}
class RemoteRepository {
-apiUrl: string
RemoteRepository(root : string_view)
}
TmpFile <.. Installer
Extractor <.. Installer
class Installer {
+install(version : PackageVersion)
}
```
Le modèle est constitué de paquets et de leurs versions.
La classe `RemoteRepository` sert de passerelle pour récupérer les informations d'un dépôt distant et instancier le modèle.
Puisque l'on traite des fichiers archivés, des classes sont dédiées à ce rôle. Une pour extraire, une pour archiver, une pour créer un fichier temporaire...
Pour effectuer des requêtes HTTP, *oki* utilise la bibliothèque C `libcurl`.
La classe `HttpRequest` permet de s'abstraire de cette dépendance extérieure et d'utiliser le principe RAII pour implicitement libérer la mémoire grâce au destructeur C++.
Toute requête HTTP peut tout à fait mal se passer, `HttpRequest` est donc susceptible de lever une `RequestException`, abstraites de la `libcurl` et `RemoteRepository` peut ne pas comprendre le JSON que l'API répond, elle lance alors une `APIException`.
L'installateur décrit la procédure d'installation d'un paquet. La plupart du temps, elle dépend d'un type précis (comme `RemoteRepository`).
Il existe des stratégies différentes d'installation comme la copie des paquets dans le projet de l'utilisateur ou le lien avec le cache local via le système de fichiers.
Les versions acceptées par un paquet sont décrites par des contraintes de version. Ces contraintes s'inspirent de la spécification de [gestion sémantique de version](https://semver.org/lang/fr/). Deux versions mineures différentes (par exemple `4.2.0` et `4.5.0`) sont généralement compatibles, tandis que deux versions majeures différentes (comme `1.3.7` et `2.0.0`) peuvent ne pas l'être.
Solveur de version
------------------
La gestion des dépendances demande de sélectionner pour chaque dépendance une version compatible avec tout le reste.
Le problème de sélection de la version d'un paquet consiste à trouver un ensemble de dépendances qui peuvent être utilisées pour construire un paquet de niveau supérieur P qui est complet (toutes les dépendances sont satisfaites) et compatible (aucun paquet incompatible n'est sélectionné).
Il se peut qu'un tel ensemble n'existe pas, à cause du problème de la dépendance en diamant : peut-être que A a besoin de B et C ; B a besoin de D version 1, pas 2 ; et C a besoin de D version 2, pas 1. Dans ce cas, en supposant qu'il n'est pas possible de choisir les deux versions de D, il n'y a aucun moyen de construire A.
```mermaid
graph TD
A-->B
A-->C
subgraph D
v1
v2
end
B-->v1
C-->v2
```
Un gestionnaire de paquets a besoin d'un algorithme pour sélectionner les versions des paquets : lorsque vous exécutez `oki install minizip`, l'application peut supposer que vous voulez dire la dernière version de `minizip`, mais elle doit alors trouver un moyen de satisfaire les dépendances transitives de `minizip`, ou bien afficher une explication compréhensible de la raison pour laquelle `minizip` ne peut pas être installé.
Le problème de la sélection de version est NP-complet, ce qui signifie qu'il est peu probable que nous trouvions un algorithme qui s'exécuterait rapidement dans tous les cas.
L'algorithme va devoir essayer potentiellement beaucoup de combinaisons de versions avant d'en trouver une compatible. Dans le meilleur des cas cependant, la solution correspond à choisir la version la plus récente de chaque paquet, et ce récursivement.
L'application `oki` utilise pour résoudre ce problème un [algorithme de retour sur trace](https://fr.wikipedia.org/wiki/Retour_sur_trace) pour parcourir de manière exhaustive toutes les combinaisons possibles.
Supposons les dépendances suivantes :
```mermaid
graph TD
root-->config
root-->h2
config-->json
```
Chaque paquet peut être résumé sommairement par sa liste de dépendances, par exemple :
```toml
[dependencies]
config = '^13.2.0'
h2 = '^1.21.0'
```
Chaque exigence est interprétée comme une contrainte sur la version à sélectionner du paquet. Cette contrainte demande à ce que chaque version retenue soit comprise dans un intervalle :
> Versions concrètes : `config` ∈ [13.2.0, 14.0.0[ et `h2` ∈ [1.21.0, 2.0.0[
Dans cet algorithme, la solution de ce problème est construite pas à pas. Initialement, la liste des dépendances à satisfaire ne contient que les dépendances connues directement, c'est-à-dire les deux précédemment citées dans notre exemple. La solution courante est quant à elle une liste vide.
Profondeur de récursivité 1 : L'algorithme sélectionne tout d'abord la première contrainte à satisfaire. Il tente tout d'abord la version la plus récente de cette dépendance qui correspond, par exemple `config` = 13.9.2.
Cette version est ajoutée à la solution partielle. Si cette version a des dépendances, elles sont ajoutées dans une copie de la liste des contraintes à satisfaire, de laquelle est retirée la contrainte qui vient d'être résolue.
Profondeur de récursivité 2 : L'algorithme explore récursivement cette solution partielle avec pour premier élément `config = 13.9.2`.
Supposons que `config` demande le paquet `json` ∈ [4.1.0, 5.0.0[ qui n'a pas de dépendance. La version la plus récente de `json` qui satisfasse cette contrainte est `4.6.0`.
Une nouvelle copie de la solution partielle est créée, auquel on ajoute cette version de `json` : `config = 13.9.2, json = 4.6.0`.
Profondeur de récursivité 3 : Une première partie de solution a été trouvée, mais il reste encore à sélectionner une version pour le paquet `h2`.
L'algorithme sélectionne à nouveau la dernière version compatible avec la contrainte de version de ce paquet. La nouvelle solution partielle devient `config = 13.9.2, json = 4.6.0, h2 = 1.21.9`.
Profondeur de récursivité 4 : La liste des contraintes restantes à satisfaire est vide. Puisque nous en sommes arrivés là, nous avons trouvé une solution complète au problème initial : installer les versions trouvées jusqu'ici est possible.
La pile de récursivité est remontée en indiquant que le chemin trouvé dans le graphe des dépendances convient.