Sommaire
- Projet d'exemple : mymathserver
- Packager une dépendance manquante
- Packager le projet
- Lancer un environnement virtuel
- Construire et installer le paquet du projet
- Fixer une version
- Intégration continue
- Cache binaire
- Construire et déployer une image Docker
- Construire et déployer une machine virtuelle
- Personnaliser des paquets existants
- Conclusion
Nix et GNU Guix sont des gestionnaires de paquets "fonctionnels", au sens de la programmation fonctionnelle. Cette approche de la gestion de paquets est très différente de l'approche habituellement utilisée par les sytèmes Linux ou BSD, à base de collections de ports ou de dépôts de paquets.
Cette approche fonctionnelle apporte de nombreux avantages. Non seulement elle permet de fournir une gestion de paquet fiable, reproductible, multi-version et multi-utilisateur, mais apporte également de nombreuses fonctionnalités supplémentaires : gestion d'un environnement de développement, packaging décentralisé, construction d'images Docker ou de machines virtuelles, personnalisation de tout l'environnement logiciel, etc.
Cet article part d'un projet de code (un serveur web) et illustre progressivement différentes fonctionnalités de Nix intéressante pour le développeur, l'empaqueteur et l'administrateur système. Les exemples sont présentés ici sous NixOS (la distribution Linux basée sur Nix) mais devraient être utilisables également avec l'outil Nix installé sur une distribution Linux classique, ou avec GNU Guix.
Voir aussi : projet d'exemple, video youtube, video peertube
Projet d'exemple : mymathserver
Le projet mymathserver est un serveur web. La route "/" retourne un texte d'accueil et la route "/mul2" permet de multiplier un entier par 2.
Le projet est codé en C++. Il contient une bibliothèque de base src/mymath.hpp
:
#pragma once
int mul2(int x) {
return 2*x;
}
Un exécutable de tests unitaires src/mymathtest.cpp
(on ne sait jamais):
#include <gtest/gtest.h>
#include "mymath.hpp"
TEST(Mymath, mul2_1) {
ASSERT_EQ(0, mul2(0));
}
// ...
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Et enfin le serveur web src/mymathserver.cpp
:
#include "mymath.hpp"
#include <cpprest/http_listener.h>
// ...
class App : public web::http::experimental::listener::http_listener {
private:
void handleGet(web::http::http_request req) {
// ...
if (path == "/") {
// ...
}
else if (splitPath.size() == 2 and splitPath[0] == "mul2") {
// ...
}
else {
req.reply(web::http::status_codes::NotFound);
}
}
public:
App(std::string url) : web::http::experimental::listener::http_listener(url) {
support(web::http::methods::GET,
bind(&App::handleGet, this, std::placeholders::_1));
}
};
int main() {
const char * portEnv = std::getenv("PORT");
const std::string port = portEnv == nullptr ? "3000" : portEnv;
const std::string address = "http://0.0.0.0:" + port;
App app(address);
// ...
return 0;
}
Le tout est configuré, de façon très classique, avec un CMakeLists.txt
:
cmake_minimum_required( VERSION 3.0 )
project( mymathserver )
find_package( GTest REQUIRED )
add_executable( mymathtest src/mymathtest.cpp )
target_include_directories( mymathtest PRIVATE ${GTEST_INCLUDE_DIRS} )
target_link_libraries( mymathtest
${GTEST_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} )
find_package( Boost REQUIRED system )
find_package ( Threads REQUIRED )
find_package ( OpenSSL REQUIRED )
add_executable( mymathserver src/mymathserver.cpp )
target_link_libraries( mymathserver PRIVATE
cpprest ${Boost_LIBRARIES} ${OPENSSL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} )
install( TARGETS mymathserver mymathtest DESTINATION bin )
Packager une dépendance manquante
On a donc le programme de tests unitaires mymathtest
, qui utilise la bibliothèque gtest
, et le programme serveur mymathserver
, qui utilise boost
, openssl
et cpprestsdk. Toutes ces dépendances sont classiques et donc présentes dans la plupart des logithèques Linux. Cependant cpprestsdk
est beaucoup moins connue que les autres dépendances et risque de manquer lors de la compilation de notre projet. Pour éviter ce problème, on peut laisser l'utilisateur installer ou compiler cpprestsdk
lui-même (ce qui peut être compliqué ou fastidieux), ou intégrer cpprestsdk
directement à notre projet via les sous-modules git
(ce qui est peu efficace et rend le projet dépendant de git
).
Avec Nix, on peut créer nous-même un paquet pour cpprestsdk
. Certaines bibliothèques peuvent être complexes à empaqueter mais généralement, cela se résume à indiquer l'adresse du code source et la liste des dépendances (nix/cpprestsdk.nix
) :
{ stdenv, fetchFromGitHub, cmake, boost, openssl, websocketpp, zlib }:
stdenv.mkDerivation {
name = "cpprestsdk";
src = fetchFromGitHub {
owner = "Microsoft";
repo = "cpprestsdk";
rev = "v2.10.14";
sha256 = "0z1yblqszs7ig79l6lky02jmrs8zmpi7pnzns237p0w59pipzrvs";
};
buildInputs = [ boost cmake openssl websocketpp zlib ];
}
On remarquera qu'on n'a écrit aucune directive de compilation. En effet, comme on a indiqué cmake
dans les dépendances de cpprestsdk
, Nix sait qu'il faut compiler avec les commandes cmake
classiques.
Packager le projet
De la même façon qu'on a empaqueté cpprestsdk
, on peut créer un fichier nix/mymathserver.nix
pour empaqueter notre projet :
{ stdenv, cpprestsdk, boost, cmake, gtest, openssl }:
stdenv.mkDerivation {
name = "mymathserver";
version = "0.1";
src = ../.;
buildInputs = [ boost cmake cpprestsdk gtest openssl ];
}
De même que pour cpprestsdk
, comme on a mis cmake
dans les dépendances, Nix sait qu'il faut utiliser cmake
et notre fichier CMakeLists.txt
pour compiler le projet.
Pour l'instant, Nix ne connait pas le code exact des dépendances. Il connait juste les noms, et ce sont des paramètres du paquet (la première ligne du fichier). D'où la fameuse "approche fonctionnelle" de Nix.
Pour terminer, on écrit un fichier default.nix
, qui est le point d'entrée de notre configuration de projet et fait le lien avec les fichiers précédents :
{ pkgs ? import <nixpkgs> {} }:
let
cpprestsdk = pkgs.callPackage ./nix/cpprestsdk.nix {};
mymathserver = pkgs.callPackage ./nix/mymathserver.nix { inherit cpprestsdk; };
in mymathserver
Ce fichier a également un paramètre (pkgs
) mais avec une valeur par défaut (la logithèque système nixpkgs
). Il suffit alors d'appeler nos paquets cpprestsdk
et mymathserver
sur la "logithèque" pkgs
. Les paramètres des paquets sont fixés aux valeurs présentes dans pkgs
, sauf pour le paramètre cpprestsdk
dans mymathserver
où c'est le paquet créé juste avant qui est utilisé (grâce au inherit
). Ainsi, le default.nix
permet de fournir concrètement le code à utiliser à travers les paramètres des descriptions de paquets précédentes, et donc de construire réellement les paquets correspondants.
On notera qu'on aurait pu se passer du fichier nix/mymathserver.nix
et tout mettre dans le fichier default.nix
. L'avantage d'utiliser deux fichiers est de rendre notre packaging plus modulaire. Par exemple, on pourrait quasiment intégrer directement nix/mymathserver.nix
dans le dépôt nixpkgs et notre projet serait disponible dans la logithèque Nix officielle !
Lancer un environnement virtuel
A partir de notre fichier default.nix
, on peut lancer un environnement virtuel :
nix-shell
Ceci installe les dépendances, compile cpprestsdk
et initialise l'environnement. On peut alors travailler sur le code du projet et le compiler avec les commandes cmake
classiques :
mkdir mybuild
cd mybuild
cmake ..
make
./mymathserver
Construire et installer le paquet du projet
Le fichier default.nix
permet également de construire automatiquement le paquet du projet :
nix-build
./result/bin/mymathserver
On peut également installer notre projet comme un logiciel classique de la logithèque système.
nix-env -i f .
mymathserver
Et on peut même installer en récupérant directement une archive du dépôt git distant :
nix-env -if "https://gitlab.com/nokomprendo/mymathserver/-/archive/master/mymathserver-master.tar.gz"
Fixer une version
Le fichier default.nix
construit le projet à partir de la logithèque système, qui peut varier dans le temps ou selon les utilisateurs. Nix permet de fixer une logithèque précise, et donc d'avoir un paquet reproductible. Par exemple avec le fichier nix/release.nix
:
let
rev = "1c92cdaf7414261b4a0e0753ca9383137e6fef06";
pkgs-src = fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz";
sha256 = "0d3fqa1aqralj290n007j477av54knc48y1bf1wrzzjic99cykwh";
};
pkgs = import pkgs-src {};
in pkgs.callPackage ../default.nix {}
Encore une fois, c'est l'approche fonctionnelle de Nix qui permet de composer les fichiers de configuration entre eux. Ici nix/release.nix
se contente de récupèrer un pkgs
particulier et s'en sert comme paramètre de default.nix
pour construire tout le projet et ses dépendances à partir de ce pkgs
particulier.
On peut alors construire ce paquet reproductible avec la commande :
nix-build nix/release.nix
Intégration continue
Comme notre configuration Nix décrit complètement l'environnement et la construction de notre projet, il est très pratique de l'utiliser dans un processus d'intégration continue. Par exemple, avec gitlab-ci
, on peut ajouter le fichier .gitlab-ci.yml
suivant pour lancer la compilation du projet et les tests unitaires.
build:
image: nixos/nix
script:
- nix-build nix/release.nix
- ./result/bin/mymathtest
Cache binaire
Nix permet d'utiliser des paquets de binaires pour éviter d'avoir à tout compiler soi-même. Ainsi, la logithèque nixpkgs
fournit un cache binaire. Cependant, cpprestsdk
n'est pas disponible dans ce cache car on l'a packagé nous-même. Nix propose des outils pour mettre en place des serveurs d'intégration continue et de cache mais ceci est assez lourd à mettre en place.
Une autre possibilité est d'utiliser le service de cache cachix, qui permet d'uploader des paquets Nix compilés puis de les télécharger sur un autre système.
L'utilisation de cachix est très simple. Il suffit de créer un compte et un dépôt de cache. On peut ensuite envoyer des paquets binaires avec la commande cachix push
et activer le téléchargement depuis un dépôt de cache avec la commande cachix use
.
Par exemple, pour accélerer l'intégration continue précédente, on peut construire cpprestsdk
avec notre configuration release
, uploader les paquets binaires correspondants dans le dépôt nokomprendo
puis utiliser ce cache dans le processus d'intégration continue (.gitlab-ci.yml
) :
build:
image: nixos/nix
script:
- nix-env -iA nixpkgs.cachix
- cachix use nokomprendo
- nix-build nix/release.nix
- ./result/bin/mymathtest
Ceci fait passer l'exécution d'un processus d'intégration continue de 20 minutes à moins de 2 minutes.
Construire et déployer une image Docker
Nix permet de construire des images Docker. Par exemple, le fichier nix/docker.nix
suivant définit une image Docker à partir de notre configuration release
:
{ pkgs ? import <nixpkgs> {} }:
let
mymathserver = import ./release.nix;
entrypoint = pkgs.writeScript "entrypoint.sh" ''
#!${pkgs.stdenv.shell}
$@
'';
in pkgs.dockerTools.buildImage {
name = "mymathserver";
tag = "latest";
config = {
WorkingDir = "${mymathserver}";
Entrypoint = [ entrypoint ];
Cmd = [ "${mymathserver}/bin/mymathserver" ];
};
}
On peut alors construire l'image, la charger dans Docker et la tester :
nix-build nix/docker.nix
docker load < result
docker run --rm -it -p 3000:3000 mymathserver:latest
Cette image Docker peut être déployée, par exemple sur Heroku :
heroku login
heroku container:login
heroku create nokomprendo-mymathserver
docker tag mymathserver:latest registry.heroku.com/nokomprendo-mymathserver/web
docker push registry.heroku.com/nokomprendo-mymathserver/web
heroku container:release web --app nokomprendo-mymathserver
heroku logs --tail
L'application est alors accessible à l'adresse http://nokomprendo-mymathserver.herokuapp.com
Construire et déployer une machine virtuelle
Nix permet de définir et de déployer des machines virtuelles, avec nixops
. Par exemple, le fichier nix/virtualbox.nix
suivant reprend notre programme mymathserver
, le lance dans un service systemd
et déploie le tout dans une virtualbox
:
{
network.description = "mynetwork";
myserver = { config, pkgs, ... }:
let
myapp = import ./release.nix;
in {
networking.firewall.allowedTCPPorts = [ 3000 ];
systemd.services.myservice = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
script = "${myapp}/bin/mymathserver";
};
deployment = {
targetEnv = "virtualbox";
virtualbox = {
memorySize = 512;
vcpu = 1;
headless = true;
};
};
};
}
Les commandes suivantes créent et déploient la machine virtuelle sous le nom myvm
:
nixops create -d myvm nix/virtualbox.nix
nixops deploy -d myvm --force-reboot
A l'issu du déploiement, une adresse IP est fournie et permet d'accèder à notre serveur sur la machine virtuelle.
Personnaliser des paquets existants
Enfin, une fonctionnalité originale de Nix, liée à son "approche fonctionnelle", est la possibilité de modifier les paramètres des paquets et de répercuter automatiquement et efficacement ces modifications dans l'ensemble de l'environnement logiciel.
Les paquets peuvent être surchargés, c'est-à-dire remplacés par des versions modifiées. Par exemple, le paquet de la bibliothèque zlib contient un attribut configureFlags
qui contient les options de compilation à utiliser. Si on veut utiliser une version de zlib
compilée avec l'option --zprefix
, on peut utiliser le fichier nix/custom0.nix
suivant :
let
pkgs = import <nixpkgs> {};
zlib = pkgs.zlib.overrideDerivation (attrs: {
configureFlags = [ attrs.configureFlags "--zprefix" ];
});
cpprestsdk = pkgs.callPackage ./cpprestsdk.nix { inherit zlib; };
mymathserver = pkgs.callPackage ./mymathserver.nix { inherit cpprestsdk; };
in pkgs.stdenv.mkDerivation rec {
name = "mymathserver-custom0";
src = ./.;
buildPhase = "";
installPhase = ''
mkdir -p $out/bin
cp ${mymathserver}/bin/mymathserver $out/bin/${name}
'';
}
Les paramètres des paquets Nix peuvent également contenir des options paramétrables. Par exemple, la bibliothèque openssl a une option enableSSL2
, qu'on peut spécifier comme dans le fichier nix/custom1.nix
suivant :
let
pkgs = import <nixpkgs> {};
openssl = pkgs.openssl.override { enableSSL2 = true; };
cpprestsdk = pkgs.callPackage ./cpprestsdk.nix { inherit openssl; };
mymathserver = pkgs.callPackage ./mymathserver.nix {
inherit cpprestsdk;
inherit openssl;
};
in pkgs.stdenv.mkDerivation rec {
name = "mymathserver-custom1";
src = ./.;
buildPhase = "";
installPhase = ''
mkdir -p $out/bin
cp ${mymathserver}/bin/mymathserver $out/bin/${name}
'';
}
Enfin, pour éviter tout risque d'incompatibilité d'ABI, on peut surcharger la configuration lors du chargement de la logithèque. Ainsi, tous les paquets dépendant des paquets surchargés seront recompilés en prenant en compte ces modifications. Par exemple, le fichier nix/custom2.nix
suivant surcharge openssl
via la configuration de la logithèque (il existe aussi un système d'overlays pour cela) :
let
config = {
packageOverrides = pkgs: {
openssl = pkgs.openssl.override {
enableSSL2 = true;
};
};
};
pkgs = import <nixpkgs> { inherit config; };
mymathserver = pkgs.callPackage ../default.nix {};
in pkgs.stdenv.mkDerivation rec {
name = "mymathserver-custom2";
src = ./.;
buildPhase = "";
installPhase = ''
mkdir -p $out/bin
cp ${mymathserver}/bin/mymathserver $out/bin/${name}
'';
}
Nix installe chaque paquet dans un dossier spécifique ce qui signifie qu'on peut utiliser en même temps plusieurs versions ou plusieurs configurations d'un même logiciel. Par exemple, dans les trois fichiers de personnalisation précédents, on a renommé l'exécutable mymathserver
produit. On peut donc les installer et les exécuter en même temps dans l'environnement utilisateur courant :
nix-env -i -f nix/custom0.nix
nix-env -i -f nix/custom1.nix
nix-env -i -f nix/custom2.nix
mymathserver-custom0
...
Conclusion
L'approche fonctionnelle de Nix constitue une base solide pour la gestion de paquet. Elle permet de construire des paquets de façon fiable, reproductible et personnalisable. Sur cette base, Nix propose également des fonctionnalités intéressantes pour le développeur, l'empaqueteur et l'administrateur système.
Ainsi, en 120 lignes de code Nix, le projet d'exemple mymathserver
:
- package une dépendance manquante (
nix/cpprestsdk.nix
) - package le projet de base (
nix/mymathserver.nix
) - définit un point d'entrée pour créer des paquets et des environnements
virtuels (
default.nix
) - définit une release (
nix/release.nix
) - définit une image Docker (
nix/docker.nix
) - définit une machine virtuelle (
nix/virtualbox.nix
) - définit trois personnalisations différentes de l'environnement logiciel
(
nix/custom0.nix
, etc) - permet de réaliser de l'intégration continue, d'utiliser un cache binaire, d'installer le projet depuis une archive gitlab, etc.
mymathserver/
├── CMakeLists.txt
├── default.nix
├── nix
│ ├── cpprestsdk.nix
│ ├── custom0.nix
│ ├── custom1.nix
│ ├── custom2.nix
│ ├── docker.nix
│ ├── mymathserver.nix
│ ├── release.nix
│ └── virtualbox.nix
└── src
├── mymath.hpp
├── mymathserver.cpp
└── mymathtest.cpp
# Journal proposé en dépêche
Posté par ZeroHeure . Évalué à 4.
Ni une, ni deux, la modération a transformé ce journal en dépêche. C'est en cours de validation.
"La liberté est à l'homme ce que les ailes sont à l'oiseau" Jean-Pierre Rosnay
# Sympa
Posté par mothsART . Évalué à 1. Dernière modification le 24 janvier 2020 à 13:45.
Comme toujours, très édifiant.
Peux-être une question bête car j'ai peu d'expérience sur l'emquetage de projet en C++ : Les dépendances "boost, openssl, websocketpp, zlib" sont obligatoires ? Qu'elle est ton cheminement pour les trouver ?
[^] # Re: Sympa
Posté par nokomprendo (site web personnel) . Évalué à 2.
Oui, ce sont des dépendances demandées par le framework cpprestsdk utilisé ici. Pour les trouver, il suffit de regarder la doc de cpprestsdk (ou son fichier de config). Au pire, la compilation échoue en indiquant la dépendance manquante mais c'est quand même mieux de regarder la doc…
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.