Son rôle est important : à l'aide du registre des installations, il sait s'il faut installer ou non une version. Il a son propre fichier et peut être demandé par n'importe quelle action qui souhaiterait installer des paquets. Ce n'est plus le dépôt qui est en charge d'installer le paquet, mais bien l'installateur. ReinstallAction devient FetchAction pour mieux correspondre à son usage : installer des dépendances qu'il manquerait sans réinstaller ce qui est déjà là. La fonction fetch() est template pour fonctionner avec les deux héritages de la classe package::DownloadableVersion sans utiliser de fonction virtuelle ou de pointeur. Fix #6 Implement #28
11 KiB
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.
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
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
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
+getName() 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 différentes stratégies 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. 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.
Types des versions
Chaque version d'un paquet est décrite par ses métadonnées et son contenu dans une archive. Elle intervient à différents niveaux de l'application :
- lecture du fichier manifeste
- lecture du fichier verrou
- récupération des données détaillées à partir du dépôt distant
- installation d'un paquet
Tous ces composants n'ont pas besoin du même niveau de détails sur une version, c'est pourquoi ont été conçus plusieurs types Version :
classDiagram
class Version {
+major: int
+minor: int
+patch: int
}
Version "2" <-- Range
class Downloadable {
-downloadUrl: string
-checksum: string
}
Version <|-- DownloadableVersion
Downloadable <|-- DownloadableVersion
DownloadableVersion <|-- VersionLock
DownloadableVersion <|-- PackageVersion
class VersionLock {
-dependencies: vector~string~
}
class PackageVersion {
-publishedDate: string
-dependencies: map~string, Range~
}
Tout d'abord, Version
représente une version selon la sémantique semver.
Un intervalle Range
modélise un ensemble de versions compatibles avec une version minimale et une version maximale.
Une version doit pouvoir être téléchargée, c'est pourquoi DownloadableVersion
hérite de la classe Downloadable
.
Enfin, alors que le fichier verrou n'a besoin que de connaître les noms des dépendances dans VersionLock
, la résolution des dépendances et l'affichage détaillé d'une version demande également la contrainte de version, grâce à la classe PackageVersion
.
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.
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 pour parcourir de manière exhaustive toutes les combinaisons possibles.
Supposons les dépendances suivantes :
graph TD
root-->config
root-->h2
config-->json
Chaque paquet peut être résumé sommairement par sa liste de dépendances, par exemple :
[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[ eth2
∈ [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.